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.
| Effect | Rent invoiced late, occupancy and booking numbers wrong until someone visits |
| Fix | Add 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 |
| Effort | Small 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.
| Effect | The desk's pending count only grows; paid conversion can't be read from the status |
| Fix | In the daily routine: PENDING, no Bankak reference, holdUntil < now → CANCELLED with cancelledAt and the reason "hold expired" — the same rule bookings already follow |
| Effort | Small |
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.
| Effect | The end-of-day cash-up is short by every tab settled that day |
| Fix | Add CafeTabSettlement totals by method to the cash-up |
| Effort | Small |
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.
| Effect | Events can overbook after check-in starts |
| Fix | Count both statuses in registerForEvent and the public event queries |
| Effort | Small |
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.
| Effect | Visitors see seats that are gone and land on the waitlist |
| Fix | revalidatePath the event page after a registration and after a status change |
| Effort | Small |
6. Nobody is promoted from the waitlist
When a registration is cancelled, the seat stays empty. The waitlist is only read by hand.
| Effect | Empty seats at events with a waitlist |
| Fix | On cancel, promote the oldest WAITLISTED row and show staff a WhatsApp link to tell them |
| Effort | Medium |
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_mediumandutm_campaign(or the referrer's host) in a cookie insrc/proxy.ts. - Store it as
sourceonBookingRequest,TicketOrder,EventRegistration,ProgramApplication,EnquiryandDeskCustomer. - 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.
| Model | Missing | Unlocks |
|---|---|---|
BookingRequest | confirmedAt, cancelledAt, completedAt | Time to confirm; holdUntil is also wiped on confirm |
TicketOrder | submittedAt | Time from transfer to verification |
EventRegistration | any change time | When people cancel |
Enquiry | firstResponseAt, closedAt | Response time — the lead-to-customer speed |
ProgramApplication | decidedAt | Review turnaround |
DeskCharge | paidAt | Days 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:
routinesLogis 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
| Order | Change | Why first |
|---|---|---|
| 1 | Defects 1–3 | They make today's money and occupancy numbers wrong |
| 2 | Blind spot A (source) | Every growth decision needs it; data only builds up from the day it ships |
| 3 | Defects 4–5 | Protect event experience as events grow |
| 4 | Blind spots B and C | Speed metrics and the WhatsApp funnel |
| 5 | Blind spot D, defect 6, smaller gaps | Complete the picture |
On This Page
Defects1. Routines only run when someone opens the desk2. Expired ticket holds stay pending forever3. The cash-up leaves out café tab settlements4. Public and admin count event seats differently5. "Full" shows up to five minutes late6. Nobody is promoted from the waitlistBlind spotsA. Where people come fromB. When each step happenedC. WhatsApp tapsD. Visits to the siteE. Smaller gapsOrder of work