diff --git a/apps/machine/electron/main.ts b/apps/machine/electron/main.ts index 875cf73..1ca4635 100644 --- a/apps/machine/electron/main.ts +++ b/apps/machine/electron/main.ts @@ -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. diff --git a/apps/machine/electron/preload.ts b/apps/machine/electron/preload.ts index 17f3ab4..ccc5760 100644 --- a/apps/machine/electron/preload.ts +++ b/apps/machine/electron/preload.ts @@ -79,6 +79,10 @@ contextBridge.exposeInMainWorld('electronAPI', { remediateTransaction: (txid: string, remediatedByTxid: string): Promise => 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 emptyCashbox: () => Promise remediateTransaction: (txid: string, remediatedByTxid: string) => Promise + getSupportPages: () => Promise<{ id: string; title: string; content: string }[]> // HAL hardware IPC halInit: (config: any) => Promise<{ success: boolean; error?: string }> halDispense: (amounts: any) => Promise diff --git a/apps/machine/package.json b/apps/machine/package.json index d7765f2..4099ef5 100644 --- a/apps/machine/package.json +++ b/apps/machine/package.json @@ -27,11 +27,13 @@ "@lamassu/nostr-client": "workspace:*", "@lamassu/state-machine": "workspace:*", "@scure/base": "^1.2.0", + "@tanstack/vue-table": "^8.21.3", "@vueuse/core": "^14.1.0", "better-sqlite3": "^11.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-vue-next": "^0.563.0", + "marked": "^17.0.5", "nostr-tools": "^2.10.0", "pinia": "^2.2.0", "qrcode.vue": "^3.6.0", diff --git a/apps/machine/src/App.vue b/apps/machine/src/App.vue index c0d0bb1..098457c 100644 --- a/apps/machine/src/App.vue +++ b/apps/machine/src/App.vue @@ -1,5 +1,6 @@ + + diff --git a/apps/machine/src/components/ui/accordion/AccordionContent.vue b/apps/machine/src/components/ui/accordion/AccordionContent.vue new file mode 100644 index 0000000..9e73edb --- /dev/null +++ b/apps/machine/src/components/ui/accordion/AccordionContent.vue @@ -0,0 +1,23 @@ + + + diff --git a/apps/machine/src/components/ui/accordion/AccordionItem.vue b/apps/machine/src/components/ui/accordion/AccordionItem.vue new file mode 100644 index 0000000..9bad146 --- /dev/null +++ b/apps/machine/src/components/ui/accordion/AccordionItem.vue @@ -0,0 +1,24 @@ + + + diff --git a/apps/machine/src/components/ui/accordion/AccordionTrigger.vue b/apps/machine/src/components/ui/accordion/AccordionTrigger.vue new file mode 100644 index 0000000..ff72250 --- /dev/null +++ b/apps/machine/src/components/ui/accordion/AccordionTrigger.vue @@ -0,0 +1,34 @@ + + + diff --git a/apps/machine/src/components/ui/accordion/index.ts b/apps/machine/src/components/ui/accordion/index.ts new file mode 100644 index 0000000..9340ac0 --- /dev/null +++ b/apps/machine/src/components/ui/accordion/index.ts @@ -0,0 +1,4 @@ +export { default as Accordion } from './Accordion.vue' +export { default as AccordionContent } from './AccordionContent.vue' +export { default as AccordionItem } from './AccordionItem.vue' +export { default as AccordionTrigger } from './AccordionTrigger.vue' diff --git a/apps/machine/src/components/ui/table/Table.vue b/apps/machine/src/components/ui/table/Table.vue new file mode 100644 index 0000000..669b0d4 --- /dev/null +++ b/apps/machine/src/components/ui/table/Table.vue @@ -0,0 +1,16 @@ + + + diff --git a/apps/machine/src/components/ui/table/TableBody.vue b/apps/machine/src/components/ui/table/TableBody.vue new file mode 100644 index 0000000..95d50be --- /dev/null +++ b/apps/machine/src/components/ui/table/TableBody.vue @@ -0,0 +1,14 @@ + + + diff --git a/apps/machine/src/components/ui/table/TableCaption.vue b/apps/machine/src/components/ui/table/TableCaption.vue new file mode 100644 index 0000000..2c2fdf7 --- /dev/null +++ b/apps/machine/src/components/ui/table/TableCaption.vue @@ -0,0 +1,14 @@ + + + diff --git a/apps/machine/src/components/ui/table/TableCell.vue b/apps/machine/src/components/ui/table/TableCell.vue new file mode 100644 index 0000000..76724f1 --- /dev/null +++ b/apps/machine/src/components/ui/table/TableCell.vue @@ -0,0 +1,22 @@ + + + diff --git a/apps/machine/src/components/ui/table/TableEmpty.vue b/apps/machine/src/components/ui/table/TableEmpty.vue new file mode 100644 index 0000000..96e917c --- /dev/null +++ b/apps/machine/src/components/ui/table/TableEmpty.vue @@ -0,0 +1,32 @@ + + + diff --git a/apps/machine/src/components/ui/table/TableFooter.vue b/apps/machine/src/components/ui/table/TableFooter.vue new file mode 100644 index 0000000..8055c55 --- /dev/null +++ b/apps/machine/src/components/ui/table/TableFooter.vue @@ -0,0 +1,17 @@ + + + diff --git a/apps/machine/src/components/ui/table/TableHead.vue b/apps/machine/src/components/ui/table/TableHead.vue new file mode 100644 index 0000000..2a18567 --- /dev/null +++ b/apps/machine/src/components/ui/table/TableHead.vue @@ -0,0 +1,22 @@ + + + diff --git a/apps/machine/src/components/ui/table/TableHeader.vue b/apps/machine/src/components/ui/table/TableHeader.vue new file mode 100644 index 0000000..ff81dbb --- /dev/null +++ b/apps/machine/src/components/ui/table/TableHeader.vue @@ -0,0 +1,14 @@ + + + diff --git a/apps/machine/src/components/ui/table/TableRow.vue b/apps/machine/src/components/ui/table/TableRow.vue new file mode 100644 index 0000000..7f0cf5a --- /dev/null +++ b/apps/machine/src/components/ui/table/TableRow.vue @@ -0,0 +1,19 @@ + + + diff --git a/apps/machine/src/components/ui/table/index.ts b/apps/machine/src/components/ui/table/index.ts new file mode 100644 index 0000000..2b4ce39 --- /dev/null +++ b/apps/machine/src/components/ui/table/index.ts @@ -0,0 +1,9 @@ +export { default as Table } from './Table.vue' +export { default as TableBody } from './TableBody.vue' +export { default as TableCaption } from './TableCaption.vue' +export { default as TableCell } from './TableCell.vue' +export { default as TableEmpty } from './TableEmpty.vue' +export { default as TableFooter } from './TableFooter.vue' +export { default as TableHead } from './TableHead.vue' +export { default as TableHeader } from './TableHeader.vue' +export { default as TableRow } from './TableRow.vue' diff --git a/apps/machine/src/components/ui/table/utils.ts b/apps/machine/src/components/ui/table/utils.ts new file mode 100644 index 0000000..6be7e61 --- /dev/null +++ b/apps/machine/src/components/ui/table/utils.ts @@ -0,0 +1,8 @@ +import type { Updater } from '@tanstack/vue-table' + +import type { Ref } from 'vue' +import { isFunction } from '@tanstack/vue-table' + +export function valueUpdater(updaterOrValue: Updater, ref: Ref) { + ref.value = isFunction(updaterOrValue) ? updaterOrValue(ref.value) : updaterOrValue +} diff --git a/apps/machine/src/main.ts b/apps/machine/src/main.ts index 83bb576..5f39e0a 100644 --- a/apps/machine/src/main.ts +++ b/apps/machine/src/main.ts @@ -16,6 +16,11 @@ const router = createRouter({ { path: '/', name: 'idle', component: IdleView }, { path: '/cash-out', name: 'cash-out', component: CashOutView }, { path: '/cash-in', name: 'cash-in', component: CashInView }, + { + path: '/support', + name: 'support', + component: () => import('./views/SupportView.vue'), + }, ], }) diff --git a/apps/machine/src/types/electron.d.ts b/apps/machine/src/types/electron.d.ts index 7e13fb7..c0c8de6 100644 --- a/apps/machine/src/types/electron.d.ts +++ b/apps/machine/src/types/electron.d.ts @@ -60,6 +60,7 @@ declare global { }) => Promise emptyCashbox: () => Promise remediateTransaction: (txid: string, remediatedByTxid: string) => Promise + getSupportPages: () => Promise<{ id: string; title: string; content: string }[]> // HAL hardware IPC halInit: (config: any) => Promise<{ success: boolean; error?: string }> halDispense: (amounts: any) => Promise diff --git a/apps/machine/src/views/IdleView.vue b/apps/machine/src/views/IdleView.vue index 7c84f92..f9081e3 100644 --- a/apps/machine/src/views/IdleView.vue +++ b/apps/machine/src/views/IdleView.vue @@ -211,6 +211,16 @@ function handleCashOut() { + + +