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 by obsidian-documenter on every project doc write. Read by historian at bootstrap (top ~15 entries).

2026-05-17

  • Cross-linked Polymarket top-trader pipeline from integrations — external Rust binary writes pm_* tables in the mgmt Supabase project; CRM consumes via createTableQuery once database.types.ts is 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.tsx computed the standup “day” as toLocalDateString(new Date()) (raw local calendar date), mount-frozen via useMemo(…, []), and passed that one standupId to 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-11 standup row and saved the 3 EVENING_CHECKLIST datapoints (+ the BOTH-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 (standup has UNIQUE(person, date), standup_datapoint has UNIQUE(standup, config) — just attached to the wrong day). Fix (commit 1bf861a, fix(standup): evening checklist/reflection day starts at 15:00 local): new eveningStandupDate(now = new Date()): string in web/src/lib/date-utils.tsconst EVENING_STANDUP_DAY_START_HOUR = 15; now.getHours() < 15 → yesterday’s toLocalDateString, else today’s (so an “evening-standup day D” spans 15:00 on D → 14:59 on D+1; uses getHours() = local deliberately). MobilePlan.tsx: the evening checklist + evening reflection sheets now target useStandup(person, eveningStandupDate())’s row (eveningStandupId), with a second on-demand useEnsureStandup(eveningDate) (guarded by eveningDate !== date); the “Evening” nudge button is gated on that row’s morning_completed_at/evening_completed_at; morning ritual / lunch checkin / day plan blocks / standup notes are unchanged — still calendar-date (standupId); date/eveningDate are no longer mount-frozen — they recompute on document.visibilitychange (long-lived PWA doesn’t go stale across midnight/15:00). Added web/src/lib/__tests__/date-utils.test.ts (4 cases). Desktop /standup page (pages/standup/index.tsx) NOT changed — has a date picker, computes “today” via toISODateString(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 UTC todayStr not 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, no UNIQUE(standup, config) conflict); May-11 row now empty. Action item open: fix is committed + pushed but not deployed — needs pnpm --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, not toISODateString — the desktop standup page gets this subtly wrong but gets lucky); (b) don’t mount-freeze it (useMemo(…, [])) in a PWA — recompute on visibilitychange; (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 mobile MobilePlan “mount-frozen date selector deliberately not fixed” item in Mount-frozen date selectors is now partially fixed for this screen (MobileHours.active still freezes). See evening-checklist-day-boundary-fix-2026-05-11.
  • DRY / simplification refactor pass shipped — 9 commits on master (d035235..ccf6f57, on top of 8fbfda1), 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 on tsc -b clean + vitest (one pre-existing MobileHours.test.tsx failure, no new) + green build. P0 — dead code: deleted web/src/lib/database.types.ts (4141-line byte-identical orphan of packages/shared/src/database.types.ts), 4 never-wired-up packages/ui components (animated-list/bento-grid/chart/text-animate), the OLD dead web/src/components/crm/PageHeader.tsx (a NEW PageHeader lands in P2a), useReorderDayPlanBlocks+test, useDeleteOnCallSchedule, userspace/src/lib/hooks/useClientUser.ts, and web/supabase/functions/notify/ (legacy notify EF, superseded by event → fn_event_to_notifications → emit_notification). CAVEAT for the user: confirm no Supabase-dashboard Database Webhook still points at notify before removing it server-side (only deleted from the repo here). P1a — hook factories: extended web/src/lib/hooks/createTableQuery.ts (added createMaybeSingleQuery/createSingleQuery/createTableQueryWithArgs/createMaybeSingleQueryWithArgs) + createMutation.ts (multi-key/function-form invalidateKeys, returning for .select().single(), new createRpcMutation); 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: extended CreateEntityDialog into the standard form-dialog shell (submitLabel/pendingLabel/maxWidth/extraFooterLeft/showPendingSpinner/trigger; later description: ReactNode + additive contentClassName); migrated 10 dialogs (AddTransaction/AddLoanee/AddConfiscation/AddPayback/CreateAccount/Envelope/Transfer/LinkBillingo/CreateInvoice); MappingRuleDialog NOT migratedCreateEntityDialog wraps children in a <form> and MappingActionBuilder’s Add/remove buttons lack type="button" → would submit; mixed @tanstack/react-form vs local useState left 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: downloadBlobcsv-export.ts (deleted byte-identical downloadTimesheetCsv), getMonthRangedate-utils.ts, userspace statusColor/statusLabel→new userspace/src/lib/project-status.ts, ~16 inline .toISOString().slice(0,10)toISODateString(), dropped private toDateStr/toISODateString copies for toLocalDateString, CSVImportWizard/MappingRuleDialog error banners→MUTATION_ERROR. P2a — list-page primitives: new web/src/components/crm/{PageHeader,ExportCsvButton,CreateButton,ErrorBanner,CardSkeleton(+SkeletonLines),ResourceTable}.tsx + web/src/lib/hooks/useUrlListState.ts (all tested); rolled out across projects/partners/contracts/people (headers+buttons+error) and tasks/invoices/inventory (buttons+error; their headers are structural variants); tasks/index.tsx URL-param boilerplate→useUrlListState; dashboard StatCardSkeleton+automations runner skeleton→SkeletonLines (other bespoke skeletons left — would change loading visuals); side effect: 3 create buttons gained data-create-action (the c shortcut). P2b — project-detail tabs: useInvoices/useContracts/useInventory/useTasks now take an optional projectId arg (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 keys projectInvoices/projectContracts/projectInventory/projectTasks/projectFiles (so existing ['invoices'] etc. invalidations still prefix-match); InvoicesTab/ContractsTab/InventoryTabResourceTable, TasksTab/FilesTab→scoped hooks (their rendering isn’t a plain table); TimesheetsTab left over-fetching (footer row + useTimesheets out of scope); FilesTab list→new useProjectFiles, OverviewTab.toggleOnCalluseUpdateProject (both off direct useSupabase()); InventoryTab switched from name-match to FK-id-match filtering (more correct — a behavior change). P2c — shared hook logic + useSupabase() bypasses: on-call-rates.ts got buildRateMaps/entryHours/entryRate (the ${person}_${project} rate maps + standby/activated/multipliers ternary were copy-pasted across 5 dashboard/on-call hooks) + new tests for those AND resolveOnCallRate (was untested financial code); makeStorageAttachmentHooks (useInvoicePdf/useContractPdf were the same module twice, 95/87→18/18 lines); foreign-resource.ts+ForeignResourceRow lifted to matcher.ts (copy-pasted interface + create-FR-then-insert-mappings/delete plumbing shared by 3 integration-mapping hooks; useStatusMappings.createMapping left inline — single-object vs array; useStorageProjectAssignments create-branch later wired to the helper); TaskChips.tsx+getTaskCardModel dedupe TaskCard/KanbanCard chip rendering — fixed TaskCard’s empty AvatarFallback bug (now shows initials), TaskCard priority pill now uses @/lib/colors (matches Kanban dots — MEDIUM amber not copper, HIGH red-500 not the status-unpaid token — deliberate palette unification, flagged for the user); DayPlanner/KanbanBoard/TaskDetailSheet/CSVImportWizard moved off direct useSupabase() — new useCreateTaskFromBlock, useBudgetTransactionLookups, KanbanBoard/TaskDetailSheet reuse useToggleTaskStatus (FilesTab still uses useSupabase() for one imperative createSignedUrl download — doesn’t fit a query hook). P2d — colors typed: stringly-typed getStatusBorderColor/getStatusTextColor(domain: string, status: string) → typed getProject/InvoiceStatusBorder/TextColor(status: ProjectStatus|InvoiceStatus) (exhaustiveness now compile-checked); dropped dead textMaps.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, missed ErrorBanner/SkeletonLines adoptions, no-op setSearchParams short-circuit, unused prop, lost bold name restored, tableBuilder/callRpc extracted to web/src/lib/hooks/supabase-builders.ts). Deferred (logged for future sessions): edge-function _shared/ consolidation of the ~6 functions hand-rolling createClient/OPTIONS-preflight/auth-checks (touches deployed functions incl. service-health which is in the app shell/sidebar, can’t be verified/deployed from a refactor pass, small win after excluding cf-access-auth/send-push — needs its own reviewed+deployed change); the useDayPlanSuggestions.ts/useOnCallMonthly.ts UTC-drift bugs (need a deliberate UTC-vs-local decision, not a mechanical dedupe — desktop fix elsewhere was toLocalDateString); packages/tender-pipeline (unshipped — nothing imports it, tender-sync EF 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 (CSVImportWizard 1497 lines, BudgetOverview, the god pages invoices/settings/tasks/system-integrations/dashboard); billingo-sync-statuses hardcoded 1.27 HUF VAT multiplier applied to ALL currencies; non-atomic client-side multi-step useCreateInvoice/useCreateBudgetTransfer (should be RPCs — complete_block_with_draft_v1/dashboard_summary_v1 are precedents); web/src/mobile/lib/formatTime.ts uses local TZ where day-plan blocks (stored 1970-01-01THH:MM:00Z) need UTC; possibly-dead Linear-push web/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(several Task*Chip exports)/getTaskCardModel+useTaskCardModel/CreateEntityDialog (now the standard form-dialog shell — see its props; wraps children in a <form>, nested buttons need type="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.ts now exports ForeignResourceRow/IntegrationMappingRow, csv-export.ts downloadBlob, colors.ts typed 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 reminder table / process_due_reminders / the day-plan-block trigger). New process_evening_checklist_reminders() SECURITY DEFINER SQL fn called by a new pg_cron job process-evening-checklist-reminders on '0 * * * *' (every hour, UTC); the fn self-gates to extract(hour from now() AT TIME ZONE 'Europe/Budapest') IN (21, 22) — DST-proof, 22/24 ticks are a one-statement RETURN. When it acts: X = count of active standup_datapoint_config rows with phase='EVENING_CHECKLIST' (currently 4; standup_datapoint_config is global, no person col → X is the same for everyone); loops SELECT DISTINCT person FROM standup WHERE date >= today-30; per person N = count(DISTINCT config) of that person’s standup_datapoint rows for today’s standup whose config is an active EVENING_CHECKLIST config and whose value is non-empty. N >= X → skip(done, RAISE LOG). Else check (person, CUSTOM_REMINDER, in_app) notification_preference (fallback notification_type_def.default_in_app) — disabled → skip(in_app disabled) (parity with process_due_reminders: disabled-in-app = no row AND no push). Else direct-INSERT a notification (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}) with ON CONFLICT (recipient, type, (data->>'source_id')) WHERE data ? 'source_id' DO NOTHING (the existing notification_dedup_uidx), then RAISE LOG ... -> insert; per-person BEGIN/EXCEPTION WHEN OTHERS + RAISE WARNING. Delivery: the notification INSERT fires the existing send_push_on_notification AFTER INSERT trigger → pg_net → send-push EF; origin='reminder' escapes the shadow-mode push silence; push tag falls back to notification:<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-checks N<X, and source_id embeds the hour so it’s a distinct deduped row). Click-through: send-push’s deriveUrl got case 'standup': return '/standup' (EF v7, verify_jwt stays false), and handleClick in both NotificationPopover.tsx and MobileNotificationsSheet.tsx got an else if (notification.entity_type === 'standup') navigate('/standup') branch (+ new NotificationPopover.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), NOT standup.evening_completed_at IS NOT NULL — the checklist fields autosave on input and there’s no separate “complete” button, so evening_completed_at isn’t a reliable “done” signal; revisit if a real complete affordance is added. Latent multi-tenant note: the fn is SECURITY DEFINER and standup RLS is WITH CHECK (true), so in a future multi-user world A could insert a standup row for B → B gets a harmless fixed-content nudge (tracked for a future multi-tenant pass — tighten standup RLS to person = auth.uid()). Snooze quirk: it’s type='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 on extract(hour from now() AT TIME ZONE '<zone>') (DST-proof; cron schedules are UTC); (11) use count(DISTINCT config) not count(*) 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 dedup source_id so successive ticks are distinct rows. Spec docs/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, the count(DISTINCT), the RAISE LOGs, the 'standup' click-through), plan docs/superpowers/plans/2026-05-11-evening-checklist-reminder.md. Commits on master: cf6e947 (migration), ed5a34d (send-push EF 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 (reminder BEFORE UPDATE guard — when role is authenticated, direct UPDATEs are restricted to status-only; process_due_reminders running as the cron/owner role is unaffected). Phase 3 schema correction: day_plan_block.start_time is a timestamp with time zone (absolute instant), NOT a time as an earlier review/spec-draft assumed — so the day-plan-block reminder’s fire_at is just start_time - interval '5 minutes' (no standup.date join, no AT TIME ZONE math), and the reminder’s owner comes from NEW.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 did standup.date + start_time would have been a date + timestamptz runtime type error, caught only by a live INSERT during smoke-testing since database.types.ts types start_time as string either 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’s ON CONFLICT DO UPDATE only resets status='pending'/fired_at=NULL when fire_at actually changed AND the new fire_at > now() — editing a block doesn’t re-arm an already-fired reminder; (C2) the single AFTER INSERT/UPDATE/DELETE trigger was split into trg_dpb_reminder_iud (INSERT/DELETE, always) + trg_dpb_reminder_upd (UPDATE, WHEN start_time/status/title changed) so the day-planner’s frequent reminder-irrelevant UPDATEs don’t churn; the snooze hook now forwards the original notification’s entity_type/entity_id so a snoozed TASK_ASSIGNED keeps its click-through target when re-fired; snooze long-press 350ms→500ms + haptic + press feedback; the reminder_update_guard trigger (above). Updated open follow-ups: dropped “apply the pending migrations”; kept multi-tenant day_plan_block/standup RLS tightening (now much less urgent — trigger uses NEW.person not the standup owner), reminder_update_own RLS 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.reminder table (RLS scoped to actor_person_id(), no INSERT policy — insert only via create_reminder SECURITY DEFINER RPC), process_due_reminders() SQL fn run every minute via pg_cron, /reminders page + MobileReminders + ReminderCreateDialog + sidebar/More entries. Reminders bypass emit_notification on purpose — they direct-INSERT a notification row with data->>'origin'='reminder' because emit_notification hardcodes origin='event_fanout' which the send-push trigger silences during shadow-mode rollout. CUSTOM_REMINDER registered in notification_type_def (default_push=true + vestigial recipient_self resolver). P1.5 — fixes: recipient_self SECDEF→INVOKER, create_reminder validates p_source_notification_id ownership, process_due_reminders got FOR UPDATE SKIP LOCKED + RAISE WARNING handler + idempotent cron.schedule; send-push EF v6 falls back to notification_type_def.default_push when 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 advances fire_at instead of marking fired, 6-tab dialog (Once/Daily/Weekdays/Weekly/Monthly/Hourly) with select-based HH:MM TimePicker + day-of-week chips + native type="date" + Today/Tomorrow/Next-week chips. Scheduler cursor must be public.reminder%ROWTYPE not record (else cannot cast type record to reminder, 42846). P2.5 — fixes (migration 20260512000400 committed, NOT applied to remote — Supabase MCP expired mid-session): compute_* STABLE not IMMUTABLE (they call now()), SET search_path='', monthly clamps day_of_month via LEAST(dom, EXTRACT(DAY FROM end-of-month)) (was overflowing Feb/30-day months and drifting permanently), RAISE LOG on terminated recurrence; frontend: <Tabs> was wrongly wrapping the always-visible title/body inputs (invalid Radix nesting, broke kbd nav/autoFocus) — restructured to wrap only TabsList+TabsContent, DialogContent got max-h-[90dvh] overflow-y-auto (6-tab layout clipped Save/Cancel), number inputs → string state, TimePicker role="group"+aria-labels. P3 — day-plan-block reminders (migration 20260513000100 committed, NOT applied — zero effect yet): sync_day_plan_block_reminder() SECDEF trigger on day_plan_block AFTER INSERT/UPDATE/DELETE — PLANNED block with start_time upserts a reminder at (standup.date + start_time - interval '5 min') AT TIME ZONE 'Europe/Budapest' (title 'Up next: <block>', keyed on reminder_dpb_uidx partial unique index); DONE/CANCELLED/SKIPPED/NULL-start/delete → cancels. Spec schema was wrong → corrected: column is person not person_id, start_time is a TIME, date lives on linked standup.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) → useSnoozeNotificationcreate_reminder with source='snooze', source_notification_id; process_due_reminders re-emits as fresh CUSTOM_REMINDER (title/body copied, but loses entity_type/entity_id → no click-through). SnoozePopover shared by desktop NotificationPopover + mobile MobileNotificationsSheet. Commits: P1 ~5de4cbc..a390815+5018344+2130a06+c60d2b8, P2 5d4bd47/4e413eb/6e87c08, P2.5 6757811(migration, pending)+fa1ea6b(frontend), P3 cd67841(migration, pending), P4 664a63f. 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_own RLS too permissive (deferred); no retention job for reminder/net._http_response/cron.job_run_details; thin test coverage; configurable lead time + tz; custom snooze picker; snooze click-through target. Spec docs/superpowers/specs/2026-05-10-scheduled-reminders-design.md, plan docs/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 singular request.jwt.claim.sub GUC that Supabase no longer sets — fixed to try auth.uid() first then plural request.jwt.claims; send-push treated a missing notification preference row as disabled — fixed to fall back to notification_type_def.default_push; toast [object Object] (Supabase errors aren’t Error instances); plus the Postgres gotcha cluster — IMMUTABLE-vs-now() is a correctness lie (use STABLE), PL/pgSQL cursor must be %ROWTYPE not record (42846), day+time yields timestamp not timestamptz (need AT TIME ZONE), Supabase MCP apply_migration assigns its own version so don’t chase supabase db push parity. 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 explicit registerType: 'prompt' flow + copper-accented bottom banner above BottomTabs. Removed install→skipWaiting() from web/src/sw.ts (it was a one-time recovery bridge from cd8394f); kept activate→clients.claim() and SKIP_WAITING message handler. useUpdatePrompt returns { needRefresh, applyUpdate, dismiss } (no toast side-effects); dismissed flag clears on raw needRefresh false→true transition so newer updates re-show the banner. MobileUpdateBanner mounted in App.tsx next to mobile branch ({isMobile && <MobileUpdateBanner />}), positioned at bottom: calc(50px + env(safe-area-inset-bottom)), z-40. Vitest gotcha: virtual:pwa-register/react requires a Vite resolve alias to a stub (web/vitest.config.ts + web/src/test/stubs/pwa-register-react.ts) because import-analysis runs before vi.mock can fire. Commits b9918bf, 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. Added MobileNotificationButton (bell + copper unread badge) wired as MobileHeader rightSlot default ({rightSlot ?? <MobileNotificationButton />} — pages opt-in to the bell by not passing rightSlot). Opens MobileNotificationsSheet — vaul Drawer at 85vh (same template as SearchSheet) containing PushDiscoveryNudge (extracted to web/src/components/crm/notifications/PushDiscoveryNudge.tsx and reused by both desktop popover and mobile sheet — DRY), Mark-all-read header, All/Tasks/PRs/System tabs (same TAB_FILTERS mapping as desktop), ScrollArea of NotificationItems (reused as-is), footer link to /settings/notifications. Codified two patterns: vaul Drawer = canonical mobile sheet template (next sheet, follow this), and MobileHeader rightSlot fallback pattern. Test gotcha codified: when default-rendered surfaces pull in useSupabase, bare-render page tests break — fix by mocking the new component to () => null rather than wrapping in providers; applied in MobileMoney.test.tsx and MobilePlan.test.tsx. Commits f2d0512, 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-screen Authentication Error: No CF Access token available, persisting across refresh. Root cause: the custom SW introduced for push notifications (commit ac5f429 build(pwa): switch to injectManifest with custom sw.ts) registered a Workbox NavigationRoute(createHandlerBoundToURL('/index.html')) in web/src/sw.ts, which intercepted every navigation and served cached /index.html instead of going to network. Behind CF Access ZTNA this is catastrophic: CF Access expires CF_Authorization cookies 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:79 throws “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 in cd8394f: (1) remove NavigationRoute from web/src/sw.ts, (2) add install→skipWaiting() + activate→clients.claim() so the new SW takes over immediately, (3) add “Reset session and reload” recovery button on AuthError screen in web/src/lib/supabase.tsx that unregisters all SWs + clears caches + reloads. Hard rule going forward: ZTNA + custom SW must never use NavigationRoute. The push-notifications spec must add this as an explicit “don’t.” Stale doc flagged: web/CLAUDE.md still claims “anon key, no JWT auth” but JWT exchange via cf-access-auth was reintroduced after 769ba5c — 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-pwa already enabled in web/vite.config.ts (would need injectManifest over generateSW), notify edge function already webhook-driven into notification table (extend or sibling send-push), notification + notification_preference tables with RLS, in-app NotificationBell/NotificationPopover/NotificationItem + useNotifications hook, iOS PWA meta tags already set in index.html. Greenfield: VAPID keys, push_subscriptions(user_id, subscription jsonb, created_at, last_seen_at) table with RLS, SW push + notificationclick handlers, usePushNotifications hook (gate subscribe behind explicit user gesture), Edge Function using npm:web-push with Promise.allSettled and DELETE on 410 Gone for cleanup, DB webhook on notification INSERT, 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 threatens pushManager.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 (extend notification_preference with push_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 + __setPlanQuickAdd typed-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 (60a1acd blocking CF Pages frozen-lockfile build). Hours-tab “garbage hours” UTC date drift bug (01880a0) — toISOString().slice(0,10) is timezone-unsafe; fixed via new toLocalDateString() in web/src/lib/date-utils.ts, same bug in useDashboardSummary.ts, dedup startOfWeek, 8 unit tests added. formatTime dedup (8e7b0ad). window.confirm() → inline two-step Cancel + Confirm-delete (dcdbec7). StandupStreamSheet render-time setState → useEffect (7da8153). Type __setPlanQuickAdd via declare global + memoize MobilePlan status-only collections (2c5960c). Stabilize usePullToRefresh effect 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 EUR Intl.NumberFormat) (2c82bd0). Brand pass (9448637) — discovered actual brand in packages/ui/src/styles/globals.css: --copper: hsl(31 53% 64%)d8a777, --warm-black: hsl(40 11% 5%). Mobile heroes switched from bg-zinc-900 to 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) widened standup_datapoint_config_phase_check CHECK to include 'EVENING_CHECKLIST', seeded 3 free-text prompts (proud_of, not_proud_of, other_notes). Migration applied to remote mkofmdtdldxgmmolxxhc. Both sheet + card now real, persist via useUpsertDatapoint (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 in web/src/lib/hooks/, returns AutosaveStatus, skips initial mount (f718425). EveningChecklistSheet (mobile) + EveningChecklistCard (desktop) refactored to per-field ChecklistField with 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.md was 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 + atomic complete_block_with_draft_v1 RPC with partial unique index for idempotency; dashboard_summary_v1 RPC 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.