From 605dd6cb1b6b5e5dce58282f28d886e1135c2109 Mon Sep 17 00:00:00 2001 From: cproudlock Date: Thu, 27 Aug 2026 10:35:11 -0400 Subject: [PATCH] Kiosk: find a free drive by typing, and use the width of the panel A cabinet of thirty drives was one long column to read through. The free list now filters from a search box and lays out in as many columns as the panel is wide - four on a wall display, two on a tablet, one on a phone - while the steps that hold a single control stay narrow, because stretching one input across a 24 inch screen reads as a broken page. A drive is found by its id, by what is written on it, or by where it lives. The words can arrive in any order, and "locker" is part of what is searched, so "locker engineer" finds the engineering locker - that is how people say it out loud. Filtering is client-side: every free drive is already on the screen, and a round trip per keystroke on a wall panel would be worse than no filter at all. The matcher lives in its own module with tests rather than inside the view. The box takes focus as the picker opens so somebody can simply start typing, and it is cleared when the kiosk resets - the next person at the cabinet must not inherit a filter and conclude there are no drives left. Also: both this view and the guides blocks coloured muted text with a `--textmuted` variable this app has never defined. The browser dropped the declaration, so every hint, subtitle and locker line was rendering at full strength. The variable is `--text-light`. --- CHANGELOG.md | 13 +++ .../frontend/components/GuideBlocks.vue | 2 +- plugins/usb/frontend/drivefilter.js | 29 ++++++ plugins/usb/frontend/drivefilter.spec.js | 62 ++++++++++++ plugins/usb/frontend/views/USBKiosk.vue | 97 ++++++++++++++++--- 5 files changed, 190 insertions(+), 13 deletions(-) create mode 100644 plugins/usb/frontend/drivefilter.js create mode 100644 plugins/usb/frontend/drivefilter.spec.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c895081..5956126e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,12 @@ ADR-007 and ADR-002. ### Fixed +- **Muted text on the USB kiosk and in guides was not muted.** Both named a + `--textmuted` variable this app has never defined, so the browser dropped the + declaration and the hints, subtitles and locker lines rendered at full + strength against the text they were meant to sit behind. The variable is + `--text-light`. + - **Plugin reports rendered a raw JSON dump.** The reports page ran a hardcoded switch over five core report ids and fell through to `console.error('Unknown report')`, and its results panel was a matching chain @@ -77,6 +83,13 @@ ADR-007 and ADR-002. left behind by somebody on leave would then be unreturnable at the kiosk, which just moves the problem to a person with a login. + The list of free drives is filtered from a search box, and lays itself out in + as many columns as the panel is wide - up to four on a wall display. A cabinet + of thirty drives was one long column somebody had to read through. A drive is + found by its id, by what is written on it, or by where it lives, in whatever + word order comes naturally: "locker engineer" finds the engineering locker, + because that is what people say. + - **Audit log retention.** The audit log is append-only and nothing ever removed a row, so it grew for as long as a site ran - here to the point where the audit page was tens of thousands of pages deep and therefore useless, because diff --git a/plugins/guides/frontend/components/GuideBlocks.vue b/plugins/guides/frontend/components/GuideBlocks.vue index d4459517..a0ed0e9b 100644 --- a/plugins/guides/frontend/components/GuideBlocks.vue +++ b/plugins/guides/frontend/components/GuideBlocks.vue @@ -253,7 +253,7 @@ function select (block) { margin: 0; padding: 0.75rem 1rem; border: 1px dashed var(--border); border-radius: 4px; font-size: 0.9rem; } -.gb-imageloading { color: var(--textmuted); } +.gb-imageloading { color: var(--text-light); } .gb-imagefailed { color: var(--danger, #b3261e); } .gb-figure figcaption { font-size: 0.8rem; color: var(--text-light); margin-top: 0.35rem; } diff --git a/plugins/usb/frontend/drivefilter.js b/plugins/usb/frontend/drivefilter.js new file mode 100644 index 00000000..920821e7 --- /dev/null +++ b/plugins/usb/frontend/drivefilter.js @@ -0,0 +1,29 @@ +// Narrowing a cabinet of drives from a kiosk keyboard. +// +// Every free drive is already on the client, so this filters what is on screen +// rather than asking the API again - the list is small, and a round trip per +// keystroke on a wall panel is a worse experience than no filter at all. + +// What one drive can be found by. 'locker' is in here on purpose: it is how +// people say where a drive lives, so "locker engineer" has to find the drives +// in the engineering locker rather than nothing at all. +export function drivetext (drive) { + return [drive.device_id, drive.device_desc, 'locker', drive.locker_location] + .filter(Boolean).join(' ').toLowerCase() +} + +// Every word typed has to appear somewhere in the drive's text, in any order, +// so "engineer kingston" and "kingston engineer" find the same drive. An empty +// query matches everything - a blank box is not a filter. +export function matchesdrive (drive, query) { + const words = String(query || '').trim().toLowerCase().split(/\s+/).filter(Boolean) + if (!words.length) return true + const text = drivetext(drive) + return words.every(word => text.includes(word)) +} + +export function filterdrives (drives, query) { + const words = String(query || '').trim() + if (!words) return drives + return drives.filter(drive => matchesdrive(drive, query)) +} diff --git a/plugins/usb/frontend/drivefilter.spec.js b/plugins/usb/frontend/drivefilter.spec.js new file mode 100644 index 00000000..1ecc3250 --- /dev/null +++ b/plugins/usb/frontend/drivefilter.spec.js @@ -0,0 +1,62 @@ +// The kiosk filter. Every case here is something somebody standing at the +// cabinet would actually type, including the phrasing they use out loud - +// "locker engineer" rather than the bare locker name. + +import { describe, it, expect } from 'vitest' + +import { filterdrives, matchesdrive } from './drivefilter' + +const DRIVES = [ + { device_id: 'USB-3', device_desc: 'Kingston 16GB', locker_location: 'DT' }, + { device_id: 'USB-4', device_desc: 'SanDisk 32GB', locker_location: 'Engineer' }, + { device_id: 'USB-10', device_desc: 'SanDisk 32GB', locker_location: 'Engineer' }, + { device_id: 'USB-11', device_desc: 'Kingston 64GB', locker_location: 'B11' }, +] + +describe('filterdrives', () => { + it('an empty box is not a filter', () => { + expect(filterdrives(DRIVES, '')).toHaveLength(4) + expect(filterdrives(DRIVES, ' ')).toHaveLength(4) + }) + + it('finds a drive by its id', () => { + expect(filterdrives(DRIVES, 'usb-4').map(d => d.device_id)).toEqual(['USB-4']) + }) + + it('finds drives by what is written on them', () => { + expect(filterdrives(DRIVES, 'kingston').map(d => d.device_id)) + .toEqual(['USB-3', 'USB-11']) + }) + + it('finds a locker by name', () => { + expect(filterdrives(DRIVES, 'engineer').map(d => d.device_id)) + .toEqual(['USB-4', 'USB-10']) + }) + + it('finds a locker the way somebody says it out loud', () => { + expect(filterdrives(DRIVES, 'locker engineer').map(d => d.device_id)) + .toEqual(['USB-4', 'USB-10']) + }) + + it('takes the words in any order', () => { + expect(filterdrives(DRIVES, 'sandisk engineer').map(d => d.device_id)) + .toEqual(['USB-4', 'USB-10']) + expect(filterdrives(DRIVES, 'engineer sandisk').map(d => d.device_id)) + .toEqual(['USB-4', 'USB-10']) + }) + + it('ignores case and stray spacing', () => { + expect(filterdrives(DRIVES, ' ENGINEER SanDisk ').map(d => d.device_id)) + .toEqual(['USB-4', 'USB-10']) + }) + + it('matches nothing rather than everything when nothing matches', () => { + expect(filterdrives(DRIVES, 'zzz')).toEqual([]) + }) + + it('does not fall over on a drive missing its description or locker', () => { + const sparse = [{ device_id: 'USB-9' }] + expect(matchesdrive(sparse[0], 'usb-9')).toBe(true) + expect(matchesdrive(sparse[0], 'engineer')).toBe(false) + }) +}) diff --git a/plugins/usb/frontend/views/USBKiosk.vue b/plugins/usb/frontend/views/USBKiosk.vue index 0b371784..626e93b3 100644 --- a/plugins/usb/frontend/views/USBKiosk.vue +++ b/plugins/usb/frontend/views/USBKiosk.vue @@ -58,12 +58,27 @@
-

Available

+
+

Available

+ +
+ + +
+

No drives are free right now.

+

+ No free drive matches "{{ drivefilter }}". +

-