Extension: show installed/latest version for drift tracking
Popup footer shows its own installed version (from manifest.json, via chrome.runtime.getManifest()); Terminals page shows the latest bundled version next to the download link (new GET /api/extension/version, reads the same manifest.json). Bumped manifest.json to 0.2.0 for the Today's Arrivals change, and documented in extension-src/README.md that every future change to this folder must bump the version too — nothing else keeps it in sync automatically.
This commit is contained in:
parent
ebf9875ca8
commit
790431c446
8 changed files with 49 additions and 5 deletions
|
|
@ -46,6 +46,15 @@ running the [`tablet-checkin`](../tablet-checkin) app.
|
||||||
4. Click the extension icon → **Open Settings** (or right-click the icon → Options) →
|
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.
|
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
|
## Known TODOs before this is production-ready
|
||||||
|
|
||||||
- **Row-detection selectors on `bookings_lists_checkin` are built from a single sample
|
- **Row-detection selectors on `bookings_lists_checkin` are built from a single sample
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Send to Check-In Tablet",
|
"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.",
|
"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"],
|
"permissions": ["cookies", "storage", "activeTab", "webNavigation"],
|
||||||
|
|
|
||||||
|
|
@ -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;
|
padding: 4px 10px; border-radius: 6px; cursor: pointer;
|
||||||
}
|
}
|
||||||
.send-small:disabled { opacity: 0.5; cursor: default; }
|
.send-small:disabled { opacity: 0.5; cursor: default; }
|
||||||
|
|
||||||
|
.version {
|
||||||
|
margin-top: 10px; text-align: center; font-size: 10px; color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@
|
||||||
<p class="muted">Loading…</p>
|
<p class="muted">Loading…</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="version" class="version"></div>
|
||||||
|
|
||||||
<script type="module" src="popup.js"></script>
|
<script type="module" src="popup.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,15 @@ const BOOKING_URL_RE = /\/bookings_(?:view|edit|checkin)\/(\d+)/i;
|
||||||
|
|
||||||
const contentEl = document.getElementById('content');
|
const contentEl = document.getElementById('content');
|
||||||
const nbStatusEl = document.getElementById('nbStatus');
|
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() {
|
async function main() {
|
||||||
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
import { readFileSync } from 'fs'
|
||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
import archiver from 'archiver'
|
import archiver from 'archiver'
|
||||||
import { requireAuth, requireCap } from '../auth.js'
|
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')
|
const EXTENSION_SRC = path.join(__dirname, '..', '..', 'extension-src')
|
||||||
|
|
||||||
export async function extensionRoutes(app) {
|
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
|
// 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
|
// see Settings can download this, nothing sensitive lives in the extension
|
||||||
// itself (the terminal API key is entered by whoever installs it, not
|
// itself (the terminal API key is entered by whoever installs it, not
|
||||||
|
|
|
||||||
|
|
@ -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
|
// backend/extension-src), zipped fresh on each request rather than served as
|
||||||
// a static file. See project_checkin memory's "Standalone push extension".
|
// a static file. See project_checkin memory's "Standalone push extension".
|
||||||
export function extensionDownloadUrl(): string { return `${BASE}/extension/download` }
|
export function extensionDownloadUrl(): string { return `${BASE}/extension/download` }
|
||||||
|
export function fetchExtensionVersion(): Promise<{ version: string }> { return request('/extension/version') }
|
||||||
|
|
||||||
// Marketing
|
// Marketing
|
||||||
export function fetchMarketingProviders(): Promise<MarketingProvider[]> { return request('/marketing/providers') }
|
export function fetchMarketingProviders(): Promise<MarketingProvider[]> { return request('/marketing/providers') }
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { RefreshCw, KeyRound, Ban, Trash2, Copy, Download } from 'lucide-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'
|
import type { Terminal } from '../types'
|
||||||
|
|
||||||
export default function Terminals() {
|
export default function Terminals() {
|
||||||
|
|
@ -10,6 +10,7 @@ export default function Terminals() {
|
||||||
const [newName, setNewName] = useState('')
|
const [newName, setNewName] = useState('')
|
||||||
const [creating, setCreating] = useState(false)
|
const [creating, setCreating] = useState(false)
|
||||||
const [revealedKey, setRevealedKey] = useState<{ name: string; key: string } | null>(null)
|
const [revealedKey, setRevealedKey] = useState<{ name: string; key: string } | null>(null)
|
||||||
|
const [extensionVersion, setExtensionVersion] = useState('')
|
||||||
|
|
||||||
function load() {
|
function load() {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
@ -20,6 +21,7 @@ export default function Terminals() {
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(load, [])
|
useEffect(load, [])
|
||||||
|
useEffect(() => { fetchExtensionVersion().then(v => setExtensionVersion(v.version)).catch(() => {}) }, [])
|
||||||
|
|
||||||
async function handleCreate(e: React.FormEvent) {
|
async function handleCreate(e: React.FormEvent) {
|
||||||
e.preventDefault()
|
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
|
select the unzipped folder. It'll need a terminal key from below to actually authorize
|
||||||
pushes — create one first if you haven't already.
|
pushes — create one first if you haven't already.
|
||||||
</p>
|
</p>
|
||||||
<a className="btn" href={extensionDownloadUrl()} style={{ display: 'inline-flex', alignItems: 'center', gap: 6, textDecoration: 'none' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||||
<Download size={16} strokeWidth={1.75} /> Download Extension (.zip)
|
<a className="btn" href={extensionDownloadUrl()} style={{ display: 'inline-flex', alignItems: 'center', gap: 6, textDecoration: 'none' }}>
|
||||||
</a>
|
<Download size={16} strokeWidth={1.75} /> Download Extension (.zip)
|
||||||
|
</a>
|
||||||
|
{extensionVersion && <span className="field-hint">Latest version: {extensionVersion}</span>}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <div className="error-banner">{error}</div>}
|
{error && <div className="error-banner">{error}</div>}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue