Coast Catering · Internal Operations

TPP Alert & Staffing-Sync System

Standard Operating Procedure & complete build brief — the logic, every platform & login, what Resend does, what has been built, and the remaining action items.
Owner: Sonny Gonzalez Version: 1.0 Updated: Aug 22, 2026 Classification: Internal — contains credentials
CURRENT STATE: System is LIVE as of Aug 27, 2026. All three cron jobs are active on the Mac Mini. Note: TPP migrated its web host (app3app); the sync now derives the host dynamically from the post-login URL, so the Staff Hiring Manager scrape keeps working. See §11.
Do not deploy this file publicly. It contains logins and points to secret keys. It lives only on the Kingston drive and in ~/Downloads. The Resend API key and Supabase service key are referenced by location (in coast.env), not printed in full.
Contents

01What this system is

Total Party Planner (TPP) is Coast's booking source of truth, but it has no API and no built-in alerting. This system quietly watches TPP, mirrors each event's staffing needs into the Coast staffing app, and emails the office the moment anything important changes — plus reminds staff two days before a shift.

It replaces the manual habit of logging into TPP repeatedly to check "did anything move?" Three things run automatically on a schedule:

02The core logic

A. How it reads TPP (there is no API)

TPP's "Classic Staff Hiring Manager" page renders the full staffing table server-side, per date: event_StaffHiringManager.aspx?PartyDte=<date>. Each position needed = one row (a Title). A plain fetch can't get through TPP's login + Cloudflare handshake, so the script drives a real headless browser (Playwright / Chromium): it logs in, walks each date in the window, and parses the DOM for every event's positions, venue, guest count, status and times.

B. How TPP titles map to Coast roles

Chef titles are deliberately kept distinct so the app can show the on-site lead separately. Rule enforced: every on-site event has exactly one Executive Chef; the rest of the kitchen are Event Chefs; drop-off events use a Drop-off Chef.

TPP titleCoast role
Event Server / ServerServer
Executive ChefExecutive Chef
Event Chef / bare ChefEvent Chef (on-site)
Drop Off ChefDrop-off Chef
BartenderBartender
Captain / On-Site Event ManagerCaptain
Tasting, Prep, Driving, Deliveryignored (not event-day serving staff)

C. What it writes — and what it never touches

WRITES
  • The needs (headcount by role) on each matching event
  • Venue, guest count, event/crew-call times
  • Snapshot + change/run logs (for the watcher)
NEVER TOUCHES
  • Never creates events (the calendar sync owns creation)
  • Never edits office-owned signups / assignments
  • Never writes back into TPP

D. How the change-watch decides to email

Each run diffs the live TPP 4-week window against the last stored snapshot (tpp_event_state). It is idempotent — state lives in the DB, so it never double-notifies. The very first run on an empty snapshot "primes" silently (no email). It detects seven change types:

New event Cancelled Guest count Date moved Time change Venue change Staffing needs

When there are changes, it sends one branded digest email to the office, logs each change to tpp_change_log, records the run in tpp_run_log (including quiet runs), then refreshes the snapshot.

E. Shift reminders

Requested by Adrianna (email 2026-08-21). Two days before an event, each staffer who has accepted an assignment (not merely been invited) gets a branded reminder with the details, crew-call time and a maps pin. A reminder_log makes it idempotent so re-runs never double-send.

03Platforms & services (full stack)

LayerServiceRole in the system
Source of truthTotal Party Planner (TPP)Where the office books events & defines staffing needs. No API — scraped via browser.
ScraperPlaywright + ChromiumReal headless browser that logs into TPP and reads the staffing pages.
Automation hostMac Mini (Tailscale)Runs the Python scripts on a cron schedule. Dir: ~/coast-tpp-sync/.
DatabaseSupabase (hosted Postgres)Holds events, profiles, assignments + the sync's snapshot/log tables. RLS on.
Email deliveryResendSends every office alert + staffer reminder. See §5.
HostingVercel (×3 projects)Staff/manager app, the demo brief, and the ops watcher dashboard.
Language / runtimePython 3 (venv)Two scripts: tpp_sync.py, event_reminders.py. Stdlib + Playwright only.

04Logins & credentials

Passwords below are real. Secret API/service keys are stored in ~/coast-tpp-sync/coast.env on the Mac Mini and shown here masked only.
SystemLoginSecret
TPPcoastcatering_rizzaCoast445!
Mac Mini (SSH)ssh aiforroi@100.88.190.93Tailscale key / local login (sonnys-mac-mini.local)
Supabase projectref najmdkbbxtslbhpuqbbp
URL https://najmdkbbxtslbhpuqbbp.supabase.co
anon + service keys in coast.env & app .env
Supabase (direct SQL)pooler postgres.najmdkbbxtslbhpuqbbp @ aws-0-us-west-2.pooler.supabase.com:5432Cst-…-Zx9 (in notes)
Resendaccount that owns the emberprousa.com sending domainRESEND_API_KEY = re_g4…(in coast.env)
VercelSonny's Vercel account
App logins (default password Coast2026)
Manager (admin)manager@coast.comCoast2026
Demo stafferastaffer@coast.com ("Alex Staffer")Coast2026
Change tracker / watchercoast-tpp-monitor dashboardCoastWatch2026
Any staff accountself-registered emaildefault Coast2026

05Resend — the email engine

Every automated email (office alerts + staffer reminders) goes out through Resend's API. Key facts about how it's wired today:

SettingValue
API endpointPOST https://api.resend.com/emails
AuthAuthorization: Bearer <RESEND_API_KEY> (re_g4…, in coast.env)
From addressCoast Catering <coaststaffing@emberprousa.com>
Sending domainemberprousa.com — already verified in an existing Resend account
Change-alert recipientsbarry@, chelseasilvia@, rizza@ @coastcatering.com
Reminder recipientseach individual staffer's email (per accepted assignment)
Test address--test-to sonny@aiforroi.co (verification runs never hit real inboxes)
Cloudflare quirk (important): Resend sits behind Cloudflare, which blocks urllib's default User-Agent (403 / error 1010). The scripts send a normal browser User-Agent header so the request goes through. Don't remove it.
Key dependency: the system currently borrows the emberprousa.com Resend domain and API key. It is not on a Coast-owned Resend account. Recommended fix in §12.

Email designs

06The three scheduled jobs (cron)

All three run on the Mac Mini. Each wrapper sources coast.env then runs the matching Python script in the venv.

WhenWrapperCommandPurpose
6:30 am dailyrun.shtpp_sync.py --days 120Full sync — all events & needs into the app
Hourly 9am–5pmrun-notify.shtpp_sync.py --days 28 --notifyChange-watch → emails office
8:00 am dailyrun-reminders.shevent_reminders.py --days 22-day-out shift reminders
# Live crontab (currently PAUSED — lines commented out):
30 6 * * *  ~/coast-tpp-sync/run.sh          >> ~/coast-tpp-sync/logs/sync.log 2>&1
0 9-17 * * * ~/coast-tpp-sync/run-notify.sh   >> ~/coast-tpp-sync/logs/notify.log 2>&1
0 8 * * *   ~/coast-tpp-sync/run-reminders.sh >> ~/coast-tpp-sync/logs/reminders.log 2>&1

Useful script flags

07Supabase data model

TableOwned byWhat it holds
eventsapp + syncEvent rows; sync updates the needs JSONB, venue, guests, times.
profilesappStaff accounts (name, role, active flag, admin flag).
assignmentsappInvitations + accepted signups (drives reminders).
tpp_event_statewatcherSnapshot of each event's last-seen state (the diff baseline).
tpp_change_logwatcherOne row per detected change (feeds the tracker dashboard).
tpp_run_logwatcherOne row per run — including quiet ones — for a rolling history.
reminder_logremindersWhich assignments were already reminded (idempotency).
RLS is on. Staff can only see the shifts they're booked on — never the full event calendar. The sync uses the service key (bypasses RLS) to write.

08The apps & dashboards

AppURLProject dir / VercelWho uses it
Staff + Manager appschedule.coastcatering.comcoast-staffing-v3 · Vercel project distOffice (manage) + staff (availability/signups)
Sales / demo briefbrief.coastcatering.comcoast-staffing-briefDemo / walkthrough
Ops watcher dashboardmonitor.coastcatering.comcoast-tpp-monitor"Coast TPP Watcher — Status": run history & changes (login CoastWatch2026)
Deploy quirk (staff app): after npx vercel deploy --prod --yes you MUST re-alias: npx vercel alias set <deployment-host> schedule.coastcatering.com — prod deploys auto-assign a random domain. The brief & monitor projects auto-alias correctly on their own.

09What has been built done

10SOP — routine operations

Change who gets the office alerts

  1. Edit RECIPIENTS (lines ~47–51) in both copies: repo coast-staffing-v3/scripts/tpp_sync.py and live ~/coast-tpp-sync/tpp_sync.py.
  2. No restart needed — cron reads the file fresh each run.
  3. Verify with a test: ./.venv/bin/python3 tpp_sync.py --days 28 --notify --test-to you@email.com.

Preview an email without touching real inboxes

cd ~/coast-tpp-sync && set -a && source ./coast.env && set +a
./.venv/bin/python3 tpp_sync.py --test-send --test-to sonny@aiforroi.co

Force a manual full sync

cd ~/coast-tpp-sync && ./run.sh          # --days 120, no emails

Re-prime the snapshot after changing role/needs logic (avoid an email storm)

./.venv/bin/python3 tpp_sync.py --days 120 --notify --no-email

Check the logs

tail -f ~/coast-tpp-sync/logs/notify.log      # change-watch
tail -f ~/coast-tpp-sync/logs/sync.log        # daily sync
tail -f ~/coast-tpp-sync/logs/reminders.log   # reminders

11Start / stop / restart

Pause (current state)

The three coast lines in crontab -l are prefixed with # PAUSED. A backup of the original crontab is at ~/coast-tpp-sync/crontab.backup-20260822-124833.txt.

Restart everything

ssh aiforroi@100.88.190.93 \
  "crontab -l | sed -E 's/^# PAUSED (.*coast-tpp-sync.*)/\1/' | crontab - && crontab -l | grep coast"

Or restore the exact backup: crontab ~/coast-tpp-sync/crontab.backup-20260822-124833.txt.

Before restarting after a pause of a few days, run one --notify --no-email pass first so any changes that happened while paused don't all fire as one big digest.

12Action items

Resend — stand up a Coast-owned account priority

  1. TODO Create a Resend account under a Coast email (e.g. an admin@coastcatering.com).
  2. TODO Add & verify the coastcatering.com sending domain in Resend (add the SPF, DKIM and DMARC DNS records Resend provides at the coastcatering.com registrar/DNS).
  3. TODO Create a new API key in that account; put it in ~/coast-tpp-sync/coast.env as RESEND_API_KEY.
  4. TODO Change FROM_EMAIL to a Coast address, e.g. Coast Catering <staffing@coastcatering.com>, in coast.env (or the two script defaults).
  5. TODO Send a --test-to preview and confirm it lands (check spam / DMARC alignment) before flipping the office back on.

Why it matters: today the system sends as coaststaffing@emberprousa.com on a borrowed (Ember Pro) Resend domain. That works, but it isn't Coast-branded in the From line and it ties Coast's deliverability to an unrelated account. Moving to a verified coastcatering.com domain makes alerts come from Coast itself and keeps the two businesses independent.

Everything else

13Troubleshooting

SymptomLikely cause / fix
No emails at allCron paused (§11), or RESEND_API_KEY missing/blank in coast.env, or Mac Mini asleep / Kingston unmounted.
Email 403 / Cloudflare 1010Browser User-Agent header was removed from the Resend request — restore it.
Sudden flood of alertsSnapshot was wiped or role logic changed. Re-prime with --notify --no-email.
Sync reads 0 eventsTPP login failed (Playwright) or TPP changed the page. Check sync.log; re-run --dry.
Staff app shows old data after deployForgot the alias step — re-run vercel alias set (§8).
A change didn't emailIt was outside the 4-week window, was the baseline prime, or ran with --no-email.