← All stories

TRUNK · day-of-operations

Day-of Operations

Persona: Event-day staff + Kiosk attendant Stage: Day-of Surfaces: kiosk.vxge-aperture.porivo.com + staff.vxge-aperture.porivo.com Branches estimated to root here: ~15

The mindset shift: less editing, more reacting. Guests are at the door. Staff have phones in their hands and 30 seconds to scan a QR, find a name, mark a check-in. The kiosk is unmanned for 5 minutes at a stretch and might get poked by a curious attendee. The network is spotty in the venue. Day-of failure modes are different from setup failure modes — speed and resilience matter more than discoverability.

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.

  1. 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.

  2. 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.

  3. Confirmation screen.

    Attendee's name (large), access type, any per-guest notes (e.g., "VIP", "Vegetarian"), check-in button.

  4. 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.

  1. 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.

  2. Pick the event.

    If staff is assigned to one event, lands directly in it. If multiple, picker.

  3. 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.

  4. 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

SurfaceDiscoverabilityError UXLayoutOrientation
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-testWherePurpose
kiosk-idleKiosk root in idle stateThe "Tap to begin" screen
kiosk-idle-ctaInside kiosk-idleThe tap-to-begin button (large, full-screen-tappable)
kiosk-scan-qrKiosk after BeginQR scan surface; absent if camera unavailable
kiosk-camera-unavailableKiosk after Begin (camera denied path)Fallback message + auto-focused name search
kiosk-name-lookupKiosk after BeginSearch-by-name input (uses ui-autocomplete)
kiosk-confirmationKiosk after matchGuest confirmation screen with name + check-in button
kiosk-confirmation-nameInside kiosk-confirmationLarge guest name display
kiosk-checkin-ctaInside kiosk-confirmationCheck-in button
kiosk-successKiosk after check-in3-second "Welcome, <name>!" state
kiosk-already-checked-inKiosk · already-checked-in pathPlain message with prior check-in time
kiosk-qr-invalidKiosk · invalid QR pathAnti-probing-safe message + name fallback
kiosk-offline-bannerTop of kiosk surfaceVisible when offline; queued check-ins indicator
staff-consoleStaff rootRoot container of the staff guest-list view
staff-counts-rowTop of staff-consoleTotal / checked-in / waitlisted / declined counts
staff-count-totalInside counts rowTotal guests
staff-count-checked-inInside counts rowChecked-in count
staff-count-waitlistedInside counts rowWaitlisted count
staff-count-declinedInside counts rowDeclined count
staff-searchTop of staff-consoleServer-backed search field
staff-guest-rowInside staff-consoleEach guest row (multiple instances)
staff-guest-checkin-toggleInside staff-guest-rowCheck-in toggle button per row
staff-guest-row-errorInside staff-guest-rowPer-row inline error (already-checked-in, etc.)
staff-add-walkin-ctaTop of staff-consoleAdd walk-in affordance
staff-add-walkin-formModal portalWalk-in capture form (uses ui-modal + ui-form)
staff-offline-bannerTop of staff-consoleOffline mode indicator
staff-conflict-reviewInside staff-console after syncList 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