For Agents
Reverse-chronological session log. Newest entries at top, grouped by date (
## YYYY-MM-DD). Each bullet: one piece of work, short summary, wikilinks to docs touched. Updated byobsidian-documenteron every project doc write. Read byhistorianat bootstrap (top ~15 entries).
2026-07-08
- Crypto lane-ops tab shipped on
/polymarket(commit73acb90onmaster, auto-deploys to Cloudflare Pages on push; 27/27 vitest,tsc -b+vite buildgreen).@crm’s first cross-agent build coordinated over babylon — channelpmv2-ui, task #1085, handed off by @crypto (not the old file channels). 5th tab on the/polymarketshell, cloned from Copy Lane. Pageweb/src/pages/polymarket/crypto/CryptoDashboard.tsx+LaneHealthTable/WalletScalingTable/WalletScaleControl; hooksuseCryptoLanes/useCryptoWallets/useSetWalletScale(as never, numeric-string coercion, 30s poll, mirrorusePmv2Runs). Reads twopublicviews from @crypto’s migration0009_crypto_ui_views.sql—crypto_lane_health(sorted bytoxicity_gap = signal_win − fill_win, the adverse-fill signal; severity-colored) +crypto_wallets. Writes exactly one columnpmv2_wallets.size_scaleviauseSetWalletScale(clamp[0.5,3], AlertDialog confirm, RLS-deny → inline “not authorized”). Authoritative because PM’ssize_scalehot-reload (babylon #1065) is now LIVE (deploy7cd41b0, 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-scopedgrant update (size_scale) … to authenticated(no select), sopmv2_wallets’enc_private_key/key_noncestay unreadable; the CRM reads wallet data only via thecrypto_walletsview. 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 onpmv2_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_scalepolicy). Rule: a new CRM write policy gates on the ROLE only —to authenticated using (true) with check (…)(canonical migration20260426100000_enforce_authenticated_rls.sql;tender_qualificationfollows it). The real operator gate is UPSTREAM — Cloudflare Access ZTNA: only authenticated operators ever get a Supabase token (cf-access-authEF), so Postgres claim-checks are NOT the operator gate.auth.uid() = person.idscoping 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 offauth.uid()” line in security. — crm-rls-write-auth-convention - CRM onboarded to the babylon coordination hub as handle
crm(provisioned via/babylon:init). Method: HTTPPOST 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 existingsupabaseserver (shadows the plugin’s globalplugin:babylon:babylon, which 401s because noBABYLON_TOKENOS env var — by design). This supersedes the crib sheet’smint-token→secrets/babylon.envrecipe (now annotated in babylon-operator-crib-sheet). Gotcha: babylon MCP tools do NOT hot-load — available only after a Claude Code restart + approving thebabylonserver; session-start flow = register → catch_up → open_questions → open_tasks. Why it matters: until today@crmwas NOT on babylon — it still used the file-basedCRM_AGENT_HANDOFF.md/ polymarketAGENT_HANDOFF.mdscratchpads, which went quiet ~2026-06-04 before the fleet’s mid-June babylon cutover, so any babylon messages to@crmsince mid-June were unseen and will surface on the firstcatch_upafter restart. — crm-babylon-onboarding-2026-07-08
2026-06-15
- Fixed
/polymarket→ Bets → Self Bets hiding resolved/redeemable positions (commitafd8740, 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 pmv2self_snapshotscraper writes ZERO redeemable rows topm_positionsfor self wallets (0 vs ~49.7K cohort redeemable rows in DB); (2)useSelfBets.tsalso.filter(p => !p.redeemable). Fix: re-pointeduseSelfBetsto fetch live from Polymarket’s public data-api (GET data-api.polymarket.com/positions?user=<wallet>&sizeThreshold=0&limit=500perpm_self_walletswallet) —sizeThreshold=0required, data-api is CORS-open so the SPA calls direct, camelCase response → newparsePolymarketPosition,SelfPositiongainedredeemable,SelfBetsViewnow splits Open table + collapsible “Resolved (N)“. Page no longer reads the snapshot tables →self_snapshotagent 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 timestampedweb/supabase/migrations/*.sql(~79 files, applied via Supabase MCPapply_migrationorsupabase db push); the numbered00N_files insupabase/migrations/(001…010, incl.003_tender_pipeline/010_disclosure_schema, present at HEAD) are a STALE secondary copy — there is nosupabase/config.tomland no CI that applies a migrations folder. New migrations go inweb/supabase/migrations/timestamped; do NOT add new00N_files. Contradictssupabase/CLAUDE.md. (2) Generated DB types duplicated:packages/shared/src/types.tsanddatabase.types.tsare both ~146K generated copies;db-mappings.ts+ the@levandor/sharedbarrel (index.ts) import from./types, so most code usestypes.ts— yet thegen-typesscript (web/package.json, targets remote--project-id mkofmdtdldxgmmolxxhc) writes onlydatabase.types.ts. So the regen script updates the copy most code does NOT import → must regen/sync both ortypes.tssilently drifts. Thesupabase/CLAUDE.md--localsnippet is stale on both counts. (3) Auth is CF ZTNA, not Clerk — root docs wrong: root/CLAUDE.mdandsupabase/CLAUDE.md:24both say “Clerk handles authentication” — FALSE for the admin app. Real flow (perweb/CLAUDE.md, correct): Cloudflare Zero Trust →CF_Authorizationcookie →cf-access-authedge fn verifies the CF JWT and mints a Supabase token (sub = person.id) → RLS keys offauth.uid()/auth.jwt()->>'email'viaget_my_person_id(); RLS tables useauthenticated-roleUSING(true)(auth enforced upstream at the edge) +service_role FOR ALL. Established by20260331…_auth_cfztna_migration.sql/20260401010000_ztna_open_all_rls.sql/20260426100000_enforce_authenticated_rls.sql. Clerk only exists inuserspace/. Wrote infra-gotchas; cross-linked + corrected the migrations/types rows and thegen-typescommand 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 onmaster, push gated on operator, commitsaea6492..5a689bd; superpowers spec→plan→subagent-TDD; plandocs/superpowers/plans/2026-06-04-disclosure-on-demand-analysis.md). Cross-agent feature requested by@bscvulnover CRM_AGENT_HANDOFF.md. What it does: operator can request an exploitability analysis of ANY BSC contract (not just today’sbsc_findingbatch) → CRM inserts apendingrow into@bscvuln’s queuepublic.bsc_analysis_request→ a VM worker drains it (Slither-judge if findings exist, else open-ended Claude source audit) → on done upserts the verdict intobsc_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 subsequentstatustransitions viaservice_role. Queue tablepublic.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; policiesbsc_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): domainweb/src/lib/disclosure.ts(parseAnalysisRequest/normalizeContractAddress/isValidContractAddress/analysisRequestTone/analysisRequestLabel); hooksweb/src/lib/hooks/disclosure/useAnalysisRequests.ts—useRequestAnalysis(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 ondone); UIAnalyzeContractBox+AnalyzeControl(inDisclosureDetailSheet). 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 atitleattr. Status: worker timer isn’t live yet (pending@bscvuln’s@deploy), so a queued request currently sitspending; asked@bscvulnto seed adonerow to verify the end-to-end refetch.bsc_analysis_requestabsent fromdatabase.types.ts→as 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
masterlocally, push gated on operator; ~40 unit tests across the three views,tscclean; 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) overpolymarket_fetch/AGENT_HANDOFF.md; all three folded intopolymarket_fetch/CRM_DASHBOARD_PROMPT.mdper @code. The Bets tab is the 4th tab on/polymarketwith an internal sub-nav Bet View | Self Bets | Trade Analyzer, all sharing the shellweb/src/pages/polymarket/bets/BetsDashboard.tsx+ domain moduleweb/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) → latestpm_runs.selector='self_snapshot'per wallet (maxid, wallet inselector_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 (reusesuseCohortBets); grouped per wallet; empty latest snapshot → “No open positions”, NO stale-run fallback. DomainlatestSelfRunIds/parseSelfPosition/attachCohortBacking/cohortBetKey; hookuseSelfBets; UISelfBetsView. (3) Trade Analyzer — paste a Polymarket URL/slug →normalizeSlug(sanitize to[a-z0-9-], which also closed a PostgREST.or()filter-injection vector) → matchpmv2_positionsbyevent_slugORslug(held non-dust) → join latestpmv2_traders→consolidateGauge→ per-outcome gauge sides. The gauge is @code’s exactconsolidate_gauge/event_merge_key/is_date_tokenRust ported to TS: lowercase slug, split on-, drop date tokens (month full-name OR 3-letter prefix somay/jundrop; year 2024–2031 inclusive; all-digit token;\d+[-/]\d+digit-group), rejoin; group sides by(event_merge_key, outcome_index); per-sidecohort_score = Σ(score×conviction); sort desc; capMAX_GAUGE_SIDES=8(overflow →hidden_sides“+N more”);n_markets= distinctcondition_id; label = modalevent_slug. DomainisDateToken/eventMergeKey/normalizeSlug/consolidateGauge; hookuseAnalyzerEvent; UITradeAnalyzer. Cross-cutting: allpmv2_*/pm_*tables absent fromdatabase.types.ts→as never; PostgREST numeric→string→always coerce vianum/numOrNull; trader names frompmv2_leaderboard_entries(notpmv2_traders). Plansdocs/superpowers/plans/2026-06-04-polymarket-{bet-view,self-bets,trade-analyzer}.md; specdocs/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
masterlocally across 8 commitsb833d59..9d8c037, push pending operator go; superpowers brainstorm→spec→plan→subagent-TDD→verification; 21 unit tests,tscclean, code-reviewed = faithful port). Cross-agent feature requested by @code (owner of the polymarket-fetch Rust pipeline) over the file channelpolymarket_fetch/AGENT_HANDOFF.md(a peer of CRM_AGENT_HANDOFF.md). What it does: new Bets tab on/polymarketwhose 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 bybet_score = Σ(score × conviction). Scoring math is a faithful port of @code’s Rustmodel.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 oravg_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 = maxscore×conviction; rank =COALESCE(rank_month,rank_week,rank_day); dust filter =redeemable=false AND initial_value>1. Layers (repo-standard Page→hook→Supabase): pure domainweb/src/lib/pmv2-bets.ts(conviction/bandOf/cohortEntry/aggregateCohortBets/BAND_TONE/formatters) → hookweb/src/lib/hooks/pmv2-bets/useCohortBets.ts(reads latest completedpmv2_runs→pmv2_traders+pmv2_leaderboard_entriesnames +pmv2_positions+pmv2_event_category, allas never, 30s poll) → UIweb/src/pages/polymarket/bets/(BetsDashboard,BetViewTable+filters,BandBadge; testBetViewTable.test.tsx). Same single CRM Supabase (mkofmdtdldxgmmolxxhc“mgmt”,useSupabase(), read-only). Gotchas: (1)pmv2_positionsis NOT run-scoped — it’s a live snapshot keyed byproxy_wallet; join to the latestpmv2_tradersrun byproxy_wallet. (2)pmv2_tradershas no name columns — names viabuildWalletNameMapoverpmv2_leaderboard_entries(same as Scout). (3)pmv2_event_categoryjoins onevent_slug, NOTevent_id. (4)StatusBadgetakestone+labelprops (not children); valid tones are exactlyamber|emerald|rose|gray|sky—BAND_TONEmust stay in that set. (5) newpmv2_*tables absent fromdatabase.types.ts→as 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 ownpm_runsself_snapshotpositions annotated with cohort backing) and Trade Analyzer (paste a Polymarket URL → gauge viaconsolidate_gauge/event_merge_key). Specdocs/superpowers/specs/2026-06-04-polymarket-bets-tab-design.md(all 3 sub-views), plandocs/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 tomasterlocally, commit382d510, push pending; branchfeat/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 → @crmrequests,@crmreplies 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 newrun_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 domainweb/src/lib/disclosure.ts(parsers +STAGES+enrichLifecycle/isStale+untrackedFindings+promotionPayload+milestonePatch+severityTone/stageTone, unit-tested) → hooksweb/src/lib/hooks/disclosure/*(useBscFindingstwo-step latest-batch read;useVulnLifecycle/useVulnEvents; promote/advance/edit/note mutations) → pageweb/src/pages/disclosure/*(index+InboxTable/DisclosureBoard/VulnCard/TrackedTable/DisclosureDetailSheet/VulnFilters); route inApp.tsx(desktop real, mobile desktop-only notice) + sidebar nav + en/hu i18n. DB migrationsupabase/migrations/010_disclosure_schema.sql(applied tomkofmdtdldxgmmolxxhc):crm_vuln_lifecycle(uniquefinding_key, 11-stage CHECK, milestone*_atts, promotion-time severity/score snapshots,updated_attrigger) +crm_vuln_event(note/stage_change timeline, FK cascade); RLSauthenticatedselect/insert/update +service_roleall; plus a... for select to authenticated using(true)read policy added to the external team’sbsc_findingtable. Reusable lessons: (1) stable identity over a daily-reloading firehose — source reloads daily (id/score/tvl drift) so lifecycles key on a backend-generatedfinding_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 isauthenticated(CF Access→Supabase JWT, no service key), so an external table needs afor select to authenticated using(true)policy (service-role-only = invisible to the browser app); same pattern aspm_*/pmv2_*. (3) @dnd-kit board reuse — existingKanbanBoardis task-coupled, so build a PARALLEL board reusing the @dnd-kit pattern (DndContext/useSortable/useDroppable,column-<id>droppable ids,handleDragEndtarget resolution) rather than genericizing;PointerSensoractivationConstraint distance:5lets card-click + drag coexist. (4) first write feature — directuseMutation(promote = insert lifecycle + initial event together; advance appliesmilestonePatchsetting the*_atonce-on-entry, never overwriting; invalidate keys; sonner toasts); untyped tables →as neveron.from()/.eq()/payload (pmv2 precedent; follow-up = regendatabase.types.ts). (5) applying a prod migration mid-build — orchestrator applied it via Supabase MCPapply_migration(NOT delegated) after explicit operator OK, committed the numbered SQL for source control, verified tables/policies +authenticatedread before building hooks. If you touch this next: regendatabase.types.ts(+types.ts) to drop theas nevercasts; thescorecolumn onbsc_findingranks by severity×confidence×tvl (Inbox ranking signal); ask@bscvulnfor a latest-batch view if volume grows. See vuln-disclosure-tracker and cross-agent-handoff-channel. - Trader-Scout (pmv2) dashboard shipped as the new
/polymarketLANDING tab (merged tomasterlocally, commite315dba, push pending; branchfeat/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_intervalsboards, scores them from resolved-market W/L, and ranks them. Nothing deleted —/polymarketis now a 3-tab shell (pages/polymarket/index.tsx, default Scout,?tab=copy/?tab=longshotdeep-links;resolveTabhelper); Copy-Lane + Longshot KEPT as secondary tabs. Layers: pure domainweb/src/lib/pmv2.ts(parsePmv2Run/parsePmv2Trader,buildWalletNameMap/mergeTraderNames,deriveRunSummary,formatScore/formatRunDuration/shortWallet/traderDisplayName, constLOW_SAMPLE_N=10) → hooksusePmv2Runs/usePmv2Traders/usePmv2RunWallets(30s poll; keyspmv2Runs/pmv2Traders/pmv2RunWallets) → pagescout/index.tsx(master-detail, defaults to latest run) + componentsRunsRail/RunParamsChips/ScoutSummary/LeaderboardTable. Data: 4pmv2_*tables in the CRM’s single Supabase (mkofmdtdldxgmmolxxhc, “mgmt”;useSupabase()), all RLS-on with*_auth_read→ app reads directly, NO migration.pmv2_runs(run_idbigserial PK; started/completed_at, status running/completed/failed,paramsjsonb {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],scorenumeric 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)scoresemantics drift between runs — run 1score_method='wl_rate'(0–1 win rate) but later runs have SIGNED scores (≈−0.12..+0.07);formatScoredoes×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 fromdatabase.types.ts→ hooks useas neveron.from()/.eq(value), row typing preserved via explicitsupabaseQuery<RawX[]>generic (repo precedent:useDayPlan/useTimesheetExport); cleaner follow-up = hand-add the 4 tables todatabase.types.ts+types.ts. (3) PostgREST numeric (incl.score) → STRING, alwaysNumber()(num/numOrNull);n_resolvedcan be 0 → guardwinRate/0. (4) small-sample honesty (reused from copy-lane) — surfacen_resolved, visually MUTE rows with n<LOW_SAMPLE_N=10rather than re-ranking; backend owns scoring/rank. (5) name enrichment via a second cheap query —pmv2_traderslacks names, join client-side to a wallet→{user_name,x_username} map frompmv2_leaderboard_entries(≈49/50 named, fallbackshortWallet). (6) test-mock trap — hooks build.select().order().limit()eagerly before awaiting the query seam, so afrom:()=>({})mock throws → mocksupabaseQuery(the single awaited seam) directly, or use a self-returning chainable builder stub. If you touch this next: confirmscore/score_methodsemantics per-run before trusting the “Score” header; hand-addpmv2_*todatabase.types.ts+types.tsto drop theas nevercasts; extendresolveTab(don’t replace the default) when adding a 4th tab. Scout spec/plan live underdocs/superpowers/{specs,plans}/. See trader-scout-dashboard.
2026-06-01
- Copy-Trade Lane dashboard shipped as the new
/polymarketlanding (merged tomasterlocally, commitbf66a6f, push pending; branchfeat/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) + excludesdetect_latency_ms=0sentinel + always shows coverage% (latency/chase span 3 code-version regimes → all-row aggregates meaningless); P&L uses the FULL resolved set.window_close_tsis epoch SECONDS (×1000 for JS); PostgREST numeric→STRING (always Number()). Hardening lessons (adversarial pass): (1)sd===0guard never fires for bit-identical floats (sd≈1.7e-17 → t-stat ~1e16 FALSELY passes gates) → relative-epsilonsd<=1e-12*max(1,|mean|); (2)realized/sizedwith null/0 size → ±Infinity poisons mean/std → t-stat NaN → filtersizedUsd>0 && Number.isFinitebefore 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. Specdocs/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, commitbea641a). (1) Top-trader benchmark on the cumulative P&L chart — a red line for wallet0x75cc3b63a2f2423085e10706c78b494017b93ce1, normalized to our flat 2,233 normalized vs our ~+10-normalized pnl (sizing lives in app code, not SQL). New pure helpers inweb/src/lib/paper-trades.ts:runningCumulative(extracted fromderiveCumulative),benchmarkPnl/parseBenchmarkTrade,mergeSeries(carry-forward both series across the union timeline, shared 0-origin → “gap” tooltip),coreStats(extracted, shared bysummarize+deriveByAssetSide),deriveByAssetSide,deriveHourly,formatUsdCompact; new hookuseTopTraderBenchmark(range)(30s poll,.limit(5000), drops non-finite ts);CumulativePnlChartrefactored 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()(usePaperTradesstill 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 indocs/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, commit9670ebd; 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/Selectorcontexts,polymarket-scope, readingpm_runs/pm_trader_metrics) was fully deleted (~138 files) and replaced at the same route/polymarket(sidebar still “Polymarket”, iconTrendingUp). New three-layer architecture: pure framework-free domain moduleweb/src/lib/paper-trades.ts(parsePaperTradeNumber()-coerces numeric-as-string columns,asset=market.split('-')[0],isOpen/isWin/slippageRatio; formatters;applyFilters/rangeStartMstaking injectednowMs;deriveKpis/deriveCumulative/deriveByLane/deriveByBand/deriveByPhase; exported exhaustivePRICE_BANDS/PHASES; 25 unit tests) → TanStack Query hookweb/src/lib/hooks/polymarket/usePaperTrades.ts(pm_paper_trades, server-side.gte('signal_wall', ...)range,refetchInterval: 30_000) → pageweb/src/pages/polymarket/index.tsx+ components (KpiCards x6, CumulativePnlChart recharts, 3x BreakdownTable lane/band/phase, OpenPositionsTable, RecentBetsTable, PaperTradeFilters URL-synced viauseUrlListState, Side). Key correction: the Supabase project internally named “mgmt” (mkofmdtdldxgmmolxxhc) IS the CRM’s single Supabase (VITE_SUPABASE_URL) — allpm_*tables live there, useuseSupabase(), there is NO separate mgmt client (supersedes the earlier “separate mgmt project” framing in integrations/polymarket-fetch). Data semantics (verified live):pm_paper_trades23 cols (brief subset + 4 extrassignal_ts_ns/up_mid_at_signal/open_btc/final_btc), flat $10 stake,numeric→ STRINGS so alwaysNumber(), invariantOPEN <=> up_won IS NULL <=> realized_pnl_usd IS NULL(0 violations); live strategieslongshot(main) + tinybtc_spillover,momentumabsent → 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_window0.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/purityflagsDate.now()inuseMemo/render → read once viaconst [nowMs]=useState(()=>Date.now());tsc -bis stricter than--noEmitand CI-relevant; pre-existing baseline reds onmasterNOT from this work (sw.ts:69waitUntil/Event,MobileHours.test.tsx, ~115 unrelated lint errors). Built via superpowers brainstorm→spec→plan→subagent TDD with two-stage review; 21 commits; specdocs/superpowers/specs/2026-05-29-paper-trading-dashboard-design.md, plandocs/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 themgmtSupabase project; CRM consumes viacreateTableQueryoncedatabase.types.tsis regenerated. Full overview at polymarket-fetch.
2026-05-11
- Bug fix + data migration: evening checklist filing onto the wrong day. Symptom: on mobile, “today’s” evening-checklist inputs showed last night’s answers pre-filled. Root cause:
web/src/mobile/plan/MobilePlan.tsxcomputed the standup “day” astoLocalDateString(new Date())(raw local calendar date), mount-frozen viauseMemo(…, []), and passed that onestandupIdto every sheet including the evening checklist + evening reflection. The user did their May-10 evening checklist at ~00:20 CEST on May 11 (procrastinated past midnight, tapped the new evening-checklist reminder push) → app created the May-11standuprow and saved the 3EVENING_CHECKLISTdatapoints (+ theBOTH-phase Energy datapoint) onto it; then on the evening of May 11, opening “today’s” (= May 11’s) checklist showed those answers. No data lost (standuphasUNIQUE(person, date),standup_datapointhasUNIQUE(standup, config)— just attached to the wrong day). Fix (commit1bf861a,fix(standup): evening checklist/reflection day starts at 15:00 local): neweveningStandupDate(now = new Date()): stringinweb/src/lib/date-utils.ts—const EVENING_STANDUP_DAY_START_HOUR = 15;now.getHours() < 15→ yesterday’stoLocalDateString, else today’s (so an “evening-standup day D” spans 15:00 on D → 14:59 on D+1; usesgetHours()= local deliberately).MobilePlan.tsx: the evening checklist + evening reflection sheets now targetuseStandup(person, eveningStandupDate())’s row (eveningStandupId), with a second on-demanduseEnsureStandup(eveningDate)(guarded byeveningDate !== date); the “Evening” nudge button is gated on that row’smorning_completed_at/evening_completed_at; morning ritual / lunch checkin / day plan blocks / standup notes are unchanged — still calendar-date (standupId);date/eveningDateare no longer mount-frozen — they recompute ondocument.visibilitychange(long-lived PWA doesn’t go stale across midnight/15:00). Addedweb/src/lib/__tests__/date-utils.test.ts(4 cases). Desktop/standuppage (pages/standup/index.tsx) NOT changed — has a date picker, computes “today” viatoISODateString(new Date())(UTC), which by accident means a late-CEST-night session (00:00–02:00 CEST = still previous day in UTC) lands on the previous day’s row anyway (minor pre-existing nit: it uses UTCtodayStrnot local — could drift by a day near midnight UTC ≈ 1–2 am CEST; not fixed). Data migration (one-shot, applied to prod 2026-05-11 ~22:xx CEST):UPDATE standup_datapoint SET standup = '<May-10 standup id>' WHERE standup = '<May-11 standup id>'— moved the 4 datapoints (proud_of,not_proud_of,standup.energy,overspent_today) from András Léderer’s May-11 standup row to his May-10 one (May-10 row had 0 datapoints, noUNIQUE(standup, config)conflict); May-11 row now empty. Action item open: fix is committed + pushed but not deployed — needspnpm --filter web deploy(wrangler) for the mobile PWA to pick it up; until then a post-midnight evening-checklist session would still mis-file (tonight-before-midnight is fine — deployed code targets May 11’s now-empty row). Gotcha for future sessions: “today’s date” for daily-ritual features (standup morning/evening) needs care — (a) compute from local time, not UTC (toLocalDateString, nottoISODateString— the desktop standup page gets this subtly wrong but gets lucky); (b) don’t mount-freeze it (useMemo(…, [])) in a PWA — recompute onvisibilitychange; (c) an evening ritual specifically should treat post-midnight sessions as the previous day — hence the 15:00 cutoff (eveningStandupDate); a morning ritual does not want this. The mobileMobilePlan“mount-frozen date selector deliberately not fixed” item in Mount-frozen date selectors is now partially fixed for this screen (MobileHours.activestill freezes). See evening-checklist-day-boundary-fix-2026-05-11. - DRY / simplification refactor pass shipped — 9 commits on
master(d035235..ccf6f57, on top of8fbfda1), 148 files changed, ~9,359 lines deleted / ~4,332 added (≈5k net removed). The Tier 0+1+2 cut (Tier 3 god-component decomposition explicitly out of scope — the cut line). Gated per-phase ontsc -bclean +vitest(one pre-existingMobileHours.test.tsxfailure, no new) + green build. P0 — dead code: deletedweb/src/lib/database.types.ts(4141-line byte-identical orphan ofpackages/shared/src/database.types.ts), 4 never-wired-uppackages/uicomponents (animated-list/bento-grid/chart/text-animate), the OLD deadweb/src/components/crm/PageHeader.tsx(a NEWPageHeaderlands in P2a),useReorderDayPlanBlocks+test,useDeleteOnCallSchedule,userspace/src/lib/hooks/useClientUser.ts, andweb/supabase/functions/notify/(legacy notify EF, superseded byevent → fn_event_to_notifications → emit_notification). CAVEAT for the user: confirm no Supabase-dashboard Database Webhook still points atnotifybefore removing it server-side (only deleted from the repo here). P1a — hook factories: extendedweb/src/lib/hooks/createTableQuery.ts(addedcreateMaybeSingleQuery/createSingleQuery/createTableQueryWithArgs/createMaybeSingleQueryWithArgs) +createMutation.ts(multi-key/function-forminvalidateKeys,returningfor.select().single(), newcreateRpcMutation); converted ~50 hand-rolled CRUD/read hooks (budget/on-call/tasks/inventory/project-members/datapoints/khr/standup/automations/rss/timesheet/vendor); hooks with transforms/key-remaps/upserts/functions.invoke/side-effects deliberately left hand-rolled. P1b — dialogs: extendedCreateEntityDialoginto the standard form-dialog shell (submitLabel/pendingLabel/maxWidth/extraFooterLeft/showPendingSpinner/trigger; laterdescription: ReactNode+ additivecontentClassName); migrated 10 dialogs (AddTransaction/AddLoanee/AddConfiscation/AddPayback/CreateAccount/Envelope/Transfer/LinkBillingo/CreateInvoice);MappingRuleDialogNOT migrated —CreateEntityDialogwraps children in a<form>andMappingActionBuilder’s Add/remove buttons lacktype="button"→ would submit; mixed@tanstack/react-formvs localuseStateleft intentional (latter need component-body cross-field reactivity react-form’s render-prop model doesn’t give cheaply —EnvelopeDialog’s dynamic description,AddTransactionDialog’s conditional fields). P1c — small dedupes:downloadBlob→csv-export.ts(deleted byte-identicaldownloadTimesheetCsv),getMonthRange→date-utils.ts, userspacestatusColor/statusLabel→newuserspace/src/lib/project-status.ts, ~16 inline.toISOString().slice(0,10)→toISODateString(), dropped privatetoDateStr/toISODateStringcopies fortoLocalDateString,CSVImportWizard/MappingRuleDialogerror banners→MUTATION_ERROR. P2a — list-page primitives: newweb/src/components/crm/{PageHeader,ExportCsvButton,CreateButton,ErrorBanner,CardSkeleton(+SkeletonLines),ResourceTable}.tsx+web/src/lib/hooks/useUrlListState.ts(all tested); rolled out acrossprojects/partners/contracts/people(headers+buttons+error) andtasks/invoices/inventory(buttons+error; their headers are structural variants);tasks/index.tsxURL-param boilerplate→useUrlListState;dashboard StatCardSkeleton+automationsrunner skeleton→SkeletonLines(other bespoke skeletons left — would change loading visuals); side effect: 3 create buttons gaineddata-create-action(thecshortcut). P2b — project-detail tabs:useInvoices/useContracts/useInventory/useTasksnow take an optionalprojectIdarg (createTableQueryWithArgs) → pushes the project filter to Supabase (.eq('project'|'for_project', projectId)) instead of fetching whole tables + client-filtering; no-arg behaviour unchanged; new prefixed query keysprojectInvoices/projectContracts/projectInventory/projectTasks/projectFiles(so existing['invoices']etc. invalidations still prefix-match);InvoicesTab/ContractsTab/InventoryTab→ResourceTable,TasksTab/FilesTab→scoped hooks (their rendering isn’t a plain table);TimesheetsTableft over-fetching (footer row +useTimesheetsout of scope);FilesTablist→newuseProjectFiles,OverviewTab.toggleOnCall→useUpdateProject(both off directuseSupabase());InventoryTabswitched from name-match to FK-id-match filtering (more correct — a behavior change). P2c — shared hook logic +useSupabase()bypasses:on-call-rates.tsgotbuildRateMaps/entryHours/entryRate(the${person}_${project}rate maps + standby/activated/multipliers ternary were copy-pasted across 5 dashboard/on-call hooks) + new tests for those ANDresolveOnCallRate(was untested financial code);makeStorageAttachmentHooks(useInvoicePdf/useContractPdfwere the same module twice, 95/87→18/18 lines);foreign-resource.ts+ForeignResourceRowlifted tomatcher.ts(copy-pasted interface + create-FR-then-insert-mappings/delete plumbing shared by 3 integration-mapping hooks;useStatusMappings.createMappingleft inline — single-object vs array;useStorageProjectAssignmentscreate-branch later wired to the helper);TaskChips.tsx+getTaskCardModeldedupe TaskCard/KanbanCard chip rendering — fixed TaskCard’s emptyAvatarFallbackbug (now shows initials), TaskCard priority pill now uses@/lib/colors(matches Kanban dots — MEDIUM amber not copper, HIGH red-500 not thestatus-unpaidtoken — deliberate palette unification, flagged for the user);DayPlanner/KanbanBoard/TaskDetailSheet/CSVImportWizardmoved off directuseSupabase()— newuseCreateTaskFromBlock,useBudgetTransactionLookups,KanbanBoard/TaskDetailSheetreuseuseToggleTaskStatus(FilesTabstill usesuseSupabase()for one imperativecreateSignedUrldownload — doesn’t fit a query hook). P2d — colors typed: stringly-typedgetStatusBorderColor/getStatusTextColor(domain: string, status: string)→ typedgetProject/InvoiceStatusBorder/TextColor(status: ProjectStatus|InvoiceStatus)(exhaustiveness now compile-checked); dropped deadtextMaps.tenderPipeline/tenderStatus. simplify-review + cleanup: 3-lens review (reuse/quality/efficiency) — verdict clean, no behavior bugs, server-side project filter confirmed a real win; 9 small follow-ups (mixed label sizes, stray comments, missedErrorBanner/SkeletonLinesadoptions, no-opsetSearchParamsshort-circuit, unused prop, lost bold name restored,tableBuilder/callRpcextracted toweb/src/lib/hooks/supabase-builders.ts). Deferred (logged for future sessions): edge-function_shared/consolidation of the ~6 functions hand-rollingcreateClient/OPTIONS-preflight/auth-checks (touches deployed functions incl.service-healthwhich is in the app shell/sidebar, can’t be verified/deployed from a refactor pass, small win after excludingcf-access-auth/send-push— needs its own reviewed+deployed change); theuseDayPlanSuggestions.ts/useOnCallMonthly.tsUTC-drift bugs (need a deliberate UTC-vs-local decision, not a mechanical dedupe — desktop fix elsewhere wastoLocalDateString);packages/tender-pipeline(unshipped — nothing imports it,tender-syncEF is an empty stub, 2 tests fail on TED v3 / EKR API drift; candidate for own-repo or deletion — untouched per instruction not to delete the spec/package); Tier 3 god-component decomposition (CSVImportWizard1497 lines,BudgetOverview, the god pagesinvoices/settings/tasks/system-integrations/dashboard);billingo-sync-statuseshardcoded 1.27 HUF VAT multiplier applied to ALL currencies; non-atomic client-side multi-stepuseCreateInvoice/useCreateBudgetTransfer(should be RPCs —complete_block_with_draft_v1/dashboard_summary_v1are precedents);web/src/mobile/lib/formatTime.tsuses local TZ where day-plan blocks (stored1970-01-01THH:MM:00Z) need UTC; possibly-dead Linear-pushweb/supabase/functions/sync-outbound/(user to confirm — might be hit by an external scheduler). New shared abstractions a future session should reuse, not re-roll:web/src/components/crm/→PageHeader/ExportCsvButton/CreateButton/ErrorBanner/CardSkeleton+SkeletonLines/ResourceTable(+Column<T>)/TaskChips(severalTask*Chipexports)/getTaskCardModel+useTaskCardModel/CreateEntityDialog(now the standard form-dialog shell — see its props; wraps children in a<form>, nested buttons needtype="button");web/src/lib/hooks/→createTableQueryWithArgs/createMaybeSingleQuery*/createRpcMutation/makeStorageAttachmentHooks/useUrlListState/useCreateTaskFromBlock/useBudgetTransactionLookups/useProjectFiles/supabase-builders.ts(tableBuilder/callRpc);web/src/lib/→on-call-rates.ts(buildRateMaps/entryHours/entryRate),foreign-resource.ts(createForeignResourceWithMappings/deleteForeignResource),matcher.tsnow exportsForeignResourceRow/IntegrationMappingRow,csv-export.tsdownloadBlob,colors.tstyped status-color fns;userspace/src/lib/project-status.ts. See dry-refactor-2026-05-11. - Evening Checklist Reminder shipped. A timed nudge to finish the daily Evening Checklist — same family as the scheduled-reminders system but a separate feature (does NOT touch the
remindertable /process_due_reminders/ the day-plan-block trigger). Newprocess_evening_checklist_reminders()SECURITY DEFINERSQL fn called by a new pg_cron jobprocess-evening-checklist-reminderson'0 * * * *'(every hour, UTC); the fn self-gates toextract(hour from now() AT TIME ZONE 'Europe/Budapest') IN (21, 22)— DST-proof, 22/24 ticks are a one-statementRETURN. When it acts: X = count of activestandup_datapoint_configrows withphase='EVENING_CHECKLIST'(currently 4;standup_datapoint_configis global, nopersoncol → X is the same for everyone); loopsSELECT DISTINCT person FROM standup WHERE date >= today-30; per person N =count(DISTINCT config)of that person’sstandup_datapointrows for today’s standup whose config is an activeEVENING_CHECKLISTconfig and whosevalueis non-empty.N >= X→ skip(done,RAISE LOG). Else check(person, CUSTOM_REMINDER, in_app)notification_preference(fallbacknotification_type_def.default_in_app) — disabled → skip(in_app disabled) (parity withprocess_due_reminders: disabled-in-app = no row AND no push). Else direct-INSERTanotification(type='CUSTOM_REMINDER',title='Evening checklist',body='N/X done — wrap up your day',entity_type='standup',entity_id=today’s standup id or NULL,data={source:'evening_checklist', source_id:'evening_checklist:<YYYY-MM-DD>:<hour>', origin:'reminder', n, x}) withON CONFLICT (recipient, type, (data->>'source_id')) WHERE data ? 'source_id' DO NOTHING(the existingnotification_dedup_uidx), thenRAISE LOG ... -> insert; per-personBEGIN/EXCEPTION WHEN OTHERS+RAISE WARNING. Delivery: thenotificationINSERT fires the existingsend_push_on_notificationAFTER INSERT trigger → pg_net →send-pushEF;origin='reminder'escapes the shadow-mode push silence; push tag falls back tonotification:<id>so the 21:00 and 22:00 pushes don’t collapse into one OS notification; the 22:00 tick re-nudges only if still incomplete (re-checksN<X, andsource_idembeds the hour so it’s a distinct deduped row). Click-through:send-push’sderiveUrlgotcase 'standup': return '/standup'(EF v7,verify_jwtstays false), andhandleClickin bothNotificationPopover.tsxandMobileNotificationsSheet.tsxgot anelse if (notification.entity_type === 'standup') navigate('/standup')branch (+ newNotificationPopover.test.tsx) — tapping the nudge opens/standup, which auto-creates today’s standup row if needed. Hardcoded: nudge hours 21:00/22:00 Europe/Budapest (user-configurable hour out of scope — needs a settings row). Key design choice: “done” =N >= X(filled-in answers), NOTstandup.evening_completed_at IS NOT NULL— the checklist fields autosave on input and there’s no separate “complete” button, soevening_completed_atisn’t a reliable “done” signal; revisit if a real complete affordance is added. Latent multi-tenant note: the fn is SECURITY DEFINER andstandupRLS isWITH CHECK (true), so in a future multi-user world A could insert astanduprow for B → B gets a harmless fixed-content nudge (tracked for a future multi-tenant pass — tightenstandupRLS toperson = auth.uid()). Snooze quirk: it’stype='CUSTOM_REMINDER'so Phase-4 long-press snooze works on it; a snoozed copy re-fires next morning copying the literal “3/4 done” body against tomorrow’s fresh 0/4 — harmless, not special-cased. New reusable gotchas captured: (10) for a “fire at a local wall-clock hour” pg_cron job, schedule'0 * * * *'UTC + gate inside the fn onextract(hour from now() AT TIME ZONE '<zone>')(DST-proof; cron schedules are UTC); (11) usecount(DISTINCT config)notcount(*)when “progress” = “how many distinct items have an answer” (duplicate rows would spuriously cross the threshold); (12) a self-nudging cron job should re-check completion every tick and put a discriminator (the hour) in the dedupsource_idso successive ticks are distinct rows. Specdocs/superpowers/specs/2026-05-11-evening-checklist-reminder-design.md(revised after a 3-perspective DB/security/ops review — the review added the in-app-pref gate, thecount(DISTINCT), theRAISE LOGs, the'standup'click-through), plandocs/superpowers/plans/2026-05-11-evening-checklist-reminder.md. Commits on master:cf6e947(migration),ed5a34d(send-pushEF v7),8fbfda1(click handlers + test). See Evening-Checklist Reminder and Evening-Checklist Reminder (timed nudge). - Scheduled reminders — P2.5/P3/P3+P4-review migrations applied; Phase 3 now live + smoke-tested. All three pending migrations are now applied to remote:
20260512000400_reminder_p2_5_hardening.sql,20260513000100_day_plan_block_reminder_trigger.sql(Phase 3 trigger),20260513000200_reminder_update_guard.sql(reminderBEFORE UPDATE guard — when role isauthenticated, direct UPDATEs are restricted tostatus-only;process_due_remindersrunning as the cron/owner role is unaffected). Phase 3 schema correction:day_plan_block.start_timeis atimestamp with time zone(absolute instant), NOT atimeas an earlier review/spec-draft assumed — so the day-plan-block reminder’sfire_atis juststart_time - interval '5 minutes'(nostandup.datejoin, noAT TIME ZONEmath), and the reminder’s owner comes fromNEW.person(the block’s own RLS-pinned column) not a standup-owner lookup — which fully resolves the cross-person-injection concern the P3 review raised (no SECURITY-DEFINER-amplified read). Gotcha worth keeping: a trigger that didstandup.date + start_timewould have been adate + timestamptzruntime type error, caught only by a live INSERT during smoke-testing sincedatabase.types.tstypesstart_timeasstringeither way. P3 + P4 each got a 4-perspective review (DB/security/frontend/ops); all critical+important findings fixed and shipped. Key fixes: (C1) the day-plan-block trigger’sON CONFLICT DO UPDATEonly resetsstatus='pending'/fired_at=NULLwhenfire_atactually changed AND the newfire_at > now()— editing a block doesn’t re-arm an already-fired reminder; (C2) the singleAFTER INSERT/UPDATE/DELETEtrigger was split intotrg_dpb_reminder_iud(INSERT/DELETE, always) +trg_dpb_reminder_upd(UPDATE,WHENstart_time/status/titlechanged) so the day-planner’s frequent reminder-irrelevant UPDATEs don’t churn; the snooze hook now forwards the original notification’sentity_type/entity_idso a snoozedTASK_ASSIGNEDkeeps its click-through target when re-fired; snooze long-press 350ms→500ms + haptic + press feedback; thereminder_update_guardtrigger (above). Updated open follow-ups: dropped “apply the pending migrations”; kept multi-tenantday_plan_block/standupRLS tightening (now much less urgent — trigger usesNEW.personnot the standup owner),reminder_update_ownRLS rewrite (now partially addressed by the new guard trigger, which closes the direct-PostgREST path), retention/cleanup job, test coverage gap, configurable lead time, custom snooze picker. See scheduled-reminders. - Scheduled reminders (Phases 1-4) shipped. 4-phase reminder system on top of the centralized notification stack. P1 — ad-hoc one-offs:
public.remindertable (RLS scoped toactor_person_id(), no INSERT policy — insert only viacreate_reminderSECURITY DEFINER RPC),process_due_reminders()SQL fn run every minute via pg_cron,/reminderspage +MobileReminders+ReminderCreateDialog+ sidebar/More entries. Reminders bypassemit_notificationon purpose — they direct-INSERT anotificationrow withdata->>'origin'='reminder'becauseemit_notificationhardcodesorigin='event_fanout'which the send-push trigger silences during shadow-mode rollout.CUSTOM_REMINDERregistered innotification_type_def(default_push=true+ vestigialrecipient_selfresolver). P1.5 — fixes:recipient_selfSECDEF→INVOKER,create_remindervalidatesp_source_notification_idownership,process_due_remindersgotFOR UPDATE SKIP LOCKED+RAISE WARNINGhandler + idempotentcron.schedule;send-pushEF v6 falls back tonotification_type_def.default_pushwhen no pref row (the bug: missing row was treated as disabled, so push only fired for user-toggled types) + returns HTTP 500 on real errors (was 200, masking failures) +notification:<id>tag fallback; frontend i18n + loading/error UI + smarter mobile back (navigate(-1)→/more); toast[object Object]fix (Supabase errors are plain objects — pull.message/.details/.hint/.code). P2 — recurring:compute_first_fire_at(recur_rule,tz)+compute_next_fire_at(reminder)SQL helpers (presets daily/weekdays/weekly-day-set/monthly-day-of-month/hourly-every-N), scheduler advancesfire_atinstead of marking fired, 6-tab dialog (Once/Daily/Weekdays/Weekly/Monthly/Hourly) with select-based HH:MM TimePicker + day-of-week chips + nativetype="date"+ Today/Tomorrow/Next-week chips. Scheduler cursor must bepublic.reminder%ROWTYPEnotrecord(elsecannot cast type record to reminder, 42846). P2.5 — fixes (migration20260512000400committed, NOT applied to remote — Supabase MCP expired mid-session):compute_*STABLEnotIMMUTABLE(they callnow()),SET search_path='', monthly clampsday_of_monthviaLEAST(dom, EXTRACT(DAY FROM end-of-month))(was overflowing Feb/30-day months and drifting permanently),RAISE LOGon terminated recurrence; frontend:<Tabs>was wrongly wrapping the always-visible title/body inputs (invalid Radix nesting, broke kbd nav/autoFocus) — restructured to wrap onlyTabsList+TabsContent,DialogContentgotmax-h-[90dvh] overflow-y-auto(6-tab layout clipped Save/Cancel), number inputs → string state, TimePickerrole="group"+aria-labels. P3 — day-plan-block reminders (migration20260513000100committed, NOT applied — zero effect yet):sync_day_plan_block_reminder()SECDEF trigger onday_plan_blockAFTER INSERT/UPDATE/DELETE — PLANNED block withstart_timeupserts a reminder at(standup.date + start_time - interval '5 min') AT TIME ZONE 'Europe/Budapest'(title'Up next: <block>', keyed onreminder_dpb_uidxpartial unique index); DONE/CANCELLED/SKIPPED/NULL-start/delete → cancels. Spec schema was wrong → corrected: column ispersonnotperson_id,start_timeis a TIME, date lives on linkedstandup.date, no tz column (hardcoded Budapest as single-tenant shortcut), status values PLANNED/DONE/CANCELLED/SKIPPED. P4 — snooze: long-press notification on mobile (350ms) or hover Snooze chip on desktop →SnoozePopover(4 presets: 1h/3h/Tomorrow-9am/Next-week, fire time computed client-side in browser tz) →useSnoozeNotification→create_reminderwithsource='snooze',source_notification_id;process_due_remindersre-emits as freshCUSTOM_REMINDER(title/body copied, but losesentity_type/entity_id→ no click-through).SnoozePopovershared by desktopNotificationPopover+ mobileMobileNotificationsSheet. Commits: P1 ~5de4cbc..a390815+5018344+2130a06+c60d2b8, P25d4bd47/4e413eb/6e87c08, P2.56757811(migration, pending)+fa1ea6b(frontend), P3cd67841(migration, pending), P4664a63f. Open follow-ups: apply the 2 pending migrations when MCP re-authorized (P3 has no effect till then); P3+P4 multi-perspective review in progress;reminder_update_ownRLS too permissive (deferred); no retention job forreminder/net._http_response/cron.job_run_details; thin test coverage; configurable lead time + tz; custom snooze picker; snooze click-through target. Specdocs/superpowers/specs/2026-05-10-scheduled-reminders-design.md, plandocs/superpowers/plans/2026-05-10-scheduled-reminders.md. See scheduled-reminders. - Debugging-log entries added for the scheduled-reminders session:
actor_person_id()was reading the legacy singularrequest.jwt.claim.subGUC that Supabase no longer sets — fixed to tryauth.uid()first then pluralrequest.jwt.claims;send-pushtreated a missing notification preference row as disabled — fixed to fall back tonotification_type_def.default_push; toast[object Object](Supabase errors aren’tErrorinstances); plus the Postgres gotcha cluster —IMMUTABLE-vs-now()is a correctness lie (useSTABLE), PL/pgSQL cursor must be%ROWTYPEnotrecord(42846),day+timeyieldstimestampnottimestamptz(needAT TIME ZONE), Supabase MCPapply_migrationassigns its own version so don’t chasesupabase db pushparity. See debugging-log-crm.
2026-05-10
- PWA update prompt (mobile bottom banner) shipped. Replaced silent
autoUpdate+ sonner toast (invisible on iOS standalone PWAs) with explicitregisterType: 'prompt'flow + copper-accented bottom banner above BottomTabs. Removedinstall→skipWaiting()fromweb/src/sw.ts(it was a one-time recovery bridge fromcd8394f); keptactivate→clients.claim()andSKIP_WAITINGmessage handler.useUpdatePromptreturns{ needRefresh, applyUpdate, dismiss }(no toast side-effects); dismissed flag clears on rawneedRefreshfalse→true transition so newer updates re-show the banner.MobileUpdateBannermounted inApp.tsxnext to mobile branch ({isMobile && <MobileUpdateBanner />}), positioned atbottom: calc(50px + env(safe-area-inset-bottom)), z-40. Vitest gotcha:virtual:pwa-register/reactrequires a Vite resolve alias to a stub (web/vitest.config.ts+web/src/test/stubs/pwa-register-react.ts) because import-analysis runs beforevi.mockcan fire. Commitsb9918bf,ab209ad,9e19390,c8b098f. See pwa-update-prompt. - Mobile notifications UI (bell + bottom drawer) shipped. Mobile previously only had
MobileNotificationsScreen(settings — push toggle / preferences / push types) and no in-app notification list. AddedMobileNotificationButton(bell + copper unread badge) wired asMobileHeaderrightSlot default ({rightSlot ?? <MobileNotificationButton />}— pages opt-in to the bell by not passing rightSlot). OpensMobileNotificationsSheet— vaul Drawer at 85vh (same template asSearchSheet) containing PushDiscoveryNudge (extracted toweb/src/components/crm/notifications/PushDiscoveryNudge.tsxand reused by both desktop popover and mobile sheet — DRY), Mark-all-read header, All/Tasks/PRs/System tabs (sameTAB_FILTERSmapping as desktop), ScrollArea ofNotificationItems (reused as-is), footer link to/settings/notifications. Codified two patterns: vaul Drawer = canonical mobile sheet template (next sheet, follow this), andMobileHeaderrightSlot fallback pattern. Test gotcha codified: when default-rendered surfaces pull inuseSupabase, bare-render page tests break — fix by mocking the new component to() => nullrather than wrapping in providers; applied inMobileMoney.test.tsxandMobilePlan.test.tsx. Commitsf2d0512,1a8c1f0,1d4bb18. See mobile-notifications-ui. - P0 production incident — service worker locked all users out of CF Access. Symptom: every user of
wrcm.levandor.io(admin web + iOS PWA) saw full-screenAuthentication Error: No CF Access token available, persisting across refresh. Root cause: the custom SW introduced for push notifications (commitac5f429 build(pwa): switch to injectManifest with custom sw.ts) registered a WorkboxNavigationRoute(createHandlerBoundToURL('/index.html'))inweb/src/sw.ts, which intercepted every navigation and served cached/index.htmlinstead of going to network. Behind CF Access ZTNA this is catastrophic: CF Access expiresCF_Authorizationcookies periodically and refreshes them via a 302 → IdP → 302 dance on the next navigation. With the SW intercepting, that dance never happens, the cookie never refreshes,cf-access.ts:79throws “No CF Access token available” forever, and refresh doesn’t help (SWs persist across refresh). Took ~24h to manifest because users with fresh cookies kept working — only as cookies naturally expired throughout the day did the lockout spread, which misled triage. Fix incd8394f: (1) removeNavigationRoutefromweb/src/sw.ts, (2) addinstall→skipWaiting()+activate→clients.claim()so the new SW takes over immediately, (3) add “Reset session and reload” recovery button on AuthError screen inweb/src/lib/supabase.tsxthat unregisters all SWs + clears caches + reloads. Hard rule going forward: ZTNA + custom SW must never useNavigationRoute. The push-notifications spec must add this as an explicit “don’t.” Stale doc flagged:web/CLAUDE.mdstill claims “anon key, no JWT auth” but JWT exchange viacf-access-authwas reintroduced after769ba5c— separate follow-up. Misleading triage signal: user (correctly) suspected recent centralized notifications work, but the migrations were a red herring; the earlier push-notifications SW commit was the culprit. See incident-2026-05-10-sw-cf-access-lockout.
2026-05-09
- Supabase push notifications research. No implementation yet. Headline: Supabase has no native push product — it provides primitives (Postgres for tokens, Edge Functions for delivery, DB webhooks for triggers); transport is BYO. Three transport options surveyed — Web Push (VAPID + Service Worker) is recommended starting point (fits the existing PWA, browser-native, works in iOS Safari 16.4+ standalone), Expo Push deferred until iOS native app exists, FCM/APNs direct only if Web Push proves insufficient. Existing infra that plugs in:
vite-plugin-pwaalready enabled inweb/vite.config.ts(would needinjectManifestovergenerateSW),notifyedge function already webhook-driven intonotificationtable (extend or siblingsend-push),notification+notification_preferencetables with RLS, in-appNotificationBell/NotificationPopover/NotificationItem+useNotificationshook, iOS PWA meta tags already set inindex.html. Greenfield: VAPID keys,push_subscriptions(user_id, subscription jsonb, created_at, last_seen_at)table with RLS, SWpush+notificationclickhandlers,usePushNotificationshook (gate subscribe behind explicit user gesture), Edge Function usingnpm:web-pushwithPromise.allSettledand DELETE on 410 Gone for cleanup, DB webhook onnotificationINSERT, mobile-detection hook (currently missing). iOS PWA gotchas — only iOS 16.4+, only after Add-to-Home-Screen, 🚨 CF Access ZTNA + iOS standalone cookie spike directly threatenspushManager.subscribe()upsert — must validate before planning. Other gotchas — DB webhook 1000ms timeout (fast or queue), subscription rot (delete on 410), SW must NOT cache user-scoped API responses,userspace/has no PWA. Open decisions: scope (PWA only vs PWA + iOS APNs), which notification types push (extendnotification_preferencewithpush_enabled?), mobile-only or desktop too, validate the cookie spike first. See push-notifications. - Mobile native-feel transformation Phase 5-10 SHIPPED. 18 commits, 37 files, +2,211 / −13 lines. Plan tab (NowHeroCard, UpNextBlockCard, BlockCreate/EditSheet, LaterTodayList swipe actions, DoneTodayCollapse, StandupStreamSheet, MobilePlan composition with
lastEditIdRef+__setPlanQuickAddtyped-global hatch). Hours tab (WeekStrip, MobileHours +useTimesheetWeek). Money tab (BudgetGlanceCard, MobileMoney +InvoiceListItem, BudgetDetailSheet v1). More tab (MobileMore + SectionLink with CF Access logout). Cross-cutting (SearchSheet stub, Sonner top-center). Tests (mock factories, MobileShell + MobileHome/Plan/Hours/Money render tests, IntersectionObserver stub). See mobile-native-feel. - Audit pass — 7 fixes. Lockfile sync (
60a1acdblocking CF Pages frozen-lockfile build). Hours-tab “garbage hours” UTC date drift bug (01880a0) —toISOString().slice(0,10)is timezone-unsafe; fixed via newtoLocalDateString()inweb/src/lib/date-utils.ts, same bug inuseDashboardSummary.ts, dedupstartOfWeek, 8 unit tests added.formatTimededup (8e7b0ad).window.confirm()→ inline two-step Cancel + Confirm-delete (dcdbec7). StandupStreamSheet render-time setState → useEffect (7da8153). Type__setPlanQuickAddviadeclare global+ memoize MobilePlan status-only collections (2c5960c). StabilizeusePullToRefresheffect deps (was rebinding 50+ touch listeners per gesture, breaking iOS PtR) + MobileHeader duplicate-h1 a11y fix (ee1652e). See Audit Pass and debugging-log-crm. - Mobile UI redesign — 4 commits. Drawer surface tokens (bg-white + warm scrim) + explicit input/select text colors fixing the invisible-dropdown-text bug (
<select>+appearance-none+ Lucide ChevronDown overlay) (7f4bc84). Unified hero/card/list visual treatment (2a43df8). Refined header, bottom tabs, sheet internals — BottomTabs got a 2px-tall active-pill indicator (60f7fa6). Polish — WeekStrip, StatusFilterPills, settings list, Sign out → rose-600 (31936a5). - Bug fixes + Levandor copper brand pass. Hours card respects selected day (“Today” only when
active === today) + wire CurrencyProvider for HUF (replacing hardcoded EURIntl.NumberFormat) (2c82bd0). Brand pass (9448637) — discovered actual brand inpackages/ui/src/styles/globals.css:--copper: hsl(31 53% 64%)≈ d8a777,--warm-black: hsl(40 11% 5%). Mobile heroes switched frombg-zinc-900to warm-black; selected pills (WeekStrip, StatusFilterPills, BottomTabs active indicator + label + icon) use copper bg with warm-black text; hero progress bars use copper fill; ProjectHealthCard “green” status dot becomes copper. See levandor-brand. - Forecast horizon fix — 2 commits. ForecastHero was comparing weekly revenue against monthly forecast (apples-to-oranges, the 27% looked off because of mixed temporal scopes). First fix
netThisMonth/netForecast“This month” (1697360). Final fix (6f1fdb5) — mobile ForecastHero rewritten to mirror desktop dashboard hero exactly: eyebrow + big number + caption (avg_hrs × workdays × rate) + 2×2 stat strip. See dashboard-forecast. - Phone-native day rituals rebuild — 2 commits. Replaced StandupStreamSheet (5 desktop components stuffed in a 90vh sheet) with 4 focused phone-native sheets — MorningCheckinSheet (single scrollable form, phone-native input controls per datapoint type: NUMBER pills 44px tap, SCALE 0..max round circles, BOOLEAN big two-button toggle, TEXT autosizing textarea), StandupNotesSheet (phone journal feed with bottom-pinned composer + brand-tinted type chips: GOAL=copper, BLOCKER=amber, WIN=emerald), LunchCheckinSheet (focused mid-day form), EveningReflectionSheet (autosizing summary + day-in-numbers stat strip + Close-day stamp). MobilePlan rewired with 5 sheet states + time-aware CTAs (
b7626d9). Evening Checklist v1 manifestation tracker (482e592) shipped both mobile sheet (EveningChecklistSheet) + desktop card (EveningChecklistCard). See mobile-day-rituals. - Evening Checklist backend wiring — 2 commits. Migration 008 (
3c3501d) widenedstandup_datapoint_config_phase_checkCHECK to include'EVENING_CHECKLIST', seeded 3 free-text prompts (proud_of, not_proud_of, other_notes). Migration applied to remotemkofmdtdldxgmmolxxhc. Both sheet + card now real, persist viauseUpsertDatapoint(upserts on(standup, config)— no duplicates on edit). Migration 009 (2849661) added 4th “Shit I overspent on” prompt (key=overspent_today, sort_order=4, optional TEXT). Idempotent INSERT + UPDATE. See evening-checklist. - Debounced autosave — 2 commits. New
useDebouncedAutosave(value, save, delay=600ms)hook inweb/src/lib/hooks/, returns AutosaveStatus, skips initial mount (f718425). EveningChecklistSheet (mobile) + EveningChecklistCard (desktop) refactored to per-fieldChecklistFieldwith autosave; Save buttons removed; aggregate (worst-of) status pill in header. Then (7d18acb) MorningCheckinSheet (mobile) per-field MorningField subcomponent with autosave, EveningReflectionSheet summary autosaves, desktop standup page (pages/standup/index.tsx) gets parent-level debounced autosave for both morning intake (per-config Map of timers) and evening summary (single timer). Completion buttons (Complete morning, Close day) flush pending timers + stamp the phase flag only — data is already persisted. See mobile-autosave. - Mobile native-feel design spec patched after pre-mortem. Initial spec at
docs/superpowers/specs/2026-05-09-mobile-native-feel-design.mdwas validated by a 4-judge council (agentops:council --deep --preset=plan-review) which returned WARN×4 / HIGH confidence with 46 findings clustering on factual errors. Convergent fixes applied inline: auth was Clerk in spec → corrected to CF Access ZTNA (with iOS standalone cookie spike flagged as plan-blocker); draft-timesheet status/source columns invented → added migration + atomiccomplete_block_with_draft_v1RPC with partial unique index for idempotency;dashboard_summary_v1RPC referenced wrong tables (timesheet_entry,project_v) → fixed + scoped as full revenue port (~5h backend); PDF<object>embed doesn’t render in iOS PWA standalone → replaced with link-out; sheet state ownership pattern added; font-display token left alone (it’s DM Serif Display) — mobile uses font-sans (Outfit) instead. 19 medium/low items captured as plan-task hints in spec. See mobile-native-feel for full decision log. - Started mobile native-feel transformation brainstorm — see mobile-native-feel. Locked 10 decisions (PWA, iPhone-only, shell-only offline, Levandor-branded visual style, 5 bottom tabs, Now-focused day-plan, day-first timesheet, mobile-first rebuild strategy). Chunk 1 (foundations) approved; Chunk 2 (per-surface designs) proposed; Chunk 3 (visual system + perf + testing) pending. Spec doc not yet written.
2026-05-04
- Initialized activity log.