For Agents
Reverse-chronological session log. Newest entries at top, grouped by date (
## YYYY-MM-DD). Each bullet: one piece of work, short summary, wikilinks to docs touched. Updated byobsidian-documenteron every project doc write. Read byhistorianat bootstrap (top ~15 entries).
2026-05-17
- Cross-linked Polymarket top-trader pipeline from integrations — external Rust binary writes
pm_*tables in themgmtSupabase project; CRM consumes viacreateTableQueryoncedatabase.types.tsis regenerated. Full overview at polymarket-fetch.
2026-05-11
- Bug fix + data migration: evening checklist filing onto the wrong day. Symptom: on mobile, “today’s” evening-checklist inputs showed last night’s answers pre-filled. Root cause:
web/src/mobile/plan/MobilePlan.tsxcomputed the standup “day” astoLocalDateString(new Date())(raw local calendar date), mount-frozen viauseMemo(…, []), and passed that onestandupIdto every sheet including the evening checklist + evening reflection. The user did their May-10 evening checklist at ~00:20 CEST on May 11 (procrastinated past midnight, tapped the new evening-checklist reminder push) → app created the May-11standuprow and saved the 3EVENING_CHECKLISTdatapoints (+ theBOTH-phase Energy datapoint) onto it; then on the evening of May 11, opening “today’s” (= May 11’s) checklist showed those answers. No data lost (standuphasUNIQUE(person, date),standup_datapointhasUNIQUE(standup, config)— just attached to the wrong day). Fix (commit1bf861a,fix(standup): evening checklist/reflection day starts at 15:00 local): neweveningStandupDate(now = new Date()): stringinweb/src/lib/date-utils.ts—const EVENING_STANDUP_DAY_START_HOUR = 15;now.getHours() < 15→ yesterday’stoLocalDateString, else today’s (so an “evening-standup day D” spans 15:00 on D → 14:59 on D+1; usesgetHours()= local deliberately).MobilePlan.tsx: the evening checklist + evening reflection sheets now targetuseStandup(person, eveningStandupDate())’s row (eveningStandupId), with a second on-demanduseEnsureStandup(eveningDate)(guarded byeveningDate !== date); the “Evening” nudge button is gated on that row’smorning_completed_at/evening_completed_at; morning ritual / lunch checkin / day plan blocks / standup notes are unchanged — still calendar-date (standupId);date/eveningDateare no longer mount-frozen — they recompute ondocument.visibilitychange(long-lived PWA doesn’t go stale across midnight/15:00). Addedweb/src/lib/__tests__/date-utils.test.ts(4 cases). Desktop/standuppage (pages/standup/index.tsx) NOT changed — has a date picker, computes “today” viatoISODateString(new Date())(UTC), which by accident means a late-CEST-night session (00:00–02:00 CEST = still previous day in UTC) lands on the previous day’s row anyway (minor pre-existing nit: it uses UTCtodayStrnot local — could drift by a day near midnight UTC ≈ 1–2 am CEST; not fixed). Data migration (one-shot, applied to prod 2026-05-11 ~22:xx CEST):UPDATE standup_datapoint SET standup = '<May-10 standup id>' WHERE standup = '<May-11 standup id>'— moved the 4 datapoints (proud_of,not_proud_of,standup.energy,overspent_today) from András Léderer’s May-11 standup row to his May-10 one (May-10 row had 0 datapoints, noUNIQUE(standup, config)conflict); May-11 row now empty. Action item open: fix is committed + pushed but not deployed — needspnpm --filter web deploy(wrangler) for the mobile PWA to pick it up; until then a post-midnight evening-checklist session would still mis-file (tonight-before-midnight is fine — deployed code targets May 11’s now-empty row). Gotcha for future sessions: “today’s date” for daily-ritual features (standup morning/evening) needs care — (a) compute from local time, not UTC (toLocalDateString, nottoISODateString— the desktop standup page gets this subtly wrong but gets lucky); (b) don’t mount-freeze it (useMemo(…, [])) in a PWA — recompute onvisibilitychange; (c) an evening ritual specifically should treat post-midnight sessions as the previous day — hence the 15:00 cutoff (eveningStandupDate); a morning ritual does not want this. The mobileMobilePlan“mount-frozen date selector deliberately not fixed” item in Mount-frozen date selectors is now partially fixed for this screen (MobileHours.activestill freezes). See evening-checklist-day-boundary-fix-2026-05-11. - DRY / simplification refactor pass shipped — 9 commits on
master(d035235..ccf6f57, on top of8fbfda1), 148 files changed, ~9,359 lines deleted / ~4,332 added (≈5k net removed). The Tier 0+1+2 cut (Tier 3 god-component decomposition explicitly out of scope — the cut line). Gated per-phase ontsc -bclean +vitest(one pre-existingMobileHours.test.tsxfailure, no new) + green build. P0 — dead code: deletedweb/src/lib/database.types.ts(4141-line byte-identical orphan ofpackages/shared/src/database.types.ts), 4 never-wired-uppackages/uicomponents (animated-list/bento-grid/chart/text-animate), the OLD deadweb/src/components/crm/PageHeader.tsx(a NEWPageHeaderlands in P2a),useReorderDayPlanBlocks+test,useDeleteOnCallSchedule,userspace/src/lib/hooks/useClientUser.ts, andweb/supabase/functions/notify/(legacy notify EF, superseded byevent → fn_event_to_notifications → emit_notification). CAVEAT for the user: confirm no Supabase-dashboard Database Webhook still points atnotifybefore removing it server-side (only deleted from the repo here). P1a — hook factories: extendedweb/src/lib/hooks/createTableQuery.ts(addedcreateMaybeSingleQuery/createSingleQuery/createTableQueryWithArgs/createMaybeSingleQueryWithArgs) +createMutation.ts(multi-key/function-forminvalidateKeys,returningfor.select().single(), newcreateRpcMutation); converted ~50 hand-rolled CRUD/read hooks (budget/on-call/tasks/inventory/project-members/datapoints/khr/standup/automations/rss/timesheet/vendor); hooks with transforms/key-remaps/upserts/functions.invoke/side-effects deliberately left hand-rolled. P1b — dialogs: extendedCreateEntityDialoginto the standard form-dialog shell (submitLabel/pendingLabel/maxWidth/extraFooterLeft/showPendingSpinner/trigger; laterdescription: ReactNode+ additivecontentClassName); migrated 10 dialogs (AddTransaction/AddLoanee/AddConfiscation/AddPayback/CreateAccount/Envelope/Transfer/LinkBillingo/CreateInvoice);MappingRuleDialogNOT migrated —CreateEntityDialogwraps children in a<form>andMappingActionBuilder’s Add/remove buttons lacktype="button"→ would submit; mixed@tanstack/react-formvs localuseStateleft intentional (latter need component-body cross-field reactivity react-form’s render-prop model doesn’t give cheaply —EnvelopeDialog’s dynamic description,AddTransactionDialog’s conditional fields). P1c — small dedupes:downloadBlob→csv-export.ts(deleted byte-identicaldownloadTimesheetCsv),getMonthRange→date-utils.ts, userspacestatusColor/statusLabel→newuserspace/src/lib/project-status.ts, ~16 inline.toISOString().slice(0,10)→toISODateString(), dropped privatetoDateStr/toISODateStringcopies fortoLocalDateString,CSVImportWizard/MappingRuleDialogerror banners→MUTATION_ERROR. P2a — list-page primitives: newweb/src/components/crm/{PageHeader,ExportCsvButton,CreateButton,ErrorBanner,CardSkeleton(+SkeletonLines),ResourceTable}.tsx+web/src/lib/hooks/useUrlListState.ts(all tested); rolled out acrossprojects/partners/contracts/people(headers+buttons+error) andtasks/invoices/inventory(buttons+error; their headers are structural variants);tasks/index.tsxURL-param boilerplate→useUrlListState;dashboard StatCardSkeleton+automationsrunner skeleton→SkeletonLines(other bespoke skeletons left — would change loading visuals); side effect: 3 create buttons gaineddata-create-action(thecshortcut). P2b — project-detail tabs:useInvoices/useContracts/useInventory/useTasksnow take an optionalprojectIdarg (createTableQueryWithArgs) → pushes the project filter to Supabase (.eq('project'|'for_project', projectId)) instead of fetching whole tables + client-filtering; no-arg behaviour unchanged; new prefixed query keysprojectInvoices/projectContracts/projectInventory/projectTasks/projectFiles(so existing['invoices']etc. invalidations still prefix-match);InvoicesTab/ContractsTab/InventoryTab→ResourceTable,TasksTab/FilesTab→scoped hooks (their rendering isn’t a plain table);TimesheetsTableft over-fetching (footer row +useTimesheetsout of scope);FilesTablist→newuseProjectFiles,OverviewTab.toggleOnCall→useUpdateProject(both off directuseSupabase());InventoryTabswitched from name-match to FK-id-match filtering (more correct — a behavior change). P2c — shared hook logic +useSupabase()bypasses:on-call-rates.tsgotbuildRateMaps/entryHours/entryRate(the${person}_${project}rate maps + standby/activated/multipliers ternary were copy-pasted across 5 dashboard/on-call hooks) + new tests for those ANDresolveOnCallRate(was untested financial code);makeStorageAttachmentHooks(useInvoicePdf/useContractPdfwere the same module twice, 95/87→18/18 lines);foreign-resource.ts+ForeignResourceRowlifted tomatcher.ts(copy-pasted interface + create-FR-then-insert-mappings/delete plumbing shared by 3 integration-mapping hooks;useStatusMappings.createMappingleft inline — single-object vs array;useStorageProjectAssignmentscreate-branch later wired to the helper);TaskChips.tsx+getTaskCardModeldedupe TaskCard/KanbanCard chip rendering — fixed TaskCard’s emptyAvatarFallbackbug (now shows initials), TaskCard priority pill now uses@/lib/colors(matches Kanban dots — MEDIUM amber not copper, HIGH red-500 not thestatus-unpaidtoken — deliberate palette unification, flagged for the user);DayPlanner/KanbanBoard/TaskDetailSheet/CSVImportWizardmoved off directuseSupabase()— newuseCreateTaskFromBlock,useBudgetTransactionLookups,KanbanBoard/TaskDetailSheetreuseuseToggleTaskStatus(FilesTabstill usesuseSupabase()for one imperativecreateSignedUrldownload — doesn’t fit a query hook). P2d — colors typed: stringly-typedgetStatusBorderColor/getStatusTextColor(domain: string, status: string)→ typedgetProject/InvoiceStatusBorder/TextColor(status: ProjectStatus|InvoiceStatus)(exhaustiveness now compile-checked); dropped deadtextMaps.tenderPipeline/tenderStatus. simplify-review + cleanup: 3-lens review (reuse/quality/efficiency) — verdict clean, no behavior bugs, server-side project filter confirmed a real win; 9 small follow-ups (mixed label sizes, stray comments, missedErrorBanner/SkeletonLinesadoptions, no-opsetSearchParamsshort-circuit, unused prop, lost bold name restored,tableBuilder/callRpcextracted toweb/src/lib/hooks/supabase-builders.ts). Deferred (logged for future sessions): edge-function_shared/consolidation of the ~6 functions hand-rollingcreateClient/OPTIONS-preflight/auth-checks (touches deployed functions incl.service-healthwhich is in the app shell/sidebar, can’t be verified/deployed from a refactor pass, small win after excludingcf-access-auth/send-push— needs its own reviewed+deployed change); theuseDayPlanSuggestions.ts/useOnCallMonthly.tsUTC-drift bugs (need a deliberate UTC-vs-local decision, not a mechanical dedupe — desktop fix elsewhere wastoLocalDateString);packages/tender-pipeline(unshipped — nothing imports it,tender-syncEF is an empty stub, 2 tests fail on TED v3 / EKR API drift; candidate for own-repo or deletion — untouched per instruction not to delete the spec/package); Tier 3 god-component decomposition (CSVImportWizard1497 lines,BudgetOverview, the god pagesinvoices/settings/tasks/system-integrations/dashboard);billingo-sync-statuseshardcoded 1.27 HUF VAT multiplier applied to ALL currencies; non-atomic client-side multi-stepuseCreateInvoice/useCreateBudgetTransfer(should be RPCs —complete_block_with_draft_v1/dashboard_summary_v1are precedents);web/src/mobile/lib/formatTime.tsuses local TZ where day-plan blocks (stored1970-01-01THH:MM:00Z) need UTC; possibly-dead Linear-pushweb/supabase/functions/sync-outbound/(user to confirm — might be hit by an external scheduler). New shared abstractions a future session should reuse, not re-roll:web/src/components/crm/→PageHeader/ExportCsvButton/CreateButton/ErrorBanner/CardSkeleton+SkeletonLines/ResourceTable(+Column<T>)/TaskChips(severalTask*Chipexports)/getTaskCardModel+useTaskCardModel/CreateEntityDialog(now the standard form-dialog shell — see its props; wraps children in a<form>, nested buttons needtype="button");web/src/lib/hooks/→createTableQueryWithArgs/createMaybeSingleQuery*/createRpcMutation/makeStorageAttachmentHooks/useUrlListState/useCreateTaskFromBlock/useBudgetTransactionLookups/useProjectFiles/supabase-builders.ts(tableBuilder/callRpc);web/src/lib/→on-call-rates.ts(buildRateMaps/entryHours/entryRate),foreign-resource.ts(createForeignResourceWithMappings/deleteForeignResource),matcher.tsnow exportsForeignResourceRow/IntegrationMappingRow,csv-export.tsdownloadBlob,colors.tstyped status-color fns;userspace/src/lib/project-status.ts. See dry-refactor-2026-05-11. - Evening Checklist Reminder shipped. A timed nudge to finish the daily Evening Checklist — same family as the scheduled-reminders system but a separate feature (does NOT touch the
remindertable /process_due_reminders/ the day-plan-block trigger). Newprocess_evening_checklist_reminders()SECURITY DEFINERSQL fn called by a new pg_cron jobprocess-evening-checklist-reminderson'0 * * * *'(every hour, UTC); the fn self-gates toextract(hour from now() AT TIME ZONE 'Europe/Budapest') IN (21, 22)— DST-proof, 22/24 ticks are a one-statementRETURN. When it acts: X = count of activestandup_datapoint_configrows withphase='EVENING_CHECKLIST'(currently 4;standup_datapoint_configis global, nopersoncol → X is the same for everyone); loopsSELECT DISTINCT person FROM standup WHERE date >= today-30; per person N =count(DISTINCT config)of that person’sstandup_datapointrows for today’s standup whose config is an activeEVENING_CHECKLISTconfig and whosevalueis non-empty.N >= X→ skip(done,RAISE LOG). Else check(person, CUSTOM_REMINDER, in_app)notification_preference(fallbacknotification_type_def.default_in_app) — disabled → skip(in_app disabled) (parity withprocess_due_reminders: disabled-in-app = no row AND no push). Else direct-INSERTanotification(type='CUSTOM_REMINDER',title='Evening checklist',body='N/X done — wrap up your day',entity_type='standup',entity_id=today’s standup id or NULL,data={source:'evening_checklist', source_id:'evening_checklist:<YYYY-MM-DD>:<hour>', origin:'reminder', n, x}) withON CONFLICT (recipient, type, (data->>'source_id')) WHERE data ? 'source_id' DO NOTHING(the existingnotification_dedup_uidx), thenRAISE LOG ... -> insert; per-personBEGIN/EXCEPTION WHEN OTHERS+RAISE WARNING. Delivery: thenotificationINSERT fires the existingsend_push_on_notificationAFTER INSERT trigger → pg_net →send-pushEF;origin='reminder'escapes the shadow-mode push silence; push tag falls back tonotification:<id>so the 21:00 and 22:00 pushes don’t collapse into one OS notification; the 22:00 tick re-nudges only if still incomplete (re-checksN<X, andsource_idembeds the hour so it’s a distinct deduped row). Click-through:send-push’sderiveUrlgotcase 'standup': return '/standup'(EF v7,verify_jwtstays false), andhandleClickin bothNotificationPopover.tsxandMobileNotificationsSheet.tsxgot anelse if (notification.entity_type === 'standup') navigate('/standup')branch (+ newNotificationPopover.test.tsx) — tapping the nudge opens/standup, which auto-creates today’s standup row if needed. Hardcoded: nudge hours 21:00/22:00 Europe/Budapest (user-configurable hour out of scope — needs a settings row). Key design choice: “done” =N >= X(filled-in answers), NOTstandup.evening_completed_at IS NOT NULL— the checklist fields autosave on input and there’s no separate “complete” button, soevening_completed_atisn’t a reliable “done” signal; revisit if a real complete affordance is added. Latent multi-tenant note: the fn is SECURITY DEFINER andstandupRLS isWITH CHECK (true), so in a future multi-user world A could insert astanduprow for B → B gets a harmless fixed-content nudge (tracked for a future multi-tenant pass — tightenstandupRLS toperson = auth.uid()). Snooze quirk: it’stype='CUSTOM_REMINDER'so Phase-4 long-press snooze works on it; a snoozed copy re-fires next morning copying the literal “3/4 done” body against tomorrow’s fresh 0/4 — harmless, not special-cased. New reusable gotchas captured: (10) for a “fire at a local wall-clock hour” pg_cron job, schedule'0 * * * *'UTC + gate inside the fn onextract(hour from now() AT TIME ZONE '<zone>')(DST-proof; cron schedules are UTC); (11) usecount(DISTINCT config)notcount(*)when “progress” = “how many distinct items have an answer” (duplicate rows would spuriously cross the threshold); (12) a self-nudging cron job should re-check completion every tick and put a discriminator (the hour) in the dedupsource_idso successive ticks are distinct rows. Specdocs/superpowers/specs/2026-05-11-evening-checklist-reminder-design.md(revised after a 3-perspective DB/security/ops review — the review added the in-app-pref gate, thecount(DISTINCT), theRAISE LOGs, the'standup'click-through), plandocs/superpowers/plans/2026-05-11-evening-checklist-reminder.md. Commits on master:cf6e947(migration),ed5a34d(send-pushEF v7),8fbfda1(click handlers + test). See Evening-Checklist Reminder and Evening-Checklist Reminder (timed nudge). - Scheduled reminders — P2.5/P3/P3+P4-review migrations applied; Phase 3 now live + smoke-tested. All three pending migrations are now applied to remote:
20260512000400_reminder_p2_5_hardening.sql,20260513000100_day_plan_block_reminder_trigger.sql(Phase 3 trigger),20260513000200_reminder_update_guard.sql(reminderBEFORE UPDATE guard — when role isauthenticated, direct UPDATEs are restricted tostatus-only;process_due_remindersrunning as the cron/owner role is unaffected). Phase 3 schema correction:day_plan_block.start_timeis atimestamp with time zone(absolute instant), NOT atimeas an earlier review/spec-draft assumed — so the day-plan-block reminder’sfire_atis juststart_time - interval '5 minutes'(nostandup.datejoin, noAT TIME ZONEmath), and the reminder’s owner comes fromNEW.person(the block’s own RLS-pinned column) not a standup-owner lookup — which fully resolves the cross-person-injection concern the P3 review raised (no SECURITY-DEFINER-amplified read). Gotcha worth keeping: a trigger that didstandup.date + start_timewould have been adate + timestamptzruntime type error, caught only by a live INSERT during smoke-testing sincedatabase.types.tstypesstart_timeasstringeither way. P3 + P4 each got a 4-perspective review (DB/security/frontend/ops); all critical+important findings fixed and shipped. Key fixes: (C1) the day-plan-block trigger’sON CONFLICT DO UPDATEonly resetsstatus='pending'/fired_at=NULLwhenfire_atactually changed AND the newfire_at > now()— editing a block doesn’t re-arm an already-fired reminder; (C2) the singleAFTER INSERT/UPDATE/DELETEtrigger was split intotrg_dpb_reminder_iud(INSERT/DELETE, always) +trg_dpb_reminder_upd(UPDATE,WHENstart_time/status/titlechanged) so the day-planner’s frequent reminder-irrelevant UPDATEs don’t churn; the snooze hook now forwards the original notification’sentity_type/entity_idso a snoozedTASK_ASSIGNEDkeeps its click-through target when re-fired; snooze long-press 350ms→500ms + haptic + press feedback; thereminder_update_guardtrigger (above). Updated open follow-ups: dropped “apply the pending migrations”; kept multi-tenantday_plan_block/standupRLS tightening (now much less urgent — trigger usesNEW.personnot the standup owner),reminder_update_ownRLS rewrite (now partially addressed by the new guard trigger, which closes the direct-PostgREST path), retention/cleanup job, test coverage gap, configurable lead time, custom snooze picker. See scheduled-reminders. - Scheduled reminders (Phases 1-4) shipped. 4-phase reminder system on top of the centralized notification stack. P1 — ad-hoc one-offs:
public.remindertable (RLS scoped toactor_person_id(), no INSERT policy — insert only viacreate_reminderSECURITY DEFINER RPC),process_due_reminders()SQL fn run every minute via pg_cron,/reminderspage +MobileReminders+ReminderCreateDialog+ sidebar/More entries. Reminders bypassemit_notificationon purpose — they direct-INSERT anotificationrow withdata->>'origin'='reminder'becauseemit_notificationhardcodesorigin='event_fanout'which the send-push trigger silences during shadow-mode rollout.CUSTOM_REMINDERregistered innotification_type_def(default_push=true+ vestigialrecipient_selfresolver). P1.5 — fixes:recipient_selfSECDEF→INVOKER,create_remindervalidatesp_source_notification_idownership,process_due_remindersgotFOR UPDATE SKIP LOCKED+RAISE WARNINGhandler + idempotentcron.schedule;send-pushEF v6 falls back tonotification_type_def.default_pushwhen no pref row (the bug: missing row was treated as disabled, so push only fired for user-toggled types) + returns HTTP 500 on real errors (was 200, masking failures) +notification:<id>tag fallback; frontend i18n + loading/error UI + smarter mobile back (navigate(-1)→/more); toast[object Object]fix (Supabase errors are plain objects — pull.message/.details/.hint/.code). P2 — recurring:compute_first_fire_at(recur_rule,tz)+compute_next_fire_at(reminder)SQL helpers (presets daily/weekdays/weekly-day-set/monthly-day-of-month/hourly-every-N), scheduler advancesfire_atinstead of marking fired, 6-tab dialog (Once/Daily/Weekdays/Weekly/Monthly/Hourly) with select-based HH:MM TimePicker + day-of-week chips + nativetype="date"+ Today/Tomorrow/Next-week chips. Scheduler cursor must bepublic.reminder%ROWTYPEnotrecord(elsecannot cast type record to reminder, 42846). P2.5 — fixes (migration20260512000400committed, NOT applied to remote — Supabase MCP expired mid-session):compute_*STABLEnotIMMUTABLE(they callnow()),SET search_path='', monthly clampsday_of_monthviaLEAST(dom, EXTRACT(DAY FROM end-of-month))(was overflowing Feb/30-day months and drifting permanently),RAISE LOGon terminated recurrence; frontend:<Tabs>was wrongly wrapping the always-visible title/body inputs (invalid Radix nesting, broke kbd nav/autoFocus) — restructured to wrap onlyTabsList+TabsContent,DialogContentgotmax-h-[90dvh] overflow-y-auto(6-tab layout clipped Save/Cancel), number inputs → string state, TimePickerrole="group"+aria-labels. P3 — day-plan-block reminders (migration20260513000100committed, NOT applied — zero effect yet):sync_day_plan_block_reminder()SECDEF trigger onday_plan_blockAFTER INSERT/UPDATE/DELETE — PLANNED block withstart_timeupserts a reminder at(standup.date + start_time - interval '5 min') AT TIME ZONE 'Europe/Budapest'(title'Up next: <block>', keyed onreminder_dpb_uidxpartial unique index); DONE/CANCELLED/SKIPPED/NULL-start/delete → cancels. Spec schema was wrong → corrected: column ispersonnotperson_id,start_timeis a TIME, date lives on linkedstandup.date, no tz column (hardcoded Budapest as single-tenant shortcut), status values PLANNED/DONE/CANCELLED/SKIPPED. P4 — snooze: long-press notification on mobile (350ms) or hover Snooze chip on desktop →SnoozePopover(4 presets: 1h/3h/Tomorrow-9am/Next-week, fire time computed client-side in browser tz) →useSnoozeNotification→create_reminderwithsource='snooze',source_notification_id;process_due_remindersre-emits as freshCUSTOM_REMINDER(title/body copied, but losesentity_type/entity_id→ no click-through).SnoozePopovershared by desktopNotificationPopover+ mobileMobileNotificationsSheet. Commits: P1 ~5de4cbc..a390815+5018344+2130a06+c60d2b8, P25d4bd47/4e413eb/6e87c08, P2.56757811(migration, pending)+fa1ea6b(frontend), P3cd67841(migration, pending), P4664a63f. Open follow-ups: apply the 2 pending migrations when MCP re-authorized (P3 has no effect till then); P3+P4 multi-perspective review in progress;reminder_update_ownRLS too permissive (deferred); no retention job forreminder/net._http_response/cron.job_run_details; thin test coverage; configurable lead time + tz; custom snooze picker; snooze click-through target. Specdocs/superpowers/specs/2026-05-10-scheduled-reminders-design.md, plandocs/superpowers/plans/2026-05-10-scheduled-reminders.md. See scheduled-reminders. - Debugging-log entries added for the scheduled-reminders session:
actor_person_id()was reading the legacy singularrequest.jwt.claim.subGUC that Supabase no longer sets — fixed to tryauth.uid()first then pluralrequest.jwt.claims;send-pushtreated a missing notification preference row as disabled — fixed to fall back tonotification_type_def.default_push; toast[object Object](Supabase errors aren’tErrorinstances); plus the Postgres gotcha cluster —IMMUTABLE-vs-now()is a correctness lie (useSTABLE), PL/pgSQL cursor must be%ROWTYPEnotrecord(42846),day+timeyieldstimestampnottimestamptz(needAT TIME ZONE), Supabase MCPapply_migrationassigns its own version so don’t chasesupabase db pushparity. See debugging-log-crm.
2026-05-10
- PWA update prompt (mobile bottom banner) shipped. Replaced silent
autoUpdate+ sonner toast (invisible on iOS standalone PWAs) with explicitregisterType: 'prompt'flow + copper-accented bottom banner above BottomTabs. Removedinstall→skipWaiting()fromweb/src/sw.ts(it was a one-time recovery bridge fromcd8394f); keptactivate→clients.claim()andSKIP_WAITINGmessage handler.useUpdatePromptreturns{ needRefresh, applyUpdate, dismiss }(no toast side-effects); dismissed flag clears on rawneedRefreshfalse→true transition so newer updates re-show the banner.MobileUpdateBannermounted inApp.tsxnext to mobile branch ({isMobile && <MobileUpdateBanner />}), positioned atbottom: calc(50px + env(safe-area-inset-bottom)), z-40. Vitest gotcha:virtual:pwa-register/reactrequires a Vite resolve alias to a stub (web/vitest.config.ts+web/src/test/stubs/pwa-register-react.ts) because import-analysis runs beforevi.mockcan fire. Commitsb9918bf,ab209ad,9e19390,c8b098f. See pwa-update-prompt. - Mobile notifications UI (bell + bottom drawer) shipped. Mobile previously only had
MobileNotificationsScreen(settings — push toggle / preferences / push types) and no in-app notification list. AddedMobileNotificationButton(bell + copper unread badge) wired asMobileHeaderrightSlot default ({rightSlot ?? <MobileNotificationButton />}— pages opt-in to the bell by not passing rightSlot). OpensMobileNotificationsSheet— vaul Drawer at 85vh (same template asSearchSheet) containing PushDiscoveryNudge (extracted toweb/src/components/crm/notifications/PushDiscoveryNudge.tsxand reused by both desktop popover and mobile sheet — DRY), Mark-all-read header, All/Tasks/PRs/System tabs (sameTAB_FILTERSmapping as desktop), ScrollArea ofNotificationItems (reused as-is), footer link to/settings/notifications. Codified two patterns: vaul Drawer = canonical mobile sheet template (next sheet, follow this), andMobileHeaderrightSlot fallback pattern. Test gotcha codified: when default-rendered surfaces pull inuseSupabase, bare-render page tests break — fix by mocking the new component to() => nullrather than wrapping in providers; applied inMobileMoney.test.tsxandMobilePlan.test.tsx. Commitsf2d0512,1a8c1f0,1d4bb18. See mobile-notifications-ui. - P0 production incident — service worker locked all users out of CF Access. Symptom: every user of
wrcm.levandor.io(admin web + iOS PWA) saw full-screenAuthentication Error: No CF Access token available, persisting across refresh. Root cause: the custom SW introduced for push notifications (commitac5f429 build(pwa): switch to injectManifest with custom sw.ts) registered a WorkboxNavigationRoute(createHandlerBoundToURL('/index.html'))inweb/src/sw.ts, which intercepted every navigation and served cached/index.htmlinstead of going to network. Behind CF Access ZTNA this is catastrophic: CF Access expiresCF_Authorizationcookies periodically and refreshes them via a 302 → IdP → 302 dance on the next navigation. With the SW intercepting, that dance never happens, the cookie never refreshes,cf-access.ts:79throws “No CF Access token available” forever, and refresh doesn’t help (SWs persist across refresh). Took ~24h to manifest because users with fresh cookies kept working — only as cookies naturally expired throughout the day did the lockout spread, which misled triage. Fix incd8394f: (1) removeNavigationRoutefromweb/src/sw.ts, (2) addinstall→skipWaiting()+activate→clients.claim()so the new SW takes over immediately, (3) add “Reset session and reload” recovery button on AuthError screen inweb/src/lib/supabase.tsxthat unregisters all SWs + clears caches + reloads. Hard rule going forward: ZTNA + custom SW must never useNavigationRoute. The push-notifications spec must add this as an explicit “don’t.” Stale doc flagged:web/CLAUDE.mdstill claims “anon key, no JWT auth” but JWT exchange viacf-access-authwas reintroduced after769ba5c— separate follow-up. Misleading triage signal: user (correctly) suspected recent centralized notifications work, but the migrations were a red herring; the earlier push-notifications SW commit was the culprit. See incident-2026-05-10-sw-cf-access-lockout.
2026-05-09
- Supabase push notifications research. No implementation yet. Headline: Supabase has no native push product — it provides primitives (Postgres for tokens, Edge Functions for delivery, DB webhooks for triggers); transport is BYO. Three transport options surveyed — Web Push (VAPID + Service Worker) is recommended starting point (fits the existing PWA, browser-native, works in iOS Safari 16.4+ standalone), Expo Push deferred until iOS native app exists, FCM/APNs direct only if Web Push proves insufficient. Existing infra that plugs in:
vite-plugin-pwaalready enabled inweb/vite.config.ts(would needinjectManifestovergenerateSW),notifyedge function already webhook-driven intonotificationtable (extend or siblingsend-push),notification+notification_preferencetables with RLS, in-appNotificationBell/NotificationPopover/NotificationItem+useNotificationshook, iOS PWA meta tags already set inindex.html. Greenfield: VAPID keys,push_subscriptions(user_id, subscription jsonb, created_at, last_seen_at)table with RLS, SWpush+notificationclickhandlers,usePushNotificationshook (gate subscribe behind explicit user gesture), Edge Function usingnpm:web-pushwithPromise.allSettledand DELETE on 410 Gone for cleanup, DB webhook onnotificationINSERT, mobile-detection hook (currently missing). iOS PWA gotchas — only iOS 16.4+, only after Add-to-Home-Screen, 🚨 CF Access ZTNA + iOS standalone cookie spike directly threatenspushManager.subscribe()upsert — must validate before planning. Other gotchas — DB webhook 1000ms timeout (fast or queue), subscription rot (delete on 410), SW must NOT cache user-scoped API responses,userspace/has no PWA. Open decisions: scope (PWA only vs PWA + iOS APNs), which notification types push (extendnotification_preferencewithpush_enabled?), mobile-only or desktop too, validate the cookie spike first. See push-notifications. - Mobile native-feel transformation Phase 5-10 SHIPPED. 18 commits, 37 files, +2,211 / −13 lines. Plan tab (NowHeroCard, UpNextBlockCard, BlockCreate/EditSheet, LaterTodayList swipe actions, DoneTodayCollapse, StandupStreamSheet, MobilePlan composition with
lastEditIdRef+__setPlanQuickAddtyped-global hatch). Hours tab (WeekStrip, MobileHours +useTimesheetWeek). Money tab (BudgetGlanceCard, MobileMoney +InvoiceListItem, BudgetDetailSheet v1). More tab (MobileMore + SectionLink with CF Access logout). Cross-cutting (SearchSheet stub, Sonner top-center). Tests (mock factories, MobileShell + MobileHome/Plan/Hours/Money render tests, IntersectionObserver stub). See mobile-native-feel. - Audit pass — 7 fixes. Lockfile sync (
60a1acdblocking CF Pages frozen-lockfile build). Hours-tab “garbage hours” UTC date drift bug (01880a0) —toISOString().slice(0,10)is timezone-unsafe; fixed via newtoLocalDateString()inweb/src/lib/date-utils.ts, same bug inuseDashboardSummary.ts, dedupstartOfWeek, 8 unit tests added.formatTimededup (8e7b0ad).window.confirm()→ inline two-step Cancel + Confirm-delete (dcdbec7). StandupStreamSheet render-time setState → useEffect (7da8153). Type__setPlanQuickAddviadeclare global+ memoize MobilePlan status-only collections (2c5960c). StabilizeusePullToRefresheffect deps (was rebinding 50+ touch listeners per gesture, breaking iOS PtR) + MobileHeader duplicate-h1 a11y fix (ee1652e). See Audit Pass and debugging-log-crm. - Mobile UI redesign — 4 commits. Drawer surface tokens (bg-white + warm scrim) + explicit input/select text colors fixing the invisible-dropdown-text bug (
<select>+appearance-none+ Lucide ChevronDown overlay) (7f4bc84). Unified hero/card/list visual treatment (2a43df8). Refined header, bottom tabs, sheet internals — BottomTabs got a 2px-tall active-pill indicator (60f7fa6). Polish — WeekStrip, StatusFilterPills, settings list, Sign out → rose-600 (31936a5). - Bug fixes + Levandor copper brand pass. Hours card respects selected day (“Today” only when
active === today) + wire CurrencyProvider for HUF (replacing hardcoded EURIntl.NumberFormat) (2c82bd0). Brand pass (9448637) — discovered actual brand inpackages/ui/src/styles/globals.css:--copper: hsl(31 53% 64%)≈ d8a777,--warm-black: hsl(40 11% 5%). Mobile heroes switched frombg-zinc-900to warm-black; selected pills (WeekStrip, StatusFilterPills, BottomTabs active indicator + label + icon) use copper bg with warm-black text; hero progress bars use copper fill; ProjectHealthCard “green” status dot becomes copper. See levandor-brand. - Forecast horizon fix — 2 commits. ForecastHero was comparing weekly revenue against monthly forecast (apples-to-oranges, the 27% looked off because of mixed temporal scopes). First fix
netThisMonth/netForecast“This month” (1697360). Final fix (6f1fdb5) — mobile ForecastHero rewritten to mirror desktop dashboard hero exactly: eyebrow + big number + caption (avg_hrs × workdays × rate) + 2×2 stat strip. See dashboard-forecast. - Phone-native day rituals rebuild — 2 commits. Replaced StandupStreamSheet (5 desktop components stuffed in a 90vh sheet) with 4 focused phone-native sheets — MorningCheckinSheet (single scrollable form, phone-native input controls per datapoint type: NUMBER pills 44px tap, SCALE 0..max round circles, BOOLEAN big two-button toggle, TEXT autosizing textarea), StandupNotesSheet (phone journal feed with bottom-pinned composer + brand-tinted type chips: GOAL=copper, BLOCKER=amber, WIN=emerald), LunchCheckinSheet (focused mid-day form), EveningReflectionSheet (autosizing summary + day-in-numbers stat strip + Close-day stamp). MobilePlan rewired with 5 sheet states + time-aware CTAs (
b7626d9). Evening Checklist v1 manifestation tracker (482e592) shipped both mobile sheet (EveningChecklistSheet) + desktop card (EveningChecklistCard). See mobile-day-rituals. - Evening Checklist backend wiring — 2 commits. Migration 008 (
3c3501d) widenedstandup_datapoint_config_phase_checkCHECK to include'EVENING_CHECKLIST', seeded 3 free-text prompts (proud_of, not_proud_of, other_notes). Migration applied to remotemkofmdtdldxgmmolxxhc. Both sheet + card now real, persist viauseUpsertDatapoint(upserts on(standup, config)— no duplicates on edit). Migration 009 (2849661) added 4th “Shit I overspent on” prompt (key=overspent_today, sort_order=4, optional TEXT). Idempotent INSERT + UPDATE. See evening-checklist. - Debounced autosave — 2 commits. New
useDebouncedAutosave(value, save, delay=600ms)hook inweb/src/lib/hooks/, returns AutosaveStatus, skips initial mount (f718425). EveningChecklistSheet (mobile) + EveningChecklistCard (desktop) refactored to per-fieldChecklistFieldwith autosave; Save buttons removed; aggregate (worst-of) status pill in header. Then (7d18acb) MorningCheckinSheet (mobile) per-field MorningField subcomponent with autosave, EveningReflectionSheet summary autosaves, desktop standup page (pages/standup/index.tsx) gets parent-level debounced autosave for both morning intake (per-config Map of timers) and evening summary (single timer). Completion buttons (Complete morning, Close day) flush pending timers + stamp the phase flag only — data is already persisted. See mobile-autosave. - Mobile native-feel design spec patched after pre-mortem. Initial spec at
docs/superpowers/specs/2026-05-09-mobile-native-feel-design.mdwas validated by a 4-judge council (agentops:council --deep --preset=plan-review) which returned WARN×4 / HIGH confidence with 46 findings clustering on factual errors. Convergent fixes applied inline: auth was Clerk in spec → corrected to CF Access ZTNA (with iOS standalone cookie spike flagged as plan-blocker); draft-timesheet status/source columns invented → added migration + atomiccomplete_block_with_draft_v1RPC with partial unique index for idempotency;dashboard_summary_v1RPC referenced wrong tables (timesheet_entry,project_v) → fixed + scoped as full revenue port (~5h backend); PDF<object>embed doesn’t render in iOS PWA standalone → replaced with link-out; sheet state ownership pattern added; font-display token left alone (it’s DM Serif Display) — mobile uses font-sans (Outfit) instead. 19 medium/low items captured as plan-task hints in spec. See mobile-native-feel for full decision log. - Started mobile native-feel transformation brainstorm — see mobile-native-feel. Locked 10 decisions (PWA, iPhone-only, shell-only offline, Levandor-branded visual style, 5 bottom tabs, Now-focused day-plan, day-first timesheet, mobile-first rebuild strategy). Chunk 1 (foundations) approved; Chunk 2 (per-surface designs) proposed; Chunk 3 (visual system + perf + testing) pending. Spec doc not yet written.
2026-05-04
- Initialized activity log.