Catering event panel — staffing pools, escalation, pack-list caveats, session re-auth

This article explains the Catering module's event panel: where events and staffing come from, the two contractor pools and why only one of them is in 7shifts, what Escalate to backup/interested actually does, why multi-day events need their hours checked before a pack list is calculated, and the re-authentication step behind the two "session expired" notes. It is the reference for the Guide link on the panel.

Where the events come from

SourceWhat it contributes
FlashquotesBooked events, add-ons, guest count, dress code and venue. Service line items come separately from the quote (see re-auth below).
BloomBookings billed in Bloom (GBTA Convention and similar). These carry no guest count from their source.
Intercom retainerWeekly visits read from Google Calendar (scripts/gws-intercom-events.sh, any calendar event whose title matches "intercom"; address defaults to 1330 W Fulton St #750, Chicago). Its menu comes from the Bloom recurring invoice, not a Flashquotes quote.
7shiftsCatering-department shifts (department 774235), which supply the Active-roster staffing count.

The backend fetches a 63-day window and caches it for 30 minutes; the 7 / 14 / 30 / All chips only filter that. Past week adds the last week's events so a Backup/Interested staffing entry can still be added or corrected after an event has happened (Maciej, 2026-08-02); it is a one-off mode and is not remembered between sessions.

The retainer pill

An event carrying the retainer pill is a recurring Intercom visit sourced from the calendar — billed on a retainer in Bloom, not a one-off booking. Handling differs in three ways: the crew window is fixed at 7:00–11:30, it needs 2 staff every visit, and its 7shifts shifts are matched by shift notes containing "fulton" or "intercom" rather than by an event id. The retainer's readiness checklist is scoped to the rules that recur per visit.

Staffing count

Standing rule (Maciej, 2026-07-18): catering events are always at least 2 staff — only cold brew bike gigs run solo. The panel uses the event's own staff number when Flashquotes has one, otherwise max(2, number of 7shifts shifts already created). A brand-new event with no shifts therefore reads as 0/2, not "fully staffed".

needed − (Active staff on 7shifts shifts + manual adds) = unclaimed. The event is staffed when that reaches zero. Manual adds are folded into this same arithmetic so the readiness list, the attention queue and the digest's short-staffed filter all agree.

The two pools — Active vs Backup/Interested

Active rosterBackup / Interested
Lives in7shifts (and Notion)Notion Contractor Baristas DB only (database 703b98fcd22b429ab093a1c07fde5b44, Status = Backup or Interested)
WhyGraduated after enough events; has a 7shifts accountDeliberately stays Notion-only until used enough times to graduate to Active (Maciej, 2026-07-29)
How they show up on an eventClaiming or being assigned a 7shifts shiftOnly through Add confirmed helper on the panel — nothing else can see them
Payroll7shifts punchesNo punches; tagged backup · no 7shifts on the pay row (see Contractor pay rules — owed queue, café↔café, van premium, no-shows, Mercury setup)

Add confirmed helper is the manual add. The picker lists the Notion Backup/Interested roster (cached with the catering data; a free-typed name is allowed for someone not in it). On a multi-day event you choose which day's shift the helper covers; the add fills the first still-open 7shifts slot for that day rather than appearing as an extra slot. Assigning someone also creates their Mercury recipient automatically against the email on their Notion row (2026-08-06) — a blank email is the one reason that setup cannot complete, which is why the panel flags it there. Adding or removing a helper also refreshes the payroll owed queue immediately.

Escalate to backup/interested

Neither pool is texted through 7shifts. Escalate stages a "DM me and I'll send details" ask to the Backup/Interested pool — it is not a shift claim and nothing in 7shifts changes. Mechanics:

  • The picker lists every short-staffed event in the next 30 days with nothing pre-checked. This pool is outside the normal roster, so each event is opted in explicitly (Maciej, 2026-07-29, reversing an earlier all-checked default).
  • Confirming runs the event-text skill in ESCALATION MODE headlessly, with the pre-computed event list (real per-day shift windows included) rather than letting it re-scan Flashquotes. It stages a card in the Inbox; it never sends.
  • Composition can take over a minute (Notion roster query plus compose). The button is replaced by a "staging" state for up to 3 minutes, and the backend refuses a repeat request for the same audience and any of the same events within 3 minutes, persisted to data/pending-barista-text-requests.json so the guard survives a backend restart.
  • Send history is kept per event and per audience (Active team vs Backup/Interested: count and last sent), so a digest or escalation that already went out is visible before tapping Stage again.

Multi-day events — why Hours/day says verify

A multi-day event has one card per calendar day (Maciej, 2026-07-31). Days labelled Setup are prep, not service, and do not count toward "Day N of M". Each day's real window comes from the manual per-day breakdown confirmed in the Bloom → 7shifts sync; the shift time per service day is derived from it deterministically in the backend.

The whole-event record, however, only carries one start and one end for the entire run. For GBTA Convention that was Aug 3 noon to Aug 5 4:45 pm — a raw span of about 53 hours, not "how long each day runs". So on the whole-event card the pack-list Hours/day field defaults to 4 and is marked verify; on an individual day card it defaults to that day's parsed hours. Days defaults to 1 on a day card and to the span in days on the whole-event card; raise it on a day card for a whole-event total. Both fields are editable and the value you type sticks across recalculation.

Pack list caveats

  • The calculator is scripts/catering-calculator.py --mode supplies (physical quantities, not pricing), wrapped by pepper-ui/backend/catering-supplies.js. Guests, hours per day and services are required; days, event type (taken from the Flashquotes quote), month, start hour, outdoors and syrup are passed when known.
  • Bloom bookings and the Intercom retainer carry no guest count, so Guests must be typed for them.
  • Services drive the recipe. When the Flashquotes session has expired the panel has add-ons but no service lines, and the pack list cannot run until the token is refreshed (below).
  • The Supply OS on-hand check is honest about coverage: as of 2026-07-31 the ledger held five registered items, all retail coffee-bag SKUs, so most lines read not tracked in Supply OS. A stage purchase button appears only when the line is tracked, short, and has a vendor offer; the backend re-checks that before staging anything.

Session re-auth

Two sessions expire independently; each has its own script and its own place where the result lands.

"Service lines unavailable — Flashquotes session expired"

Flashquotes' REST payload carries add-ons but not service line items; those live on the quote and are reachable only through the tRPC API, authenticated by the __Secure-next-auth.session-token browser cookie stored as FQ_SESSION_TOKEN. The cookie expires periodically. The same token drives fq-auto-quote.py and the n8n auto-quote workflow, so an expired token also stalls quoting, not just this panel.

  1. Run python3 scripts/fq-auth.py. A browser opens at app.flashquotes.com; log in yourself (Pepper never handles the credentials) and press Enter.
  2. The script reads the cookie from the live session, persists FQ_SESSION_TOKEN through the shared secret writer (Proton Pass first, .env as fallback — do not hand-edit .env, a Pass copy would shadow it), and saves .flashquotes-session.json (chmod 600) as a fallback. It never prints the token.
  3. Add --deploy to push the new token to the n8n workflow as well.

Two Mini jobs cover the routine case: com.pepper.fq-health (daily 9:15 AM CDT) validates the token and redeploys if expired, and com.pepper.fq-token-refresh (Mondays 8:17 AM CDT) proactively redeploys the token on file. A manual run is needed when the cookie itself has died.

"Retainer menu unavailable — Bloom session expired"

The retainer's menu is the newest non-draft invoice on the Bloom "Intercom" project, fetched with the bloom_token cookie from .bloom-session.json. When the session has expired the panel falls back to the last snapshot at data/intercom-offering.json and shows this note.

  1. Run python3 scripts/bloom-auth.py. A headed Chrome window opens at bloom.io/login; log in, wait for the dashboard, press Enter.
  2. The session is saved to .bloom-session.json next to the repo; the onboarding monitor and the dashboard reuse it.

The onboarding monitor renews this token on its own: the bloom_token is a 90-day JWT, and under 21 days remaining it mints a fresh one via scripts/bloom-login.py. Only if that fails does it Telegram the manual route above. Judge the session by the JWT's own expiry, never by the cookie's short expires.

Both scripts open a real browser window, so they need a GUI session. Where to run them differs. fq-auth.py can run on the MacBook: it writes FQ_SESSION_TOKEN to the shared Proton Pass vault, which is where the Mini reads from as well. But the Mini's pepper-ui backend loads its secrets into the process environment at launch (scripts/env-wrapper.sh, per com.blfo.pepper-ui.plist) and env() in catering-data.js reads process.env before ~/pepper/.env, so the running backend keeps the token it started with. Restart it afterwards: ssh mini 'launchctl kickstart -k gui/$(id -u)/com.blfo.pepper-ui'. The restart requirement is inferred from that read order, not tested live. bloom-auth.py is different: it writes a file, .bloom-session.json, that the backend re-reads on every call from the Mini's ~/pepper, so a session saved on the MacBook does not reach it — run it in the Mini's own session over VNC (open vnc://sztuczna@Maciejs-Mac-mini.local).


Was this article helpful?