Skip to content

Optimizations

PreviousNext

What stops the numbers from being trusted today, and the changes that fix it — ranked by what they unlock.

Tracing the automations turned up two kinds of problems: defects that make a number wrong or lose money, and blind spots where the data needed for a decision is never recorded. Both are listed here in the order we recommend fixing them.

Defects

1. Routines only run when someone opens the desk

runDailyRoutines is called from one place: the /dashboard/desk page render. A cashier's home page is /dashboard/cafe, so on a day nobody opens the desk, no rent invoices are issued, forgotten visits stay open, and finished bookings are not completed.

EffectRent invoiced late, occupancy and booking numbers wrong until someone visits
FixAdd a Cloudflare cron trigger (triggers.crons in wrangler.jsonc) that runs the routines at 06:00 Port Sudan time. OpenNext's worker only handles fetch, so this needs a thin custom entry that adds a scheduled handler. Keep the page call as a fallback — the routine already claims the day, so it never runs twice
EffortSmall to medium

2. Expired ticket holds stay pending forever

When a ticket hold lapses, the seats are released on read but the order stays PENDING and still shows in the desk's pending count. Nothing ever cancels it.

EffectThe desk's pending count only grows; paid conversion can't be read from the status
FixIn the daily routine: PENDING, no Bankak reference, holdUntil < now → CANCELLED with cancelledAt and the reason "hold expired" — the same rule bookings already follow
EffortSmall

3. The cash-up leaves out café tab settlements

Settling a tab keeps the payments' method as TAB. The desk cash-up (cashUp in src/lib/desk/reports.ts) only counts café payments made in CASH or BANKAK. The café's own sales report does add the settlements, so the two reports disagree.

EffectThe end-of-day cash-up is short by every tab settled that day
FixAdd CafeTabSettlement totals by method to the cash-up
EffortSmall

4. Public and admin count event seats differently

The public form counts REGISTERED rows against capacity. The admin counts REGISTERED and ATTENDED. Marking someone as attended frees a public seat.

EffectEvents can overbook after check-in starts
FixCount both statuses in registerForEvent and the public event queries
EffortSmall

5. "Full" shows up to five minutes late

Event pages are cached for 300 seconds, and neither a new registration nor an admin status change refreshes them.

EffectVisitors see seats that are gone and land on the waitlist
FixrevalidatePath the event page after a registration and after a status change
EffortSmall

6. Nobody is promoted from the waitlist

When a registration is cancelled, the seat stays empty. The waitlist is only read by hand.

EffectEmpty seats at events with a waitlist
FixOn cancel, promote the oldest WAITLISTED row and show staff a WhatsApp link to tell them
EffortMedium

Blind spots

A. Where people come from

Nothing records how a visitor arrived: no utm tags, no referrer, no "how did you hear about us". Without it, no marketing decision can be measured.

  • Keep the first utm_source, utm_medium and utm_campaign (or the referrer's host) in a cookie in src/proxy.ts.
  • Store it as source on BookingRequest, TicketOrder, EventRegistration, ProgramApplication, Enquiry and DeskCustomer.
  • Add "How did you hear about us?" to the desk's new-customer form, for walk-ins.
  • Tag every link Space posts: ?utm_source=facebook&utm_medium=post&utm_campaign=<event-slug>.

Unlocks: revenue and conversion by channel — the core of the growth plan.

B. When each step happened

Only the café records the time of every change. Elsewhere, the time of a transition is lost as soon as the row changes again.

ModelMissingUnlocks
BookingRequestconfirmedAt, cancelledAt, completedAtTime to confirm; holdUntil is also wiped on confirm
TicketOrdersubmittedAtTime from transfer to verification
EventRegistrationany change timeWhen people cancel
EnquiryfirstResponseAt, closedAtResponse time — the lead-to-customer speed
ProgramApplicationdecidedAtReview turnaround
DeskChargepaidAtDays to collect rent

The cleanest fix is one event table per domain, the way CafeOrderEvent already works: one row per change, with who and when.

C. WhatsApp taps

Every flow ends in a wa.me link, and none of the taps are counted. Send them through a small redirect route (for example /go/whatsapp?from=booking) that records the tap, then forwards to WhatsApp.

Unlocks: how many people choose WhatsApp over the web flow, and which page sends them.

D. Visits to the site

There is no page-view data, so the top of every funnel is missing. Cloudflare Web Analytics is free, cookieless, and needs only a script tag on a site already served by Cloudflare.

Unlocks: views → requests conversion per page.

E. Smaller gaps

  • Routines history: routinesLog is overwritten every day. Keep one row per run.
  • Door rejects: scans of unpaid, unknown or already-used tickets are not recorded.
  • Sold-out time: marking a café item sold out keeps no history, so lost sales can't be estimated.
  • Bots: honeypot and rate-limit hits are not counted, so form abuse is invisible.

Order of work

OrderChangeWhy first
1Defects 1–3They make today's money and occupancy numbers wrong
2Blind spot A (source)Every growth decision needs it; data only builds up from the day it ships
3Defects 4–5Protect event experience as events grow
4Blind spots B and CSpeed metrics and the WhatsApp funnel
5Blind spot D, defect 6, smaller gapsComplete the picture