Component: kiosk
Who is this for
Section titled “Who is this for”A developer who changes the tablet page, and an operator who wants to know why the station went back to the start screen on its own.
- The kiosk is a static page:
index.htmlplus three plain scripts, no framework, no build step, no external libraries.sticker-apiserves it at/kiosk/. - It holds no secrets and no authentication. Everything it knows about a session comes from polling
GET /session/{token}once per second. - A session is created only when a guest taps Start, never on page load, so dead sessions do not pile up.
- The kiosk never waits for generation. After the photo is accepted it shows “done” for 5 seconds and returns to the start screen for the next guest.
- Every way of leaving a session early calls
POST /session/{token}/expirewith a reason code, so the bot can tell the guest that the session is closed instead of silently failing. - The camera is on only between consent and the end of capture.
Component diagram
Section titled “Component diagram”flowchart LR
subgraph page["Kiosk page in the tablet browser"]
theme["kiosk-theme.js - sets theme before first paint"]
html["index.html - screens and styles"]
preview["preview.js - optional frame for another screen size"]
idle["idle.js - idle timer for the outfit screen"]
kiosk["kiosk.js - session, polling, camera, upload"]
brand["brand - CSS, fonts, images"]
end
api["sticker-api"]
cam["Tablet camera via getUserMedia"]
html --> theme
html --> brand
html --> preview
html --> idle
html --> kiosk
kiosk --> idle
kiosk -- "fetch, same origin" --> api
kiosk --> cam
theme -- "served by" --> api
| File | Responsibility | Source |
|---|---|---|
index.html |
All screens as <section id="screen-…"> elements and all styles. Loads the theme script in <head>, then preview.js, idle.js, kiosk.js |
bot/kiosk/index.html |
kiosk.js |
Everything dynamic: texts, session lifecycle, QR timer, polling, outfit cards, camera, burst capture, upload with retries, error and notice screens, staff reset, wake lock, gesture locking | bot/kiosk/kiosk.js |
idle.js |
A small state machine (off → waiting → prompt) for the outfit screen. Kept free of DOM access so the same file is tested in Node with fake timers |
bot/kiosk/idle.js, tests/js/kiosk_idle.test.mjs |
preview.js |
With ?preview=<W>x<H> the page becomes a frame that shows the kiosk in an iframe of exactly that size. The station itself does not start in the outer frame |
bot/kiosk/preview.js |
brand/ |
Brand stylesheet, self-hosted font files, decorative images | bot/kiosk/brand/brand.css |
static/ |
Optional footer logos; the API lists only the files that exist | bot/kiosk/static/.gitkeep |
/kiosk-theme.js |
Not a file: generated by the API from KIOSK_THEME |
bot/api.py |
Screens
Section titled “Screens”flowchart TB
home["home - Start button"]
qr["home with QR - 90 s ring"]
linked["linked - continue in Telegram"]
templates["templates - outfit cards"]
camera["camera - countdown and burst"]
uploading["uploading"]
done["done - 5 s"]
notice["notice - pack already exists or is in progress"]
error["error - 4 s"]
home -- "tap Start, POST /session" --> qr
qr -- "status linked" --> linked
linked -- "status consented" --> templates
templates -- "tap a card, POST template" --> camera
camera --> uploading
uploading -- "photo accepted" --> done
done --> home
qr -- "90 s or Cancel" --> home
linked -- "3 min without consent" --> home
templates -- "idle prompt ran out" --> home
camera -- "camera failed" --> error
uploading -- "upload failed twice" --> error
error --> home
templates -- "pack limit" --> notice
uploading -- "pack limit" --> notice
qr -- "expired with notice" --> notice
notice --> home
The screen shown is driven by the session status returned by the API:
| Session status | Screen | What the kiosk does |
|---|---|---|
idle |
QR | Waits; the ring around the QR shrinks over 90 s |
linked |
“Continue in Telegram” | Stops the QR timer; waits for consent |
consented |
Outfit cards | Renders cards from GET /templates, opens the camera in the background to warm it up, starts the idle timer |
template_chosen |
Camera | Starts the capture flow |
expired with a notice |
Notice | Shows “you already have a pack” or “your pack is being prepared” for 7 s |
photo_taken, expired without notice, or 404 |
Start | Returns to the start screen |
Timers and limits
Section titled “Timers and limits”All values are constants in CFG at the top of kiosk.js, except the two idle values, which the server supplies.
| Timer | Value | What happens when it fires | Close reason sent |
|---|---|---|---|
| Status polling | every 1000 ms | GET /session/{token} |
— |
| Cooldown between session creations | 1000 ms | A second tap on Start is ignored | — |
| QR not scanned | 90 s from the moment the QR is visible | Session expired, back to start | qr_timeout |
| Cancel button on the QR screen | — | Session expired, back to start | cancel |
| Linked but no consent | 3 min | Session expired, back to start | kiosk_reset (the default code) |
| Outfit screen idle | KIOSK_IDLE_SEC (default 90 s) without a touch, then a “Are you here?” prompt counting down KIOSK_IDLE_PROMPT_SEC (default 15 s) |
Any touch dismisses the prompt and restarts the idle count. If the countdown ends: session expired, back to start | idle_timeout |
| “Look at the camera” | 2 s, then a 3-2-1 countdown | Capture | — |
| Burst | 3 frames, 150 ms apart | The sharpest frame is kept | — |
| Upload retries | 3 tries per capture, pauses 1 s, 2 s, 3 s | A 4xx answer (except 408 and 429) and a maintenance answer are not retried | — |
| Capture attempts per session | 2 | After the second failure: error screen, back to start | kiosk_reset |
| Camera cannot be opened | error screen for 4 s | Session expired, back to start | camera_error |
| Staff reset | hold the hidden zone for 3 s | Session expired, back to start | staff_reset |
| “Done” screen | 5 s | Back to start | — |
| Notice screen | 7 s | Back to start | — |
Closing a session this way does not use up the guest’s pack: no job exists yet, so the pack limit is untouched. A new tap on Start gives a new session.
Session and capture logic
Section titled “Session and capture logic”- Generation counter. A module-level counter is incremented on every new session and every return to the start screen. Each asynchronous step remembers the value it started with and stops if the value changed. This is what prevents the “tail” of an abandoned session (a late poll answer, a late camera opening) from touching the screen of the next guest.
- QR. The image is
GET /session/{token}/qr.svg. It is shown only after it has fully loaded; the 90-second window starts at that moment. - Camera lifetime. Permission is requested once when the page starts (with staff present), and the stream is
closed immediately. After consent the camera opens again while the guest chooses an outfit, and it is released as
soon as the photo is accepted or the session ends.
?camera=alwayskeeps it open permanently — a fallback for a tablet where reopening is slow. - Sharpness. Each burst frame is scored by the variance of a Laplacian over a 320 px wide greyscale copy; the
highest score wins. The frame is encoded as JPEG at quality 0.92 and uploaded as multipart field
file. - Pack limit. A 409 answer with code
pack_limit(on template choice or photo upload) is not an error: the kiosk shows the friendly notice screen, and the API sends the guest a Telegram message with the existing link. - Maintenance. A 503 answer with code
maintenancereturns the station to the start screen with the lineСейчас идёт техобслуживание, загляни чуть позже(“Maintenance in progress, come back a bit later”). - Network loss. A failed poll only shows a small “no connection” indicator; polling continues.
- Kiosk hardening. Context menu, double-tap, pinch zoom, ctrl+wheel and drag are blocked; the mouse cursor hides after 3 s; a screen wake lock is requested and re-requested when the page becomes visible again.
Endpoints the kiosk calls
Section titled “Endpoints the kiosk calls”| Call | When |
|---|---|
GET /kiosk-theme.js |
Page load, before first paint |
GET /branding |
Page load — footer logos |
GET /templates, then each preview image |
Page load — cards are fetched and images preloaded before the first guest |
GET /kiosk-config |
Page load — idle timers |
POST /session |
Tap on Start |
GET /session/{token}/qr.svg |
Right after the session is created |
GET /session/{token} |
Every second while a session exists and no capture is running |
POST /session/{token}/template |
Tap on an outfit card |
POST /session/{token}/photo |
After capture |
POST /session/{token}/expire?reason=<code> |
Every return to the start screen while a session exists (best effort) |
All paths are relative, so the page works on whatever origin serves it. The camera API needs HTTPS (or localhost),
which is why the kiosk is always opened as https://stickers.eventtalks.uz/kiosk/.
URL parameters
Section titled “URL parameters”| Parameter | Effect |
|---|---|
?theme=light or ?theme=dark |
Overrides KIOSK_THEME for this page load. /kiosk/light is a fixed address for the light theme |
?lang=<code> |
Selects a text set from TEXTS in kiosk.js. Only ru exists at this commit; an unknown code falls back to ru |
?camera=always |
Keeps the camera open from page load |
?preview=<W>x<H> |
Shows the kiosk inside a frame of that size (for checking layouts on a desktop) |
?qr_timeout_sec=, ?idle_sec=, ?idle_prompt_sec= |
Shorten timers for automated station checks (tools/kiosk_check.py) |
Where to go next
Section titled “Where to go next”- Flow: guest session — the kiosk, the bot and the API in one sequence.
- Component: sticker-api — the server side of every call above.
- State machines — the session statuses and close reasons.
- Event playbook — setting up the tablet at a venue.
- Runbook: station stuck — what to do when the screen does not react.
Last verified: 2026-11-13 against ai-sticker-pack@b715418 and ai-stickers-landing@e350ff5.