feat(machine): add markdown-driven support pages

Add support/help pages accessible via a ? button on the idle screen.
Pages are driven by .md files in /var/lib/lamassu-atm/support/ —
operators can customize content without rebuilding the app.

Features:
- Tabbed view with markdown rendering (via marked)
- Standalone URLs auto-render as QR codes (scannable from phone)
- Table URLs: click-to-reveal QR codes (prevents accidental scans)
- Yes/No rendered as green checkmarks / red X marks
- Wallet comparison table with download QR codes
- FAQ with Lightning-only clarification
- Support page with operator Nostr QR placeholder
- Custodial vs non-custodial footnote
- Large text for touchscreen accessibility
- Centered layout for short-content pages

Closes #36

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-03-28 14:11:54 -04:00
commit d71815a15d
26 changed files with 825 additions and 2 deletions

View file

@ -227,6 +227,29 @@ ipcMain.handle('state:remediate-transaction', (_event, txid: string, remediatedB
remediateTransaction(txid, remediatedByTxid)
)
// Support pages — read .md files from /var/lib/lamassu-atm/support/
ipcMain.handle('support:get-pages', () => {
const supportDir = path.join(
fs.existsSync('/var/lib/lamassu-atm') ? '/var/lib/lamassu-atm' : process.cwd(),
'support'
)
if (!fs.existsSync(supportDir)) return []
const files = fs
.readdirSync(supportDir)
.filter((f) => f.endsWith('.md'))
.sort()
return files.map((file) => {
const content = fs.readFileSync(path.join(supportDir, file), 'utf-8')
const titleMatch = content.match(/^#\s+(.+)/m)
return {
id: file.replace('.md', ''),
title: titleMatch ? titleMatch[1] : file.replace('.md', ''),
content,
}
})
})
// =============================================================================
// HAL Hardware IPC Handlers
// HAL runs in the main process (Node.js) because it needs serialport.

View file

@ -79,6 +79,10 @@ contextBridge.exposeInMainWorld('electronAPI', {
remediateTransaction: (txid: string, remediatedByTxid: string): Promise<boolean> =>
ipcRenderer.invoke('state:remediate-transaction', txid, remediatedByTxid),
// Support pages
getSupportPages: (): Promise<{ id: string; title: string; content: string }[]> =>
ipcRenderer.invoke('support:get-pages'),
// HAL hardware (runs in main process, exposed via IPC)
halInit: (config: any): Promise<{ success: boolean; error?: string }> =>
ipcRenderer.invoke('hal:init', config),
@ -152,6 +156,7 @@ declare global {
}) => Promise<void>
emptyCashbox: () => Promise<void>
remediateTransaction: (txid: string, remediatedByTxid: string) => Promise<boolean>
getSupportPages: () => Promise<{ id: string; title: string; content: string }[]>
// HAL hardware IPC
halInit: (config: any) => Promise<{ success: boolean; error?: string }>
halDispense: (amounts: any) => Promise<any>