Skip to content

Component: kiosk

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.html plus three plain scripts, no framework, no build step, no external libraries. sticker-api serves 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}/expire with 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.
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
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

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.

  • 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=always keeps 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 maintenance returns 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.
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/.

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)

Last verified: 2026-11-13 against ai-sticker-pack@b715418 and ai-stickers-landing@e350ff5.