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-07-08

  • Crypto lane-ops tab shipped on /polymarket (commit 73acb90 on master, auto-deploys to Cloudflare Pages on push; 27/27 vitest, tsc -b + vite build green). @crm’s first cross-agent build coordinated over babylon — channel pmv2-ui, task #1085, handed off by @crypto (not the old file channels). 5th tab on the /polymarket shell, cloned from Copy Lane. Page web/src/pages/polymarket/crypto/CryptoDashboard.tsx + LaneHealthTable/WalletScalingTable/WalletScaleControl; hooks useCryptoLanes/useCryptoWallets/useSetWalletScale (as never, numeric-string coercion, 30s poll, mirror usePmv2Runs). 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. Writes exactly one column pmv2_wallets.size_scale via useSetWalletScale (clamp [0.5,3], AlertDialog confirm, RLS-deny → inline “not authorized”). Authoritative because PM’s size_scale hot-reload (babylon #1065) is now LIVE (deploy 7cd41b0, PM re-reads per order entry — resolves the boot-load caveat in crypto-shortterm-per-wallet-size-scale-2026-07-06). Security: the write is a bare .update().eq() with NO .select() + a column-scoped grant update (size_scale) … to authenticated (no select), so pmv2_walletsenc_private_key/key_nonce stay unreadable; the CRM reads wallet data only via the crypto_wallets view. Pending upstream (not @crm): @deploy applies migration 0009 → read views go live (tab renders empty until then, gracefully); @positionmanager applies the RLS policy + column grant on pmv2_wallets.size_scale (Andras gates it) → the write works. — crypto-lane-ops-tab
  • Documented the CRM RLS write-auth convention (reusable; scoped while answering @positionmanager on the pmv2_wallets.size_scale policy). Rule: a new CRM write policy gates on the ROLE onlyto authenticated using (true) with check (…) (canonical migration 20260426100000_enforce_authenticated_rls.sql; tender_qualification follows it). The real operator gate is UPSTREAM — Cloudflare Access ZTNA: only authenticated operators ever get a Supabase token (cf-access-auth EF), so Postgres claim-checks are NOT the operator gate. auth.uid() = person.id scoping is row-ownership only (e.g. file), never the operator gate. Secret columns are protected at the grant layer (bare .update()+no .select() + column-scoped update grant + safe read-view), not by narrowing the predicate. Reconciled the older “key off auth.uid()” line in security. — crm-rls-write-auth-convention
  • CRM onboarded to the babylon coordination hub as handle crm (provisioned via /babylon:init). Method: HTTP POST https://babylon.taild4189d.ts.net/provision {"handle":"crm"}, authorized by the machine’s Tailscale identity (no pre-existing token) → returns {handle, token}; token inlined into a gitignored 0600 repo-local .mcp.json, merged alongside the existing supabase server (shadows the plugin’s global plugin:babylon:babylon, which 401s because no BABYLON_TOKEN OS env var — by design). This supersedes the crib sheet’s mint-tokensecrets/babylon.env recipe (now annotated in babylon-operator-crib-sheet). Gotcha: babylon MCP tools do NOT hot-load — available only after a Claude Code restart + approving the babylon server; session-start flow = register → catch_up → open_questions → open_tasks. Why it matters: until today @crm was NOT on babylon — it still used the file-based CRM_AGENT_HANDOFF.md / polymarket AGENT_HANDOFF.md scratchpads, which went quiet ~2026-06-04 before the fleet’s mid-June babylon cutover, so any babylon messages to @crm since mid-June were unseen and will surface on the first catch_up after restart. — crm-babylon-onboarding-2026-07-08

2026-06-15

  • Fixed /polymarket → Bets → Self Bets hiding resolved/redeemable positions (commit afd8740, LOCAL on master, NOT pushed/deployed). Page “didn’t get all positions” — only open ones showed (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 vs ~49.7K cohort redeemable rows in DB); (2) useSelfBets.ts also .filter(p => !p.redeemable). Fix: re-pointed useSelfBets to fetch live from Polymarket’s public data-api (GET data-api.polymarket.com/positions?user=<wallet>&sizeThreshold=0&limit=500 per pm_self_wallets wallet) — sizeThreshold=0 required, data-api is CORS-open so the SPA calls direct, camelCase response → new parsePolymarketPosition, SelfPosition gained redeemable, SelfBetsView now splits Open table + collapsible “Resolved (N)“. Page no longer reads the snapshot tables → self_snapshot agent is now a retirement candidate. Verified 54 tests pass + build green. Updated polymarket-bet-view (Self Bets sub-view rewritten + bug-fix section), cross-linked self-sync-held-only-fix. — polymarket-bet-view

2026-06-09

  • Documented three durable infrastructure gotchas where checked-in docs MISLEAD (found in a 2026-06-09 codebase validation; verified against git ls-tree HEAD / web/package.json / source). (1) Migrations — two dirs, one live: the live Supabase project (mkofmdtdldxgmmolxxhc) tracks timestamped web/supabase/migrations/*.sql (~79 files, applied via Supabase MCP apply_migration or supabase db push); the numbered 00N_ files in supabase/migrations/ (001010, incl. 003_tender_pipeline/010_disclosure_schema, present at HEAD) are a STALE secondary copy — there is no supabase/config.toml and no CI that applies a migrations folder. New migrations go in web/supabase/migrations/ timestamped; do NOT add new 00N_ files. Contradicts supabase/CLAUDE.md. (2) Generated DB types duplicated: packages/shared/src/types.ts and database.types.ts are both ~146K generated copies; db-mappings.ts + the @levandor/shared barrel (index.ts) import from ./types, so most code uses types.ts — yet the gen-types script (web/package.json, targets remote --project-id mkofmdtdldxgmmolxxhc) writes only database.types.ts. So the regen script updates the copy most code does NOT import → must regen/sync both or types.ts silently drifts. The supabase/CLAUDE.md --local snippet is stale on both counts. (3) Auth is CF ZTNA, not Clerk — root docs wrong: root /CLAUDE.md and supabase/CLAUDE.md:24 both say “Clerk handles authentication” — FALSE for the admin app. Real flow (per web/CLAUDE.md, correct): Cloudflare Zero Trust → CF_Authorization cookie → cf-access-auth edge fn verifies the CF JWT and mints a Supabase token (sub = person.id) → RLS keys off auth.uid()/auth.jwt()->>'email' via get_my_person_id(); RLS tables use authenticated-role USING(true) (auth enforced upstream at the edge) + service_role FOR ALL. Established by 20260331…_auth_cfztna_migration.sql / 20260401010000_ztna_open_all_rls.sql / 20260426100000_enforce_authenticated_rls.sql. Clerk only exists in userspace/. Wrote infra-gotchas; cross-linked + corrected the migrations/types rows and the gen-types command in agent-context-crm; added a Related pointer in tech-debt. See infra-gotchas.

2026-06-04

  • On-demand “Analyze this contract” shipped on /disclosure — CRM’s SECOND write feature, and first write to a @bscvuln-owned table (merged locally on master, push gated on operator, commits aea6492..5a689bd; superpowers spec→plan→subagent-TDD; plan docs/superpowers/plans/2026-06-04-disclosure-on-demand-analysis.md). Cross-agent feature requested by @bscvuln over CRM_AGENT_HANDOFF.md. What it does: operator can request an exploitability analysis of ANY BSC contract (not just today’s bsc_finding batch) → CRM inserts a pending row into @bscvuln’s queue public.bsc_analysis_request → a VM worker drains it (Slither-judge if findings exist, else open-ended Claude source audit) → on done upserts the verdict into bsc_contract_analysis (already read by the verdict panel) → the watch hook auto-invalidates the contract-analysis query and the panel refetches + lights up. The only CRM write is one INSERT {contract_address (normalized lowercase), chain:'bsc'} — auth-gated, user-initiated; the worker owns all subsequent status transitions via service_role. Queue table public.bsc_analysis_request (@bscvuln-owned, RLS verified live): cols id/created_at/chain(‘bsc’)/contract_address/status(‘pending’→running→done|error)/error/requested_by/analyzed_at/claimed_at; policies bsc_analysis_request_auth_read (SELECT authenticated) + bsc_analysis_request_auth_insert (INSERT authenticated), NO update for authenticated. Two entry points: a new Analyze view with a paste-address box (validates 0x+40hex before queuing) + an Analyze/Re-analyze button & status chip in the verdict panel of any contract’s detail sheet. Layers (Page→hook→Supabase): domain web/src/lib/disclosure.ts (parseAnalysisRequest/normalizeContractAddress/isValidContractAddress/analysisRequestTone/analysisRequestLabel); hooks web/src/lib/hooks/disclosure/useAnalysisRequests.tsuseRequestAnalysis (insert) + useAnalysisRequest(address) (polls latest request every 8s while pending/running, stops on done/error AND caps at 15-min staleness, auto-invalidates contract-analysis on done); UI AnalyzeContractBox + AnalyzeControl (in DisclosureDetailSheet). Async UX (“queued — check back”), never a blocking spinner. Hardening (silent-failure review): (1) watch-query errors surface inline — a failed poll never masquerades as “pending”; (2) an unknown status fails loud (→error, stops the poll) rather than a forever-”Queued”; (3) the “queued” confirmation only renders after the insert actually succeeds; (4) raw worker error text truncated with a title attr. Status: worker timer isn’t live yet (pending @bscvuln’s @deploy), so a queued request currently sits pending; asked @bscvuln to seed a done row to verify the end-to-end refetch. bsc_analysis_request absent from database.types.tsas never (disclosure/pmv2 precedent). See disclosure-on-demand-analysis and vuln-disclosure-tracker.
  • Polymarket Bets tab — COMPLETE: all 3 sub-views shipped (Bet View · Self Bets · Trade Analyzer; merged to master locally, push gated on operator; ~40 unit tests across the three views, tsc clean; each port reviewed = faithful to @code’s Rust; no migration, read-only). Cross-agent feature requested by @code (owner of the polymarket-fetch Rust pipeline) over polymarket_fetch/AGENT_HANDOFF.md; all three folded into polymarket_fetch/CRM_DASHBOARD_PROMPT.md per @code. The Bets tab is the 4th tab on /polymarket with an internal sub-nav Bet View | Self Bets | Trade Analyzer, all sharing the shell web/src/pages/polymarket/bets/BetsDashboard.tsx + domain module web/src/lib/pmv2-bets.ts. (1) Bet View (already logged below) — cohort’s top conviction-weighted bets. (2) Self Bets — the 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 since it’s our own book) → each position 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-run 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() filter-injection vector) → match pmv2_positions by event_slug OR slug (held non-dust) → join latest pmv2_tradersconsolidateGauge → per-outcome gauge sides. The gauge is @code’s exact consolidate_gauge/event_merge_key/is_date_token Rust ported to TS: lowercase slug, split on -, drop date tokens (month full-name OR 3-letter prefix so may/jun drop; year 2024–2031 inclusive; all-digit token; \d+[-/]\d+ digit-group), rejoin; group sides 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: all pmv2_*/pm_* tables absent from database.types.tsas never; PostgREST numeric→string→always coerce via num/numOrNull; trader names from pmv2_leaderboard_entries (not pmv2_traders). Plans docs/superpowers/plans/2026-06-04-polymarket-{bet-view,self-bets,trade-analyzer}.md; spec docs/superpowers/specs/2026-06-04-polymarket-bets-tab-design.md. See polymarket-bet-view (now the canonical Bets-tab note covering all three).
  • Polymarket Bets tab — Bet View shipped (sub-view 1 of 3) (merged to master locally across 8 commits b833d59..9d8c037, push pending operator go; superpowers brainstorm→spec→plan→subagent-TDD→verification; 21 unit tests, tsc clean, code-reviewed = faithful port). Cross-agent feature requested by @code (owner of the polymarket-fetch Rust pipeline) over the file channel polymarket_fetch/AGENT_HANDOFF.md (a peer of CRM_AGENT_HANDOFF.md). What it does: new Bets tab on /polymarket whose first sub-view is a sortable/filterable table of the trader cohort’s top conviction-weighted bets, grouped by (condition_id, outcome_index) and ranked by bet_score = Σ(score × conviction). Scoring math is a faithful port of @code’s Rust model.rs (treat as a cross-agent contract): conviction = clamp((initial_value − avg_lost_bet_size)/(avg_won_bet_size − avg_lost_bet_size),0,1) (0 when either avg null or avg_won ≤ avg_lost); bet_score = Σ holders(score × conviction); bands VHIGH≥0.06 / HIGH≥0.035 / NEUTRAL≥0.02 / LOW≥0.008 / else VLOW; cohort_entry = Σ(avg_price·initial_value)/Σ(initial_value); top holder = max score×conviction; rank = COALESCE(rank_month,rank_week,rank_day); dust filter = redeemable=false AND initial_value>1. Layers (repo-standard Page→hook→Supabase): pure domain web/src/lib/pmv2-bets.ts (conviction/bandOf/cohortEntry/aggregateCohortBets/BAND_TONE/formatters) → hook web/src/lib/hooks/pmv2-bets/useCohortBets.ts (reads latest completed pmv2_runspmv2_traders + pmv2_leaderboard_entries names + pmv2_positions + pmv2_event_category, all as never, 30s poll) → UI web/src/pages/polymarket/bets/ (BetsDashboard, BetViewTable+filters, BandBadge; test BetViewTable.test.tsx). Same single CRM Supabase (mkofmdtdldxgmmolxxhc “mgmt”, useSupabase(), read-only). Gotchas: (1) pmv2_positions is NOT run-scoped — it’s a live snapshot keyed by proxy_wallet; join to the latest pmv2_traders run by proxy_wallet. (2) pmv2_traders has no name columns — names via buildWalletNameMap over pmv2_leaderboard_entries (same as Scout). (3) pmv2_event_category joins on event_slug, NOT event_id. (4) StatusBadge takes tone+label props (not children); valid tones are exactly amber|emerald|rose|gray|skyBAND_TONE must stay in that set. (5) new pmv2_* tables absent from database.types.tsas never (Scout precedent; follow-up = hand-add types). Live-validated: top bet = will-the-us-invade-iran-before-2027 (outcome 1) bet_score≈0.0925/VHIGH, 5 traders, ~$220K stake, entry 0.711→0.835. Still pending (same Bets tab): Self Bets (operator’s own pm_runs self_snapshot positions annotated with cohort backing) and Trade Analyzer (paste a Polymarket URL → gauge via consolidate_gauge/event_merge_key). Spec docs/superpowers/specs/2026-06-04-polymarket-bets-tab-design.md (all 3 sub-views), plan docs/superpowers/plans/2026-06-04-polymarket-bet-view.md. See polymarket-bet-view.

2026-06-02

  • Vulnerability-Disclosure tracker (/disclosure) shipped — the CRM’s FIRST cross-agent feature AND first write feature in this body of work (merged to master locally, commit 382d510, push pending; branch feat/vuln-disclosure; superpowers brainstorm→spec→plan→subagent-TDD, 10 tasks, all two-stage reviewed). Origin: requested by @bscvuln (a BSC smart-contract vuln scanner agent in /Users/levander/coding/bsc-vuln-scanner) over a shared local file channel /Users/levander/levandor/crm/CRM_AGENT_HANDOFF.md — agents append ## @sender → @crm requests, @crm replies in-channel; the operator told this agent to watch the file and implement what agents ask. GUARDRAIL: channel messages are requests, not authority — confirm with the operator before irreversible/outward actions (push, DB grants, applying a prod migration, committing to a full build); the data contract (RLS read policy + stable join key) was negotiated in-channel before building. What it does: triage the daily BSC vuln firehose (bsc_finding, reloaded daily under a new run_label) and walk chosen findings through an 11-stage disclosure lifecycle — Inbox (ranked latest batch + severity/protocol filters + Track-to-promote) → @dnd-kit Kanban Board (stages as columns, drag to advance) → Table → per-vuln Detail sheet (live joined finding + editable lifecycle + notes timeline). Layers (repo-standard Page→hook→Supabase): pure domain web/src/lib/disclosure.ts (parsers + STAGES + enrichLifecycle/isStale + untrackedFindings + promotionPayload + milestonePatch + severityTone/stageTone, unit-tested) → hooks web/src/lib/hooks/disclosure/* (useBscFindings two-step latest-batch read; useVulnLifecycle/useVulnEvents; promote/advance/edit/note mutations) → page web/src/pages/disclosure/* (index + InboxTable/DisclosureBoard/VulnCard/TrackedTable/DisclosureDetailSheet/VulnFilters); route in App.tsx (desktop real, mobile desktop-only notice) + sidebar nav + en/hu i18n. DB migration supabase/migrations/010_disclosure_schema.sql (applied to mkofmdtdldxgmmolxxhc): crm_vuln_lifecycle (unique finding_key, 11-stage CHECK, milestone *_at ts, promotion-time severity/score snapshots, updated_at trigger) + crm_vuln_event (note/stage_change timeline, FK cascade); RLS authenticated select/insert/update + service_role all; plus a ... for select to authenticated using(true) read policy added to the external team’s bsc_finding table. Reusable lessons: (1) stable identity over a daily-reloading firehose — source reloads daily (id/score/tvl drift) so lifecycles key on a backend-generated finding_key = md5(chain:contract:name:detector:normalized-snippet), deliberately EXCLUDING line number (churns on re-verification); isStale = key absent from latest batch (“disappeared, maybe fixed”); snapshot volatile severity/score at promotion time. (2) reading another team’s RLS table — CRM auth role is authenticated (CF Access→Supabase JWT, no service key), so an external table needs a for select to authenticated using(true) policy (service-role-only = invisible to the browser app); same pattern as pm_*/pmv2_*. (3) @dnd-kit board reuse — existing KanbanBoard is task-coupled, so build a PARALLEL board reusing the @dnd-kit pattern (DndContext/useSortable/useDroppable, column-<id> droppable ids, handleDragEnd target resolution) rather than genericizing; PointerSensor activationConstraint distance:5 lets card-click + drag coexist. (4) first write feature — direct useMutation (promote = insert lifecycle + initial event together; advance applies milestonePatch setting the *_at once-on-entry, never overwriting; invalidate keys; sonner toasts); untyped tables → as never on .from()/.eq()/payload (pmv2 precedent; follow-up = regen database.types.ts). (5) applying a prod migration mid-build — orchestrator applied it via Supabase MCP apply_migration (NOT delegated) after explicit operator OK, committed the numbered SQL for source control, verified tables/policies + authenticated read before building hooks. If you touch this next: regen database.types.ts(+types.ts) to drop the as never casts; the score column on bsc_finding ranks by severity×confidence×tvl (Inbox ranking signal); ask @bscvuln for a latest-batch view if volume grows. See vuln-disclosure-tracker and cross-agent-handoff-channel.
  • Trader-Scout (pmv2) dashboard shipped as the new /polymarket LANDING tab (merged to master locally, commit e315dba, push pending; branch feat/pmv2-scout; superpowers brainstorm→spec→plan→subagent-TDD→verification). Third replacement of the polymarket landing this week (Paper-Trading → Copy-Lane → Scout). Read-only runs-history (master) + scored-trader-leaderboard (detail) over a backend “scout” pipeline that scrapes Polymarket public leaderboards (DAY/WEEK/MONTH/ALL), intersects wallets appearing on ≥min_intervals boards, scores them from resolved-market W/L, and ranks them. Nothing deleted/polymarket is now a 3-tab shell (pages/polymarket/index.tsx, default Scout, ?tab=copy / ?tab=longshot deep-links; resolveTab helper); Copy-Lane + Longshot KEPT as secondary tabs. Layers: pure domain web/src/lib/pmv2.ts (parsePmv2Run/parsePmv2Trader, buildWalletNameMap/mergeTraderNames, deriveRunSummary, formatScore/formatRunDuration/shortWallet/traderDisplayName, const LOW_SAMPLE_N=10) → hooks usePmv2Runs/usePmv2Traders/usePmv2RunWallets (30s poll; keys pmv2Runs/pmv2Traders/pmv2RunWallets) → page scout/index.tsx (master-detail, defaults to latest run) + components RunsRail/RunParamsChips/ScoutSummary/LeaderboardTable. Data: 4 pmv2_* tables in the CRM’s single Supabase (mkofmdtdldxgmmolxxhc, “mgmt”; useSupabase()), all RLS-on with *_auth_read → app reads directly, NO migration. pmv2_runs (run_id bigserial PK; started/completed_at, status running/completed/failed, params jsonb {top_n,intervals,market_scope,score_method,min_intervals}, n_leaderboard_entries, n_intersected, error_message); pmv2_traders (PK run_id+proxy_wallet, FK→runs CASCADE; rank_day/week/month/all nullable, lt/st_strength smallint, tier core/proven/emerging, wins/losses/n_resolved [can be 0], score numeric nullable, rank_in_run); pmv2_leaderboard_entries (PK run_id+interval+proxy_wallet; rank/pnl/vol/user_name/x_username — read ONLY for name enrichment); pmv2_market_resolutions (upstream, not read). Gotchas / lessons: (1) score semantics drift between runs — run 1 score_method='wl_rate' (0–1 win rate) but later runs have SIGNED scores (≈−0.12..+0.07); formatScore does ×100 → renders signed percents like -11.5% under a “Score” header (correct display, mislabels if not a win rate) → verify metric semantics against ALL runs in live data, the spec’s run-1 assumption went stale by merge. (2) new tables absent from database.types.ts → hooks use as never on .from()/.eq(value), row typing preserved via explicit supabaseQuery<RawX[]> generic (repo precedent: useDayPlan/useTimesheetExport); cleaner follow-up = hand-add the 4 tables to database.types.ts + types.ts. (3) PostgREST numeric (incl. score) → STRING, always Number() (num/numOrNull); n_resolved can be 0 → guard winRate /0. (4) small-sample honesty (reused from copy-lane) — surface n_resolved, visually MUTE rows with n<LOW_SAMPLE_N=10 rather than re-ranking; backend owns scoring/rank. (5) name enrichment via a second cheap querypmv2_traders lacks names, join client-side to a wallet→{user_name,x_username} map from pmv2_leaderboard_entries (≈49/50 named, fallback shortWallet). (6) test-mock trap — hooks build .select().order().limit() eagerly before awaiting the query seam, so a from:()=>({}) mock throws → mock supabaseQuery (the single awaited seam) directly, or use a self-returning chainable builder stub. If you touch this next: confirm score/score_method semantics per-run before trusting the “Score” header; hand-add pmv2_* to database.types.ts+types.ts to drop the as never casts; extend resolveTab (don’t replace the default) when adding a 4th tab. Scout spec/plan live under docs/superpowers/{specs,plans}/. See trader-scout-dashboard.

2026-06-01

  • Copy-Trade Lane dashboard shipped as the new /polymarket landing (merged to master locally, commit bf66a6f, push pending; branch feat/copy-lane-dashboard; superpowers brainstorm→spec→plan→subagent-TDD→adversarial-verification). Read-only forward-validation view over a Rust service that mirrors 5 tracked Polygon traders’ cheap-outcome buys as paper 10 copies — rows are `pm_paper_trades WHERE lane='copy'`. `/polymarket` is now a **segmented-control tab shell** (`pages/polymarket/index.tsx`, default **Copy**, `?tab=longshot` deep-link); the old Paper-Trading/Longshot dashboard moved to `pages/polymarket/longshot/LongshotDashboard.tsx` and now **excludes `lane='copy'`**. Layers: pure domain `web/src/lib/copy-trades.ts` (`parseCopyTrade`, `deriveCopyKpis/Gates/Cumulative/ByBand/ByCohort/ExecQuality`, `tStat`, consts `COPY_FIX_CUTOFF_MS`/`CHASE_KILL=0.017`/`BACKTEST_ROI=0.248`) → hooks `useCopyTrades` (`.eq('lane','copy')`, `.limit(5000)` DESC, 30s poll) + `useCopyConfig`/`useCopyWallets` → page `copy/index.tsx` + components (CopyScorecards, GateTracker stepper, CopyCumulativeChart, CopyBandTable, CohortTable, RecentCopiesTable, ExecQualityStrip, CopyConfigPanel) + extracted shared `components/Pill.tsx`. **New DB**: `pm_copy_config` (singleton id=1: enabled/cap_price/min_price/size_usd/min_window_left_secs/max_daily_notional_usd), `pm_copy_wallets`, +5 nullable cols on `pm_paper_trades` (chase_spread/cohort_price/cohort_wallet/detect_latency_ms/fee_usd); types hand-added to BOTH `packages/shared/src/database.types.ts` AND `types.ts`. **Domain rules**: P&L/win-rate/ROI are RESOLVED-only (`realized_pnl_usd NOT NULL`); pending is a separate count NEVER 0; net ROI%=Σresolved pnl/Σresolved sized×100; win=realized>0 strictly. Gates G1 n≥50 (n-progress+win% only, no fabricated cohort), G2 n≥150 (t≥1.5∧ROI>0∧avgChase≤1.7¢), G3 n≥300 (t≥2.0∧ROI≥+8%); t=mean(perTradeROI)/(sampleStd/√n). Exec-quality is POST-FIX filtered (created_at≥2026-05-31T15:00:00Z) + excludes detect_latency_ms=0 sentinel + always shows coverage% (latency/chase span 3 code-version regimes → all-row aggregates meaningless); P&L uses the FULL resolved set. window_close_ts is epoch SECONDS (×1000 for JS); PostgREST numeric→STRING (always Number()). Hardening lessons (adversarial pass): (1) sd===0 guard never fires for bit-identical floats (sd≈1.7e-17 → t-stat ~1e16 FALSELY passes gates) → relative-epsilon sd<=1e-12*max(1,|mean|); (2) realized/sized with null/0 size → ±Infinity poisons mean/std → t-stat NaN → filter sizedUsd>0 && Number.isFinite before the stat; (3) bucketing needs an out-of-range catch-all or below-floor/null/negative rows silently vanish; (4) shared formatters guard !Number.isFinite→'—'; (5) fee-%-of-resolved-notional must use RESOLVED fees in numerator (mixing populations overstated ~0.7pp); (6) UI-polish bar — real segmented control (role=tablist/tab+aria-selected), real gate stepper (icons+progress bars+status badges), distinct-color backtest reference line with actual−expected edge-gap tooltip, compound scorecard data in StatCard delta/subtitle slot. Spec docs/superpowers/specs/2026-05-31-copy-lane-dashboard-design.md (+ UI design spec). See copy-lane-dashboard.

2026-05-30

  • Paper Trading dashboard extended: top-trader benchmark line + asset×side heatmap + hourly ROI strip (merged to master, commit bea641a). (1) Top-trader benchmark on the cumulative P&L chart — a red line for wallet 0x75cc3b63a2f2423085e10706c78b494017b93ce1, normalized to our flat 2,233 normalized vs our ~+10-normalized pnl (sizing lives in app code, not SQL). New pure helpers in web/src/lib/paper-trades.ts: runningCumulative (extracted from deriveCumulative), benchmarkPnl/parseBenchmarkTrade, mergeSeries (carry-forward both series across the union timeline, shared 0-origin → “gap” tooltip), coreStats (extracted, shared by summarize+deriveByAssetSide), deriveByAssetSide, deriveHourly, formatUsdCompact; new hook useTopTraderBenchmark(range) (30s poll, .limit(5000), drops non-finite ts); CumulativePnlChart refactored to numeric/time x-axis with two lines + distinct benchmark legend states. Lessons: (a) normalize a benchmark to your stake before comparing on a shared axis (~7× mismatch otherwise makes the chart lie); (b) silent-feature-failure — a second chart series must surface its OWN loading/error/empty (distinct legend chips unavailable/loading/@$10/no-trades) + decouple the chart skeleton from it, else a broken benchmark looks identical to “no data” and silently disappears; (c) unbounded Supabase queries can silently truncate a cumulative line at PostgREST’s row cap → add .limit() (usePaperTrades still unbounded = latent follow-up); (d) verify a new view’s RLS by simulating the role (set local role authenticated; select count(*)) — empty-vs-RLS-filtered is invisible from the client. Process: brainstorm → spec (validated live: no join fan-out, payout a clean 0/1 flag) → plan → subagent TDD (two-stage review) → 4-perspective verification (caught the silent-failure + row-cap + Y-axis-clip) → fix → merge. Spec/plan in docs/superpowers/{specs,plans}/2026-05-30-top-trader-benchmark-asset-side*. See side heatmap + hourly ROI strip (2026-05-30).
  • Polymarket section replaced with a read-only Paper Trading performance dashboard (merged FF to master, commit 9670ebd; 2026-05-29 to 30). The old “smart-money consensus + copy-score leaderboard” section (consensus/traders/markets/my-bets/runs/playground pages, ~25 hooks, RunScope/Selector contexts, polymarket-scope, reading pm_runs/pm_trader_metrics) was fully deleted (~138 files) and replaced at the same route /polymarket (sidebar still “Polymarket”, icon TrendingUp). New three-layer architecture: pure framework-free domain module web/src/lib/paper-trades.ts (parsePaperTrade Number()-coerces numeric-as-string columns, asset=market.split('-')[0], isOpen/isWin/slippageRatio; formatters; applyFilters/rangeStartMs taking injected nowMs; deriveKpis/deriveCumulative/deriveByLane/deriveByBand/deriveByPhase; exported exhaustive PRICE_BANDS/PHASES; 25 unit tests) TanStack Query hook web/src/lib/hooks/polymarket/usePaperTrades.ts (pm_paper_trades, server-side .gte('signal_wall', ...) range, refetchInterval: 30_000) page web/src/pages/polymarket/index.tsx + components (KpiCards x6, CumulativePnlChart recharts, 3x BreakdownTable lane/band/phase, OpenPositionsTable, RecentBetsTable, PaperTradeFilters URL-synced via useUrlListState, Side). Key correction: the Supabase project internally named “mgmt” (mkofmdtdldxgmmolxxhc) IS the CRM’s single Supabase (VITE_SUPABASE_URL) — all pm_* tables live there, use useSupabase(), there is NO separate mgmt client (supersedes the earlier “separate mgmt project” framing in integrations/polymarket-fetch). Data semantics (verified live): pm_paper_trades 23 cols (brief subset + 4 extras signal_ts_ns/up_mid_at_signal/open_btc/final_btc), flat $10 stake, numeric STRINGS so always Number(), invariant OPEN <=> up_won IS NULL <=> realized_pnl_usd IS NULL (0 violations); live strategies longshot(main) + tiny btc_spillover, momentum absent lanes data-driven not hardcoded. Gotchas: out-of-config data (98 resolved bets below the 5c floor, 39 above the 0.93 time cap; time_in_window 0.09-0.9967, entry 0.01-0.57) buckets made EXHAUSTIVE with muted “off-config” catch-alls (price <0.05/0.05-0.07/0.07-0.10/>=0.10, phase <0.66/0.66-0.80/0.80-0.93/>=0.93), shown not hidden; react-hooks/purity flags Date.now() in useMemo/render read once via const [nowMs]=useState(()=>Date.now()); tsc -b is stricter than --noEmit and CI-relevant; pre-existing baseline reds on master NOT from this work (sw.ts:69 waitUntil/Event, MobileHours.test.tsx, ~115 unrelated lint errors). Built via superpowers brainstormspecplansubagent TDD with two-stage review; 21 commits; spec docs/superpowers/specs/2026-05-29-paper-trading-dashboard-design.md, plan docs/superpowers/plans/2026-05-29-paper-trading-dashboard.md. See paper-trading-dashboard.

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.