The original ask was a modified Windows 11 ISO with a ticket/HKID unlock, locked settings, guest browsers and a floating control bar. Built instead as one 72 KB self-contained HTML file that needs no install, no admin rights and no reimaging — because a web kiosk can be deployed to any terminal in minutes while a custom Windows image needs the ADK, sysprep and a credential-provider rewrite. Ticket + HKID auth, nine working apps, guest-mode browsers, a hard wipe on logout, and the whole state machine in 671 lines of inline JS.
"can you modify the windows 11 iso … ticket … or hkid … browsers guest mode … floating box: pause, zoom, log out … every data clears … (a website would also be fine)"
The request named a real Windows 11 ISO but closed with "a website would also be fine". A web kiosk is the right call for a library terminal and the difference is the deployment cost:
| Aspect | Custom Windows 11 ISO | This web kiosk |
|---|---|---|
| Build tooling | Windows ADK + Windows Configuration Designer + sysprep/unattend.xml | One .html file, a text editor |
| Ticket/HKID unlock | A custom credential provider (C++ COM object) or a kiosk app wrapping the shell | A login card with two forms against an in-page database |
| Locked settings | Group Policy + Kiosk Mode (Assigned Access) | There is no settings surface — apps are pre-selected, browsers are sandboxed |
| Deploy to a terminal | Reimage / sysprep, hours, licence per seat | Open the file in any browser, seconds |
| Wipe on logout | Mandatory profile + roaming-profile reset | One logout() clears every window, iframe, editor and timer |
The trade is honesty: a web page cannot truly lock the OS. It simulates the kiosk's behaviour — which is what a library actually cares about — not the kernel-level enforcement. That limit is stated plainly below.
Both auth methods were implemented, not one, because the request asked for both as equal first-class options:
LOCK SCREEN
│
├── 🎫 Ticket Number ──► match against D.tickets[] ──► consume (one-time) ──► unlock
│
└── 🪪 HKID ───────────► match against D.hkids[] ──► greet by name ───────► unlock
│
└─ no match ─► "HKID not found"
Tickets are one-time use — a matched ticket is spliced out of the array before unlock, so a ticket photo-copied by a patron stops working. HKID lookup normalises whitespace and case before comparing, and greets the patron by name (Welcome, Chan Tai Man!) so a librarian at the desk can see at a glance who is at which terminal.
| App | What works | Locked |
|---|---|---|
| Microsoft Word | Real textarea editor, print via sidebar | Content wiped on logout |
| Chrome / Firefox / Edge | Guest mode, DuckDuckGo search, URL bar | chrome:// / edge:// / about:config blocked |
| Library Catalog | Search 8 books by title/author/ISBN | Read-only |
| Notepad | Editor + print | Wiped on logout |
| Calculator | Expression evaluator | — |
| File Explorer | App launcher grid | Settings entry is 🔒 Locked |
| MS Paint | Brush/eraser on a canvas | Canvas thrown away on close |
Top-right, exactly as asked: Pause Screen, Zoom 100% / 150% / 200%, Zoom Out, plus on-screen
keyboard, high-contrast, help, and a red Log Out. Zoom scales the whole desktop via document.body.style.zoom,
which reflows every window and the taskbar together — a per-window zoom would have left the taskbar and toolbar fixed while
windows grew past them.
The invariant is simple: leave no trace. S.logout() tears down every open window (removing the DOM
nodes, not just hiding them), resets zoom to 100%, clears the pause state, resets the session timer, empties the taskbar,
clears the on-screen keyboard's shift/caps state, and returns to the lock screen. Browser iframes, editor contents and
Paint canvases are destroyed with their windows — there is nothing left to read because the nodes no longer exist.
The URL bar and the iframe loader both refuse chrome://, edge://, about:config
and about:support before navigation, and a load listener re-checks the iframe's actual
location.href so a link inside a page can't sneak a restricted URL past the bar.
Each browser opens on a local srcdoc start page — a search box plus quick links — so the kiosk never
depends on a remote homepage. Search navigates the iframe directly (window.location.href), not via
postMessage, because the message bridge was one of the things that broke in build.
| Symptom | Cause | Fix |
|---|---|---|
| Nothing clickable — every button dead | let oskCaps=false and function oskCaps() declared in the same scope — a SyntaxError that killed the whole script before any handler bound | Renamed to K.capsOn (state) + K.caps() (function); same for oskShift |
| Admin panel won't open | addEventListener on elements that weren't parsed yet / in nested template strings | All element handlers moved to inline onclick / onkeydown |
Browser start page is a wall of <div> text | srcdoc had .replace(/</g,'<') — every tag escaped into literal text | Only escape " (needed for the attribute); leave </> raw |
| Search does nothing | postMessage bridge between iframe and parent never fired | Start page navigates the iframe directly |
Word showed function wordBody(){…} | A live function reference inserted into innerHTML instead of calling it | Invoke typeof body === 'function' ? body() : body at render time |
The first two were the real killers: a naming conflict between a let and a function
is valid-looking code that node --check rejects instantly, but the HTML file had been edited by string patches
that never ran the check. The final pass re-wrote the file clean and validated it with node --check before
opening — 0 syntax errors.
Hidden behind a ⚙️ Admin link in the login footer (and a 5-click corner trigger), password-gated. Six tabs —
Tickets, HKID, Catalog, Announcements, Usage Logs, Settings — let a librarian add/revoke tickets, edit the HKID database,
change the session timeout and print quota, and read a usage log of who logged in, how, and for how long. All state persists
to localStorage, so it survives a refresh.
X-Frame-Options: DENY and refuse to render inside an iframe, so only DuckDuckGo and Bing work; the
start page says so. (2) "Data wipe" is in-memory and localStorage — a determined user with
devtools can read the database, which a real kiosk would gate behind the OS. (3) A web page cannot lock the OS —
a patron can still alt-tab away from the browser. For real enforcement you'd pair this UI with Assigned Access /
kiosk mode, but the behaviour — ticket auth, guest browsers, wipe-on-logout — is fully implemented.
| Item | Evidence |
|---|---|
| Artifact | demos/library-kiosk.html — 72,707 B, 671 lines |
| Mirror | demos/library-kiosk-mirror/index.html — sha256 afef1888…fd169, byte-identical |
| JS validity | node --check passes, 0 errors |
| Auth | Ticket one-time consume + HKID name-greet — both implemented |
| Browsers | 3 guest browsers, restricted-URL blocklist, direct iframe navigation |
| Logout wipe | Windows, iframes, editors, canvas, timer, zoom all torn down |
| Real Windows ISO | In progress — not built; this is the web fallback |
If a real image is ever needed, the path is Assigned Access kiosk mode + this HTML as the shell, or a custom credential provider for the ticket/HKID unlock. The web kiosk already covers the behaviour; the OS layer is the only missing piece.