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 @@
No drives are free right now.
++ No free drive matches "{{ drivefilter }}". +