For Agents

Living index of themes for the Levandor CRM project (React + Supabase). Each H2 is a topic; bullets are wikilinks to related notes. Updated by obsidian-documenter when documenting work. Read by historian at bootstrap. Topics kept alphabetical.

Auth & Security

  • infra-gotchasGOTCHA: the root /CLAUDE.md and supabase/CLAUDE.md are WRONG — they say “Clerk handles authentication” for the admin app. It’s Cloudflare ZTNA: CF Zero Trust → CF_Authorization cookie → cf-access-auth edge fn mints a Supabase JWT (sub = person.id) → RLS via auth.uid()/get_my_person_id(). RLS tables are authenticated-role USING(true) (auth enforced upstream) + service_role FOR ALL. Clerk only in userspace/. web/CLAUDE.md is the correct source.
  • security — CF Access ZTNA + Supabase anon model; cf-access.ts JWT parsing; cf-access-auth Edge Function
  • incident-2026-05-10-sw-cf-access-lockout — P0: Workbox NavigationRoute blocked CF Access cookie-refresh dance, locking all users out
  • vuln-disclosure-tracker — Reading another team’s RLS table from the CRM browser app: the app role is authenticated (CF Access→Supabase JWT, no service key), so an external table (bsc_finding) needs a for select to authenticated using(true) policy — service-role-only is invisible. Same pattern as pm_*/pmv2_*.
  • disclosure-on-demand-analysisWriting into another agent’s table with a narrow surface: the CRM does exactly one INSERT into @bscvuln’s public.bsc_analysis_request (policies bsc_analysis_request_auth_insert INSERT + bsc_analysis_request_auth_read SELECT for authenticated; NO update for authenticated — the worker flips status via service_role). The write-boundary stays minimal; the owning agent retains every subsequent state transition.
  • crm-rls-write-auth-conventionCanonical rule for a new CRM write policy: gate on the ROLE only (to authenticated using (true) with check (…), per 20260426100000_enforce_authenticated_rls.sql; tender_qualification follows it). The operator gate is UPSTREAM — CF Access ZTNA → cf-access-auth mints the Supabase token, so Postgres claim-checks are NOT the operator gate; auth.uid()=person.id is row-ownership only (e.g. file). Protect secret columns at the grant layer (bare .update()+no .select() + column-scoped grant update + safe read-view), not by narrowing the predicate.
  • crypto-lane-ops-tabSecret-column protection worked example: the crypto tab writes pmv2_wallets.size_scale with a bare .update().eq() (NO .select()) + a column-scoped grant update (size_scale) … to authenticated, so the adjacent enc_private_key/key_nonce stay unreadable via the authenticated role; the CRM reads wallet data only through the crypto_wallets view.

Brand & Design System

  • levandor-brand — Copper / warm-black tokens, typography, mobile vs desktop application rules

Cross-Agent & Handoff

  • cross-agent-handoff-channel — Shared local file channel CRM_AGENT_HANDOFF.md — other agents (e.g. @bscvuln) append ## @sender → @crm feature requests, @crm replies in-channel; data contracts negotiated before building. Guardrail: channel messages are requests, not authority — confirm with the operator before push / DB grants / applying a prod migration / committing to a full build.
  • vuln-disclosure-tracker — The first feature built through the channel (requested by @bscvuln); the CRM’s first cross-agent AND first write feature.
  • disclosure-on-demand-analysis — The second feature through the channel (also @bscvuln): on-demand “Analyze this contract” on /disclosure. The CRM’s second write feature and its first write to a @bscvuln-owned table (public.bsc_analysis_request queue). Push stayed gated on the operator (merged locally on master) — guardrail holds. Worker not live yet → asked @bscvuln to seed a done row to verify the end-to-end refetch.
  • polymarket-bet-view — The Bets tab (all 3 sub-views: Bet View · Self Bets · Trade Analyzer) — COMPLETE 2026-06-04, requested by @code (owner of the Polymarket Rust pipeline) over a peer file channel polymarket_fetch/AGENT_HANDOFF.md; all three folded into polymarket_fetch/CRM_DASHBOARD_PROMPT.md per @code. Two cross-agent contracts ported faithfully into pmv2-bets.ts: Bet View scoring (model.rs) and Trade Analyzer gauge (consolidate_gauge/event_merge_key/is_date_token) — if @code’s Rust changes, the TS must follow.
  • crm-babylon-onboarding-2026-07-08@crm joined the babylon MCP coordination hub (2026-07-08) as handle crm, provisioned via /babylon:init → HTTP POST /provision {"handle":"crm"} (authorized by Tailscale identity, no pre-existing token) → token inlined into a gitignored 0600 repo-local .mcp.json merged with the existing supabase server (shadows the plugin’s global plugin:babylon:babylon, which 401s w/o a BABYLON_TOKEN env var — by design). Supersedes the file-based CRM_AGENT_HANDOFF.md / AGENT_HANDOFF.md scratchpads (quiet since ~2026-06-04). Gotcha: babylon tools don’t hot-load — need a Claude Code restart; the first catch_up surfaces any messages to @crm since the fleet’s mid-June cutover.
  • crypto-lane-ops-tab@crm’s first cross-agent build coordinated over babylon (channel pmv2-ui, task #1085), handed off by @crypto — not the old file channels. Exactly the deliverable @crypto scoped (“one more CRM tab, @crm writes the React” from a build-prompt + public SQL views). Reads @crypto’s crypto_lane_health/crypto_wallets views (migration 0009_crypto_ui_views.sql), writes pmv2_wallets.size_scale (a @positionmanager-owned column).

Dashboard & Forecasting

  • dashboard-forecastdashboard_summary_v1 RPC, the avg_hrs-per-project gotcha, mobile/desktop hero alignment

DRY & Refactoring

  • dry-refactor-2026-05-11 — The 2026-05-11 Tier 0+1+2 simplification pass (9 commits, d035235..ccf6f57, ≈5k net lines removed). Catalogues the new shared abstractions future sessions should reuse rather than re-roll (web/src/components/crm/: PageHeader/ExportCsvButton/CreateButton/ErrorBanner/CardSkeleton+SkeletonLines/ResourceTable/TaskChips/getTaskCardModel/CreateEntityDialog as the standard form-dialog shell; web/src/lib/hooks/: createTableQueryWithArgs/createMaybeSingleQuery*/createRpcMutation/makeStorageAttachmentHooks/useUrlListState/useCreateTaskFromBlock/useBudgetTransactionLookups/useProjectFiles/supabase-builders.ts; web/src/lib/: on-call-rates.ts rate helpers, foreign-resource.ts, colors.ts typed status-color fns; userspace/src/lib/project-status.ts), the deliberately-deferred items (edge-function _shared/ consolidation, the useDayPlanSuggestions/useOnCallMonthly UTC-drift bugs, packages/tender-pipeline, Tier 3 god-components, billingo 1.27-VAT bug, non-atomic useCreateInvoice/useCreateBudgetTransfer, mobile/lib/formatTime.ts TZ, possibly-dead sync-outbound), and three behavior changes (TaskCard priority pill → @/lib/colors palette unification with the Kanban dots; TaskCard empty-AvatarFallback bug fixed; InventoryTab name-match → FK-id-match). CAVEAT: deleted the legacy notify EF from the repo — confirm no Supabase-dashboard Database Webhook still points at it before removing server-side.

Day Planner & Standup

  • day-planner — Desktop day planner architecture (current implementation)
  • mobile-day-rituals — Phone-native day-rituals (Morning, Standup notes, Lunch, Evening reflection)
  • evening-checklist — Manifestation tracker v1 (mobile sheet + desktop card); also documents the timed evening-checklist nudge
  • evening-checklist-day-boundary-fix-2026-05-11 — Bug + data migration: a post-midnight evening checklist filed onto the new calendar day’s standup row. Fix (commit 1bf861a): new eveningStandupDate() in web/src/lib/date-utils.ts — an “evening-standup day D” spans 15:00 on D → 14:59 on D+1 (uses local getHours()); MobilePlan’s evening checklist + reflection sheets now target that row (eveningStandupId + on-demand useEnsureStandup), gated on its morning_completed_at/evening_completed_at; morning/lunch/day-plan/standup-notes unchanged (calendar-date). MobilePlan.date/eveningDate un-frozen — recompute on visibilitychange. Desktop /standup not changed (its UTC toISODateString accidentally cancels the bug for late CEST nights). Data migration moved the 4 mis-filed datapoints back. Open: not deployed yet — needs pnpm --filter web deploy. Gotcha: daily-ritual “today” must be (a) local not UTC, (b) not mount-frozen in a PWA, (c) for evening rituals, post-midnight = previous day (15:00 cutoff).
  • mobile-autosave — Debounced field-level autosave for the standup sheets
  • scheduled-reminders — Phase 3 sync_day_plan_block_reminder() trigger (split into trg_dpb_reminder_iud + trg_dpb_reminder_upd) fires “Up next” reminders 5 min before a PLANNED block’s start_time — which is a timestamptz (absolute instant), so fire_at = start_time - interval '5 min', no standup.date/tz math; reminder owner is NEW.person. Live & smoke-tested. Also: the evening-checklist reminder (process_evening_checklist_reminders pg_cron job, shipped 2026-05-11) — a 21:00/22:00-Budapest nudge to finish the Evening Checklist if < X of today’s EVENING_CHECKLIST datapoints have a non-empty value (done = filled-in answers, not evening_completed_at); CUSTOM_REMINDER notification with entity_type='standup', tap → /standup (auto-creates today’s standup).

External Integrations

  • integrations — Index of CRM external integrations (Billingo, GitHub/GitLab, Jira, Linear, Apple Reminders, Tailscale, Cloudflare, Yahoo Finance, Polymarket)
  • infra-gotchasDB plumbing ground-truth for anyone adding a migration or regenerating types: live migrations are timestamped in web/supabase/migrations/ (applied via Supabase MCP apply_migration / supabase db push); the numbered 00N_ files in supabase/migrations/ are a STALE copy (no config.toml, no CI applies them). The pmv2_*/pm_*/bsc_* as never casts seen elsewhere are because both generated type files (types.ts AND database.types.ts) must be regenerated, but gen-types only writes database.types.tstypes.ts (the one db-mappings.ts/barrel actually import) drifts unless hand-synced.
  • polymarket-fetch — External Rust pipeline writes pm_* tables in the mgmt Supabase project; CRM consumes via createTableQuery once database.types.ts is regenerated
  • paper-trading-dashboard — Read-only Paper Trading performance dashboard at /polymarket (replaced the old consensus/copy-score section). Correction: the pm_* tables live in the CRM’s single Supabase (the project internally named “mgmt”, mkofmdtdldxgmmolxxhc = VITE_SUPABASE_URL) — there is NO separate mgmt client; use useSupabase().
  • polymarket-bet-view — The Bets tab on /polymarket (all 3 sub-views complete) consumes @code’s pmv2_* tables (pmv2_positions/pmv2_event_category/pmv2_traders/pmv2_leaderboard_entries) and the pm_* self-tracking tables (pm_self_wallets/pm_runs/pm_positions) from the single CRM Supabase; replicates @code’s model.rs conviction/bet_score scoring AND the consolidate_gauge/event_merge_key/is_date_token gauge math exactly.
  • vuln-disclosure-tracker/disclosure reads an external agent’s bsc_finding table (the BSC vuln scanner at /Users/levander/coding/bsc-vuln-scanner) — first cross-agent integration, requested over cross-agent-handoff-channel; needed a for select to authenticated RLS policy + a stable backend finding_key to survive the table’s daily reload.
  • disclosure-on-demand-analysis/disclosure now also writes into @bscvuln’s public.bsc_analysis_request queue (one INSERT, auth-gated) to trigger an on-demand exploitability analysis of any BSC contract; a VM worker drains the queue (Slither-judge or open-ended Claude audit) and upserts the verdict into bsc_contract_analysis (already read by the verdict panel). Watch hook useAnalysisRequest polls every 8s while pending/running, caps at 15-min staleness, auto-invalidates the contract-analysis query on done.

Incidents & Outages

Mobile / PWA

Mobile Notifications UI

  • mobile-notifications-uiMobileNotificationButton + MobileNotificationsSheet (vaul Drawer, 85vh); MobileHeader rightSlot fallback pattern; PushDiscoveryNudge extracted DRY; bare-render test mock pattern

Notifications

  • mobile-notifications-ui — Mobile in-app list (bell + sheet); reuses NotificationItem and TAB_FILTERS from desktop; extracts PushDiscoveryNudge for sharing; handleClick got a standup/standup branch for the evening-checklist nudge
  • push-notifications — Supabase push notifications research (transports, infra, gotchas, open decisions)
  • scheduled-remindersreminder table + process_due_reminders() pg_cron job + CUSTOM_REMINDER type; reminders bypass emit_notification to dodge the event_fanout push-silence; snooze (P4) re-emits via create_reminder. Also the evening-checklist reminder — a second pg_cron SQL fn (process_evening_checklist_reminders, cron '0 * * * *', self-gating to hours 21/22 Budapest) that direct-INSERTs a CUSTOM_REMINDER notification (entity_type='standup', data.source='evening_checklist', origin='reminder', dedup source_id includes the hour) when the Evening Checklist is incomplete; does NOT use the reminder table; send-push EF v7 + deriveUrl case 'standup'
  • evening-checklist — Documents the evening-checklist reminder from the ritual side (what “done”/N/X means, why it’s filled-in-answers not evening_completed_at)
  • incident-2026-05-10-sw-cf-access-lockout — Push-notifications SW commit caused the auth outage; hard rule: no NavigationRoute in any future push SW

Polymarket / Paper Trading

  • polymarket-bet-viewBets tab — COMPLETE 2026-06-04 (4th tab on /polymarket, sub-nav Bet View | Self Bets | Trade Analyzer; merged local, push gated on operator; ~40 unit tests; no migration, read-only). Cross-agent feature requested by @code over polymarket_fetch/AGENT_HANDOFF.md; all three folded into polymarket_fetch/CRM_DASHBOARD_PROMPT.md. All share shell BetsDashboard.tsx + domain web/src/lib/pmv2-bets.ts. (1) Bet View — sortable/filterable table of the cohort’s top conviction-weighted bets, grouped by (condition_id, outcome_index), ranked by bet_score = Σ(score × conviction). Faithful port of @code’s model.rs: conviction = clamp((initial_value−avg_lost)/(avg_won−avg_lost),0,1) (0 if either avg null or avg_won≤avg_lost); bands VHIGH≥0.06/HIGH≥0.035/NEUTRAL≥0.02/LOW≥0.008/VLOW; cohort_entry stake-weighted; rank = COALESCE(rank_month,rank_week,rank_day); dust filter redeemable=false AND initial_value>1. Domain conviction/bandOf/cohortEntry/aggregateCohortBets/BAND_TONE; hook useCohortBets (latest completed run → traders+names+positions+category); UI BetViewTable/BandBadge. (2) Self Bets — operator’s OWN tracked wallets (pm_self_wallets, Balu/Bandi) → latest pm_runs.selector='self_snapshot' per wallet (max id, wallet in selector_params->>'wallet') → pm_positions (held; dust KEPT — our own book) → each annotated with cohort backing (band + # cohort traders) where (condition_id, outcome_index) matches a Bet-View cohort bet (reuses useCohortBets); grouped per wallet; empty latest snapshot → “No open positions”, NO stale fallback. Domain latestSelfRunIds/parseSelfPosition/attachCohortBacking/cohortBetKey; hook useSelfBets; UI SelfBetsView. (3) Trade Analyzer — paste a Polymarket URL/slug → normalizeSlug (sanitize to [a-z0-9-], which also closed a PostgREST .or() injection vector) → match pmv2_positions by event_slug OR slug (held non-dust) → join latest pmv2_tradersconsolidateGauge → per-outcome gauge sides. Faithful port of @code’s consolidate_gauge/event_merge_key/is_date_token: lowercase slug, split -, drop date tokens (month full-name OR 3-letter prefix; year 2024–2031; all-digit; \d+[-/]\d+), rejoin; group by (event_merge_key, outcome_index); per-side cohort_score=Σ(score×conviction); sort desc; cap MAX_GAUGE_SIDES=8 (overflow hidden_sides “+N more”); n_markets=distinct condition_id; label=modal event_slug. Domain isDateToken/eventMergeKey/normalizeSlug/consolidateGauge; hook useAnalyzerEvent; UI TradeAnalyzer. Cross-cutting gotchas: pmv2_positions is NOT run-scoped (live snapshot keyed by proxy_wallet); pmv2_traders has no names (use buildWalletNameMap over pmv2_leaderboard_entries); pmv2_event_category joins on event_slug not event_id; StatusBadge takes tone+label (tones exactly amber|emerald|rose|gray|sky); all pmv2_*/pm_* tables absent from database.types.tsas never; PostgREST numeric→string→always num/numOrNull.
  • trader-scout-dashboard — Read-only runs-history (master) + scored-trader-leaderboard (detail) dashboard, now the default /polymarket landing tab (commit e315dba, push pending; branch feat/pmv2-scout). Scrapes Polymarket public leaderboards (DAY/WEEK/MONTH/ALL), intersects wallets on ≥min_intervals boards, scores from resolved-market W/L, ranks. /polymarket is now a 3-tab shell (default Scout; ?tab=copy/?tab=longshot KEEP the prior dashboards — nothing deleted; resolveTab helper). Layers: pure web/src/lib/pmv2.ts (parsePmv2Run/parsePmv2Trader, buildWalletNameMap/mergeTraderNames, deriveRunSummary, formatScore/formatRunDuration/shortWallet/traderDisplayName, LOW_SAMPLE_N=10) → usePmv2Runs/usePmv2Traders/usePmv2RunWallets (30s poll) → scout/index.tsx master-detail (defaults to latest run) + RunsRail/RunParamsChips/ScoutSummary/LeaderboardTable. Data: 4 pmv2_* tables (runs/traders/leaderboard_entries/market_resolutions) in the single CRM Supabase, all RLS *_auth_read → read directly, NO migration; pmv2_traders PK run_id+proxy_wallet, FK→runs CASCADE, score numeric nullable, n_resolved can be 0. Lessons: score semantics drift between runs (run-1 wl_rate 0–1 vs later SIGNED ≈−0.12..+0.07; formatScore ×100 renders signed % under a “Score” header — verify per-run vs live data, the spec snapshot went stale); new tables absent from database.types.tsas never on .from()/.eq() + explicit supabaseQuery<RawX[]> generic (precedent useDayPlan/useTimesheetExport; follow-up = hand-add to database.types.ts+types.ts); PostgREST numeric→STRING (Number()); small-sample honesty (mute n<10, don’t re-rank — backend owns scoring); name enrichment via second cheap query (pmv2_traders has no names → join client-side to pmv2_leaderboard_entries map, ≈49/50, fallback shortWallet); test-mock trap — hooks build .select().order().limit() eagerly so a from:()=>({}) mock throws → mock supabaseQuery (the awaited seam) directly.
  • copy-lane-dashboard — Read-only forward-validation dashboard for the copy-trade lane, now a secondary /polymarket tab (?tab=copy; was the default landing before Scout) (commit bf66a6f, push pending). Rows = pm_paper_trades WHERE lane='copy' (paper 10 mirrors of 5 tracked Polygon wallets' cheap-outcome buys). `/polymarket` is a segmented-control tab shell (default Copy, `?tab=longshot`); old Longshot dashboard demoted to `pages/polymarket/longshot/LongshotDashboard.tsx` (excludes `lane='copy'`). Pure domain `copy-trades.ts` (`deriveCopy*`, `tStat`, `COPY_FIX_CUTOFF_MS`/`CHASE_KILL=0.017`/`BACKTEST_ROI=0.248`) → `useCopyTrades`/`useCopyConfig`/`useCopyWallets` → page + GateTracker stepper/CopyScorecards/CopyCumulativeChart/CohortTable/ExecQualityStrip. New DB: `pm_copy_config`/`pm_copy_wallets` + 5 nullable cols on `pm_paper_trades` (types hand-added to BOTH `database.types.ts` AND `types.ts`). **Rules**: P&L/win/ROI RESOLVED-only (pending never 0); win strictly >0; gates G1 n≥50 / G2 n≥150 (t≥1.5∧ROI>0∧chase≤1.7¢) / G3 n≥300 (t≥2.0∧ROI≥+8%); exec-quality is a separate POST-FIX-filtered population (3 code regimes) from P&L; window_close_ts epoch SECONDS. Hardening lessons: sd===0 guard fails on bit-identical floats (sd≈1.7e-17 → t-stat 1e16 false-pass) → relative-epsilon guard; null/0 size → ±Inf poisons t-stat NaN → filter sizedUsd>0 && isFinite first; bucketing needs out-of-range catch-all; formatters guard !isFinite→'—'; fee-% must keep resolved population consistent; UI-polish bar (real segmented control/gate stepper/distinct benchmark line/StatCard delta slot).
  • paper-trading-dashboard — The read-only Paper Trading performance dashboard that replaced the old “smart-money consensus + copy-score leaderboard” Polymarket section (~138 files deleted) at the same route /polymarket. Three layers: pure framework-free domain module web/src/lib/paper-trades.ts (parsePaperTrade Number()-coerces numeric-as-string cols, exhaustive PRICE_BANDS/PHASES buckets, derive* fns, 25 tests) TanStack hook usePaperTrades (pm_paper_trades, server-side .gte('signal_wall',…), 30s refetch) page + KpiCards/CumulativePnlChart/BreakdownTable×3/OpenPositionsTable/RecentBetsTable. Key facts: the pm_* tables live in the CRM’s single Supabase (project named “mgmt”, mkofmdtdldxgmmolxxhc = VITE_SUPABASE_URL — no separate client, use useSupabase()); flat $10 stake; OPEN ⟺ up_won IS NULL ⟺ realized_pnl_usd IS NULL; lanes data-driven (longshot main, momentum absent); out-of-config pre-tuning bets shown in muted “off-config” buckets not hidden. Gotcha: react-hooks/purity flags Date.now() in render → const [nowMs]=useState(()=>Date.now()); tsc -b stricter than --noEmit.
  • side heatmap + hourly ROI strip (2026-05-30) — 2026-05-30 extension (commit bea641a): a top-trader benchmark line on the cumulative P&L chart for wallet 0x75cc…3ce1, normalized to our flat 2,233 vs our ~+10 normalization is in app code not SQL. New pure helpers in paper-trades.ts (runningCumulative/benchmarkPnl/parseBenchmarkTrade/mergeSeries/coreStats/deriveByAssetSide/deriveHourly/formatUsdCompact), hook useTopTraderBenchmark (.limit(5000)). Reusable lessons: normalize a benchmark to your stake before comparing on a shared axis; a second chart series needs its OWN loading/error/empty states (silent-feature-failure) + decouple the chart skeleton from it; unbounded Supabase queries silently truncate at PostgREST’s row cap → .limit() (usePaperTrades still unbounded = latent); verify a new view’s RLS by simulating the role.
  • Bug fix — Self Bets hid resolved positions (2026-06-15)Self Bets bug fix, commit afd8740 (LOCAL on master, NOT pushed/deployed). Page “didn’t get all positions” — hid resolved/redeemable bets (Bandi 9 open/17 resolved, Balu 3/10; resolved all $0 settled). Two-layer root cause: (1) the external pmv2 self_snapshot scraper writes ZERO redeemable rows to pm_positions for self wallets (0 redeemable self rows vs ~49.7K cohort redeemable rows in DB), so resolved self positions never existed in the data; (2) useSelfBets.ts also .filter(p => !p.redeemable). Fix: re-point useSelfBets to fetch live from Polymarket’s public data-apiGET data-api.polymarket.com/positions?user=<wallet>&sizeThreshold=0&limit=500 per pm_self_wallets wallet. Gotchas: sizeThreshold=0 REQUIRED (default 1 hides small positions); data-api is CORS-open (access-control-allow-origin: *) → browser SPA calls direct, no proxy/edge fn; response is camelCase (proxyWallet/conditionId/outcomeIndex/redeemable/currentValue) → new parsePolymarketPosition mapper, SelfPosition gained redeemable; SelfBetsView now partitions each wallet into Open table + collapsible <details> “Resolved (N)“. Per-wallet fetch failure → []; wallet-list query error still throws to ErrorBanner. Consequence: page no longer reads pm_runs/pm_positions/latestSelfRunIds → the external self_snapshot agent now produces snapshots nobody reads (retirement candidate, operator’s call; latestSelfRunIds/parseSelfPosition remain exported but unused). Mirror of self-sync-held-only-fix on the write side. Verified pnpm --filter web test pmv2-bets SelfBets → 54 passed; build green.
  • integrations — Index entry for the Polymarket integration (its “separate mgmt project” framing is superseded by paper-trading-dashboard)
  • polymarket-fetch — The external strategy pipeline that produces the paper-trade bets (dangling/planned note)
  • crypto-lane-ops-tabCrypto lane-ops tab (5th /polymarket tab, commit 73acb90, 2026-07-08; auto-deploys on push), cloned from Copy Lane. @crm’s first babylon-coordinated cross-agent build (channel pmv2-ui #1085, from @crypto). Reads two public views from @crypto’s migration 0009_crypto_ui_views.sqlcrypto_lane_health (sorted by toxicity_gap = signal_win − fill_win, the adverse-fill signal; severity-colored) + crypto_wallets — via useCryptoLanes/useCryptoWallets (as never, numeric-string coercion, 30s poll). Writes exactly one column pmv2_wallets.size_scale via useSetWalletScale (clamp [0.5,3], AlertDialog, RLS-deny → inline “not authorized”); authoritative because PM’s size_scale hot-reload (babylon #1065) is LIVE (deploy 7cd41b0, re-reads per order). Pending: @deploy applies migration 0009 (read views); @positionmanager applies the RLS policy + column grant (write). 27/27 tests, tsc -b/vite build green.

PWA Updates

  • pwa-update-promptregisterType: 'prompt' + MobileUpdateBanner + useUpdatePrompt hook with auto-reshow on newer-update transition; vitest alias gotcha for virtual:pwa-register/react

Scheduled Reminders

  • scheduled-reminders — Phases 1-4, all shipped & applied to remote: ad-hoc one-offs (reminder table, create_reminder RPC, process_due_reminders() pg_cron), recurring rules (compute_*_fire_at SQL helpers, 6-tab dialog), day-plan-block “Up next” trigger (split trg_dpb_reminder_iud/trg_dpb_reminder_upd; start_time is timestamptz), and notification snooze (SnoozePopover, forwards entity_type/entity_id). P3 + P4 each got a 4-perspective review (DB/security/frontend/ops) — fixes shipped (ON CONFLICT re-arm guard, trigger split, reminder_update_guard BEFORE UPDATE trigger restricting authenticated-role direct UPDATEs to status-only). Plus the evening-checklist reminder (shipped 2026-05-11; process_evening_checklist_reminders + process-evening-checklist-reminders cron job) — same pattern, separate feature, does NOT touch the reminder table; revised after a 3-perspective DB/security/ops review (added the in-app-pref gate, count(DISTINCT), RAISE LOGs, 'standup' click-through). Includes the Postgres gotcha cluster (STABLE vs IMMUTABLE, %ROWTYPE cursors, day+timetimestamp, timestamptztime & TS types both as string, WHEN-clause triggers, MCP migration-version drift, UTC '0 * * * *' cron + in-fn AT TIME ZONE hour-gate for local-wall-clock jobs, count(DISTINCT) not count(*) for “how many items have an answer”, self-nudging cron jobs re-check every tick + put a discriminator in the dedup key)
  • evening-checklist — Evening-checklist reminder documented from the ritual side

Service Worker & PWA Build

  • pwa-update-prompt — Removed install→skipWaiting() (was a recovery bridge from cd8394f); kept activate→clients.claim() + SKIP_WAITING message handler for user-initiated updates
  • incident-2026-05-10-sw-cf-access-lockoutinjectManifest + web/src/sw.ts; what NOT to register; recovery via skipWaiting + clients.claim + in-app reset button

Tech Debt & Audit

  • infra-gotchasThree checked-in docs that MISLEAD (2026-06-09 validation): (1) two migration dirs, only web/supabase/migrations/ (timestamped) is live — numbered 00N_ in supabase/migrations/ is stale; (2) generated DB types duplicated in types.ts + database.types.ts, and gen-types regenerates only the latter (the one most code does NOT import); (3) root /CLAUDE.md + supabase/CLAUDE.md wrongly claim Clerk auth — it’s Cloudflare ZTNA. Trust order: web/CLAUDE.md correct, root/supabase stale.
  • tech-debt — Updated 2026-05-09 with mobile audit follow-ups (UTC drift in 2 more hooks, mount-frozen date selectors, useUpdatePrompt cleanup, stub aggregations, per-user prompts v2, forecast formula cap); 2026-05-11 DRY pass deleted the “No Query Key Constants” debt (factories now centralize keys) and shrank “Direct Supabase Calls in Components” (DayPlanner/KanbanBoard/TaskDetailSheet/CSVImportWizard/TaskCard moved off — AddLoanee/AddConfiscation/CommandPalette still bypass), and added a “Deferred from the 2026-05-11 DRY Pass” section; 2026-05-11 the “Mount-frozen date selectors” item went partly fixedMobilePlan.date/eveningDate now recompute on visibilitychange (MobileHours.active still freezes; desktop standup page still uses UTC toISODateString)
  • evening-checklist-day-boundary-fix-2026-05-11 — The fix that partly closed the mount-frozen-date-selectors debt; also surfaced the “daily-ritual today must be local, not mount-frozen, and evening = post-midnight-is-previous-day” gotcha
  • dry-refactor-2026-05-11 — The DRY pass’s own catalogue of what it deferred (edge-function _shared/, the 2 UTC-drift hooks, packages/tender-pipeline, Tier 3 god-components) — the canonical record of why each was skipped
  • incident-2026-05-10-sw-cf-access-lockout — Stale doc flagged: web/CLAUDE.md and security both still describe the anon-only Supabase model; JWT exchange via cf-access-auth was reintroduced after 769ba5c
  • scheduled-reminders — Open follow-ups (post P3/P4 review): full reminder_update_own RLS rewrite (now partially backstopped by the reminder_update_guard trigger), multi-tenant day_plan_block/standup RLS tightening (lower priority — trigger uses NEW.person), no retention job for reminder/net._http_response/cron.job_run_details, thin test coverage, configurable lead-time, custom snooze picker