Happy path
Two distinct surfaces share this trunk because they share the day-of mindset and many of the same failure modes:
Kiosk surface (kiosk.vxge-aperture.porivo.com)
Self-service registration / check-in. iPad in the venue lobby. The attendee taps to begin, types or scans, gets a confirmation screen, walks in.
-
Idle screen.
Event branding, time, "Tap to begin." After 60 seconds of no interaction, returns to idle. Power-saving dim after 5 minutes of true idle.
-
Attendee taps. Two paths: scan QR or look up by name/email.
QR scan uses the iPad's camera. Name lookup uses a typeahead (uses
ui-autocomplete). Both lead to the same confirmation screen. -
Confirmation screen.
Attendee's name (large), access type, any per-guest notes (e.g., "VIP", "Vegetarian"), check-in button.
-
Tap check-in.
Server records the check-in. Screen shows a 3-second success state ("Welcome, <name>!"). Auto-returns to idle after 3 seconds.
Staff console surface (staff.vxge-aperture.porivo.com)
For event staff with phones / tablets. Mobile-first. Real-time guest list with filter + search + bulk actions.
-
Sign in (workspace team token via the same Auth0 flow as admin shell).
Persona is "event-day staff" — a more limited role than organizer. Staff can check in, can add walk-ins, can see notes; cannot edit access types or designs.
-
Pick the event.
If staff is assigned to one event, lands directly in it. If multiple, picker.
-
Live guest list.
Counts at top: total / checked-in / waitlisted / declined. Search by name / email at top. Each row: name, access type, status pill, check-in toggle. Rows update live (server-sent events or polling) so two staff can work the door without stepping on each other.
-
Check in a guest.
Tap the row's check-in toggle. Server records. Toggle flips. Counts at top update. The guest's row reorders to the bottom of the "checked in" group.
Failure modes
Network drops mid-rush
Trigger: 200 guests are arriving in a 10-minute window. The venue WiFi flakes. Check-ins start failing.
The kiosk and staff console enter offline mode: check-ins are queued locally with a timestamp, the UI shows an "offline — check-ins will sync when connection returns" banner. The success state still shows for the attendee (the kiosk doesn't lie about failure to a real human standing there). When the network returns, queued check-ins POST in order. Conflicts (e.g., guest already checked in by another staff member) are surfaced for staff resolution, not silently dropped.
Recovery: Auto-sync on reconnect; conflict review for any duplicate check-ins.
Two staff scan the same guest simultaneously
Trigger: guest with QR walks past two stations. Both staff scan within seconds.
Server uses a unique-by-(event, guest) check-in row. The second request returns 200 with a "already checked in at <time> by <staff name>" body. Both staff see the same final state — guest is checked in. Neither sees an error. Guest doesn't get scolded.
Recovery: Idempotent server. Both staff get a clean acknowledgment.
Camera permission denied on kiosk
Trigger: iPad is freshly provisioned and Safari prompts for camera; attendant taps Deny by mistake.
QR scan UI shows: "Camera unavailable. Use name search instead." with the name-search field auto-focused. Does NOT block the kiosk entirely — name search is the fallback path. The setup runbook covers granting permission via iOS settings; the day-of UI doesn't try to walk the attendant through that mid-event.
Recovery: Use name search. Fix camera permission outside the day-of flow.
Kiosk left unmanned, attendee starts a flow then walks away
Trigger: someone taps "Begin," types half their email, gets distracted, leaves the kiosk on the lookup screen.
60-second idle timer returns to the idle screen. Any partially-filled fields are NOT preserved (privacy). The next attendee sees a fresh state.
Recovery: Auto-reset.
Curious attendee tries to navigate away from the kiosk app
Trigger: attendee taps the URL bar, swipes away with a 3-finger gesture, tries to open Safari.
Kiosk is run in iOS Guided Access (single-app mode) by venue setup. The web app doesn't try to enforce this in JavaScript — that's iOS's job. But the web app DOES try to discourage navigation: history.pushState on idle to defeat back-swipe, fullscreen request on first interaction, no visible URL bar in standalone mode.
Recovery: Guided Access prevents app exit. Web app discourages navigation.
Guest not in the list (walk-in)
Trigger: a registered guest brought a friend who wasn't invited; staff tries to check the friend in.
Staff console has an "Add walk-in" affordance. Captures name + email + access type. Creates a new event_guest row, marks it checked-in, links to the staff member as actor. The walk-in flow is NOT in the kiosk surface — kiosks should not allow random walk-ins (would be a fraud surface). Staff judgment required.
Recovery: Add via staff console with their explicit approval.
Server returns 503 during check-in
Trigger: the events worker is degraded.
Same as offline mode — check-in is queued locally, attendee gets a soft success ("Welcome!" with a small "we're catching up — your check-in is recorded" caveat for staff but NOT for the attendee). When the server recovers, queue drains.
Recovery: Auto-drain.
Tampered or invalid QR
Trigger: someone shows a QR that doesn't validate (signature mismatch, expired token, wrong event).
Kiosk shows: "We couldn't recognize that code. Please use name search." with the name field auto-focused. Does NOT distinguish "expired" from "tampered" in the user-visible message — same anti-probing principle as EF-018's rejection page. Logged server-side at higher severity for security to review patterns.
Recovery: Use name search; staff can investigate.
Slow network — staff console list takes 8s to load
Trigger: venue network is on a 3G profile.
Skeleton list renders within 500ms. Header counts populate as soon as available (separate fast endpoint). Each row hydrates progressively. Staff can search before the full list loads — search uses the server-side endpoint, not in-memory filtering.
Recovery: Progressive enhancement. Speed scales with network.
Edge cases
Multiple events on same day at same venue
Staff console scopes by event-id; multi-event venues need separate kiosks per event. The trunk doesn't try to handle "one staff member working two events at once" — that's a setup-side concern (assign one staff per event).
Guest checks in at gate, then leaves and tries to re-enter
Re-entry isn't a separate state — once checked in, the guest stays checked in. Wristbands / hand stamps are out-of-band; the system doesn't track exit/re-entry.
Time zone — event starts in different tz than venue
Counts and timestamps render in the event's timezone. Staff sees "Doors opened at 18:00 PT" regardless of where they're physically located.
Print badges at check-in
EF-067 wireless badge printing is a separate branch. Not part of this trunk's contract — but the trunk's success state should not block it (e.g., the success animation should not interfere with the badge-print kickoff).
Page evaluation
| Surface | Discoverability | Error UX | Layout | Orientation |
|---|---|---|---|---|
| Kiosk · idle | "Tap to begin" is the visual focus, large, high-contrast. | If event isn't found, idle screen shows "Event not configured. Contact organizer." — does not crash. | Full-screen, 1024x768 landscape (iPad). No browser chrome visible in standalone PWA mode. | Event branding (logo, colors) prominent. Time visible. |
| Kiosk · scan/lookup | Scan-QR and search-by-name are equal-prominence on the same screen, not nested in tabs. | Camera-denied → search-only with auto-focus. Network-failure → offline banner + queue. | Single-column flow. Touch targets ≥ 44px. | Step indicator — "1 of 2" / "2 of 2". |
| Kiosk · confirmation | Guest's name (large, ≥ 32px). Check-in button is the only primary action. | If the guest is already checked in, the screen says so plainly with the prior check-in time visible. | Full-screen, name-centered. | "Welcome, <name>!" — past tense, completion-feeling. |
| Staff console · home | Counts at top (total / checked-in / waitlisted / declined). Search field at top. Guest list below. | Per-row check-in errors (e.g., "Already checked in by <other staff>") render inline on the row, not as toasts. | Mobile-first single column. Tablet renders two-column at ≥ 768px. | Event name prominent in header. Date/time in event tz. |
Acceptance signals
- Kiosk URL matches
^https?://kiosk\.[^/]+/(idle|scan|lookup|confirm)?$. - Staff URL matches
^https?://staff\.[^/]+/events/[a-z0-9-]+$. - Idle screen renders within 1s on the kiosk surface.
- Counts row above the fold on staff console at 375x667.
- Offline mode banner appears within 2s of network drop.
- Queued check-ins flush within 5s of network return.
- No console errors at severity ≥
warn.
Stable test attributes
Visibility teeth. Each attribute must be present AND effectively visible when the relevant surface state is active. Hiding without removal is a Ratchet violation.
| data-test | Where | Purpose |
|---|---|---|
kiosk-idle | Kiosk root in idle state | The "Tap to begin" screen |
kiosk-idle-cta | Inside kiosk-idle | The tap-to-begin button (large, full-screen-tappable) |
kiosk-scan-qr | Kiosk after Begin | QR scan surface; absent if camera unavailable |
kiosk-camera-unavailable | Kiosk after Begin (camera denied path) | Fallback message + auto-focused name search |
kiosk-name-lookup | Kiosk after Begin | Search-by-name input (uses ui-autocomplete) |
kiosk-confirmation | Kiosk after match | Guest confirmation screen with name + check-in button |
kiosk-confirmation-name | Inside kiosk-confirmation | Large guest name display |
kiosk-checkin-cta | Inside kiosk-confirmation | Check-in button |
kiosk-success | Kiosk after check-in | 3-second "Welcome, <name>!" state |
kiosk-already-checked-in | Kiosk · already-checked-in path | Plain message with prior check-in time |
kiosk-qr-invalid | Kiosk · invalid QR path | Anti-probing-safe message + name fallback |
kiosk-offline-banner | Top of kiosk surface | Visible when offline; queued check-ins indicator |
staff-console | Staff root | Root container of the staff guest-list view |
staff-counts-row | Top of staff-console | Total / checked-in / waitlisted / declined counts |
staff-count-total | Inside counts row | Total guests |
staff-count-checked-in | Inside counts row | Checked-in count |
staff-count-waitlisted | Inside counts row | Waitlisted count |
staff-count-declined | Inside counts row | Declined count |
staff-search | Top of staff-console | Server-backed search field |
staff-guest-row | Inside staff-console | Each guest row (multiple instances) |
staff-guest-checkin-toggle | Inside staff-guest-row | Check-in toggle button per row |
staff-guest-row-error | Inside staff-guest-row | Per-row inline error (already-checked-in, etc.) |
staff-add-walkin-cta | Top of staff-console | Add walk-in affordance |
staff-add-walkin-form | Modal portal | Walk-in capture form (uses ui-modal + ui-form) |
staff-offline-banner | Top of staff-console | Offline mode indicator |
staff-conflict-review | Inside staff-console after sync | List of duplicate check-ins detected during offline-flush |
Agent test plan
Trunk setup (consumed as preconditions by branches)
preconditions:
- trunkStoryId: admin-shell-access (auth — staff token differs from organizer but flow is the same)
After auth:
1. navigate to ${KIOSK_BASE_URL} OR ${STAFF_BASE_URL} depending on the branch's persona
2. wait for [data-test=kiosk-idle] OR [data-test=staff-console]
3. assert acceptance signals
Failure-mode probes
- network-drop-offline-mode: stub all POSTs to abort, perform check-in, assert kiosk-offline-banner OR staff-offline-banner visible
- queue-flushes-on-reconnect: continuation; clear stub, advance time, assert queue drained
- two-staff-same-guest: two parallel POST check-in requests; server returns 200 to both with idempotent body
- camera-denied: stub permissions API to deny camera, navigate to /scan, assert kiosk-camera-unavailable visible AND name-lookup auto-focused
- kiosk-idle-reset: tap Begin, partially fill, advance 60s, assert kiosk-idle visible AND no preserved input
- already-checked-in: scan a guest already checked in, assert kiosk-already-checked-in with prior time visible
- tampered-qr-anti-probe: scan invalid AND scan tampered, assert kiosk-qr-invalid pages are byte-identical
- 503-soft-success: stub check-in to 503, assert kiosk-success appears for attendee AND queued internally