diff --git a/backend/extension-src/README.md b/backend/extension-src/README.md index 75aa05c..26d3e85 100644 --- a/backend/extension-src/README.md +++ b/backend/extension-src/README.md @@ -46,6 +46,15 @@ running the [`tablet-checkin`](../tablet-checkin) app. 4. Click the extension icon → **Open Settings** (or right-click the icon → Options) → enter the tablet-checkin base URL and the terminal key → Save → Test connection. +## Versioning + +Bump `manifest.json`'s `"version"` field every time anything in this folder changes. +Nothing does this automatically. The popup shows its own installed version in a small +footer; tablet-checkin's Terminals page shows the latest bundled version next to the +download link (`GET /api/extension/version`, reads this same file) — that comparison is +how staff tell an already-installed copy on a reception PC is stale and needs +re-downloading, so a missed bump silently breaks that signal. + ## Known TODOs before this is production-ready - **Row-detection selectors on `bookings_lists_checkin` are built from a single sample diff --git a/backend/extension-src/manifest.json b/backend/extension-src/manifest.json index 2ca4eca..3a9c3d1 100644 --- a/backend/extension-src/manifest.json +++ b/backend/extension-src/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Send to Check-In Tablet", - "version": "0.1.0", + "version": "0.2.0", "description": "Pushes the currently open NewBook booking to a check-in tablet — standalone from NewBook Assistant, see project_checkin memory.", "permissions": ["cookies", "storage", "activeTab", "webNavigation"], diff --git a/backend/extension-src/popup/popup.css b/backend/extension-src/popup/popup.css index 860f358..4f3f82a 100644 --- a/backend/extension-src/popup/popup.css +++ b/backend/extension-src/popup/popup.css @@ -97,3 +97,7 @@ a.settings-link { display: block; margin-top: 12px; font-size: 12px; color: #7b4 padding: 4px 10px; border-radius: 6px; cursor: pointer; } .send-small:disabled { opacity: 0.5; cursor: default; } + +.version { + margin-top: 10px; text-align: center; font-size: 10px; color: #94a3b8; +} diff --git a/backend/extension-src/popup/popup.html b/backend/extension-src/popup/popup.html index ec0b483..5d98889 100644 --- a/backend/extension-src/popup/popup.html +++ b/backend/extension-src/popup/popup.html @@ -15,6 +15,8 @@

Loading…

+
+ diff --git a/backend/extension-src/popup/popup.js b/backend/extension-src/popup/popup.js index 3e9ecda..a7d0ae5 100644 --- a/backend/extension-src/popup/popup.js +++ b/backend/extension-src/popup/popup.js @@ -9,6 +9,15 @@ const BOOKING_URL_RE = /\/bookings_(?:view|edit|checkin)\/(\d+)/i; const contentEl = document.getElementById('content'); const nbStatusEl = document.getElementById('nbStatus'); +const versionEl = document.getElementById('version'); + +// Shown so staff/admin can tell at a glance whether a given reception PC is +// running an old install — compare against the "Latest version" shown next +// to the download link on tablet-checkin's own Terminals page (see +// routes/extension.js's /api/extension/version). Pulled from the manifest +// rather than hardcoded here, so it can never itself drift out of sync with +// manifest.json's own version field. +versionEl.textContent = `v${chrome.runtime.getManifest().version}`; async function main() { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); diff --git a/backend/src/routes/extension.js b/backend/src/routes/extension.js index edd80cf..0e9cdae 100644 --- a/backend/src/routes/extension.js +++ b/backend/src/routes/extension.js @@ -1,4 +1,5 @@ import path from 'path' +import { readFileSync } from 'fs' import { fileURLToPath } from 'url' import archiver from 'archiver' import { requireAuth, requireCap } from '../auth.js' @@ -14,6 +15,19 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)) const EXTENSION_SRC = path.join(__dirname, '..', '..', 'extension-src') export async function extensionRoutes(app) { + // GET /api/extension/version — read straight from manifest.json rather than + // a separately-maintained value, so this can never itself drift out of sync + // with what the download link actually serves. Shown next to that link on + // the Terminals page so staff can tell an already-installed copy (its own + // version shown in the extension's popup footer) is behind the latest + // bundled one and should be re-downloaded. Bump manifest.json's "version" + // field on every change to backend/extension-src/ — nothing else does this + // automatically. + app.get('/api/extension/version', { preHandler: [requireAuth, requireCap('settings')] }, async () => { + const manifest = JSON.parse(readFileSync(path.join(EXTENSION_SRC, 'manifest.json'), 'utf8')) + return { version: manifest.version } + }) + // Gated the same as the Settings page it's linked from — any staff who can // see Settings can download this, nothing sensitive lives in the extension // itself (the terminal API key is entered by whoever installs it, not diff --git a/frontend/src/api.ts b/frontend/src/api.ts index d9f3c4e..3bf51a9 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -58,6 +58,7 @@ export function checkinPdfUrl(id: number): string { return `${BASE}/checkins/${i // backend/extension-src), zipped fresh on each request rather than served as // a static file. See project_checkin memory's "Standalone push extension". export function extensionDownloadUrl(): string { return `${BASE}/extension/download` } +export function fetchExtensionVersion(): Promise<{ version: string }> { return request('/extension/version') } // Marketing export function fetchMarketingProviders(): Promise { return request('/marketing/providers') } diff --git a/frontend/src/pages/Terminals.tsx b/frontend/src/pages/Terminals.tsx index 04abba5..295c4ab 100644 --- a/frontend/src/pages/Terminals.tsx +++ b/frontend/src/pages/Terminals.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react' import { RefreshCw, KeyRound, Ban, Trash2, Copy, Download } from 'lucide-react' -import { fetchTerminals, createTerminal, revokeTerminal, deleteTerminal, extensionDownloadUrl } from '../api' +import { fetchTerminals, createTerminal, revokeTerminal, deleteTerminal, extensionDownloadUrl, fetchExtensionVersion } from '../api' import type { Terminal } from '../types' export default function Terminals() { @@ -10,6 +10,7 @@ export default function Terminals() { const [newName, setNewName] = useState('') const [creating, setCreating] = useState(false) const [revealedKey, setRevealedKey] = useState<{ name: string; key: string } | null>(null) + const [extensionVersion, setExtensionVersion] = useState('') function load() { setLoading(true) @@ -20,6 +21,7 @@ export default function Terminals() { } useEffect(load, []) + useEffect(() => { fetchExtensionVersion().then(v => setExtensionVersion(v.version)).catch(() => {}) }, []) async function handleCreate(e: React.FormEvent) { e.preventDefault() @@ -77,9 +79,12 @@ export default function Terminals() { select the unzipped folder. It'll need a terminal key from below to actually authorize pushes — create one first if you haven't already.

- - Download Extension (.zip) - +
+ + Download Extension (.zip) + + {extensionVersion && Latest version: {extensionVersion}} +
{error &&
{error}
}