PLUTO-64 ·
plutoToast to appEvent UX-friction capture (port Mars pattern + Pluto deltas)
- Ref
PLUTO-64(#938)- Project
pluto- Status
- done
- Priority
- normal
- Type
- task
- Assigned
- —
- Created by
- wi-cli-venus
- Created
- 2026-06-12T06:54:13.336Z
- Updated
- 2026-06-12T07:26:01.664Z
- Closed
- 2026-06-12T07:26:01.664Z
Questions
No questions.
Event log
-
Source: cross-app brainstorm (Elazar 2026-06-12) — adopt Mars's toast->appEvent UX-friction capture. Pluto is half-instrumented (logs clientError server-side) but has ZERO toast capture today: sonner.tsx is presentation-only, 154 toast() call sites, none log (coder confirmed). Greenfield. Severity discipline already clean (rejections log at info via logAuthz; category 'input' exists but unused) — no remediation needed first. CONVERGED DESIGN (Mars pattern + Pluto deltas, validated by pm-mars + pm-venus): - CLIENT: toast provider, on error-type toast ONLY, fire-and-forget fetch('/api/log-toast', keepalive:true).catch(noop) — never blocks the toast. success/info NOT logged. Add a client-side dedup window (Map<key,lastSentTs>, suppress same key ~45s/session) to kill render-loop bloat from day one (DELTA B — Mars had a 600+row loop). - API: /api/log-toast (nodejs, force-dynamic) — trim+cap message 500c, reject empty 400, getSession for roles, logEvent. Logging failure returns 200 (swallowed). - KEY INVERSION (non-negotiable): an error-TYPE toast logs at LEVEL=info, never warn/error — it is UX-friction guidance, not a system failure. Keeps captured toasts OFF the failure pager. No auto-escalate-by-toastType on this route. - ROW: level=info, category=toast, action=error-shown, message<=500c, route=pathname, actorUserId=session (null if unauth), detail={toastCode, toastType, role:[...], path, message}, appVersion auto. - DELTA A (stable code): every error toast carries a STABLE toastCode; GROUP BY code+route in the miner so renaming Spanish copy doesn't split history (Mars groups by raw string = its debt; Pluto skips it). - DELTA C (PII): static-template toast messages only, never PII-interpolated; parameterize route to /practicas/[id], not raw uuid. - ANALYZER two-tier: (A) daily failure pager EXCLUDES category=toast; (B) weekly UX-pain miner — GROUP BY toastCode,route HAVING count>=10, distinct-users via count(DISTINCT actorUserId), role breakdown via LATERAL jsonb_array_elements_text(detail->'role'), TOP_N=15, CRITICAL=100. Pluto is hub-reachable -> DM the digest DIRECTLY (skip Mars's agent-relay-row). Ranked artifact = db/pluto-ux-pain-map.md. - DEFER phase 2: one-shot persisted seen-flag (notifications.toastSeenAt) — only if Pluto adds server-originated once-only toasts; orthogonal to capture. LANES: coder (client provider + /api/log-toast + dedup + toastCode plumbing across call sites + miner cron); db (add category=toast to lookupOptions appEventCategory grupo; phase-2 toastSeenAt column if/when). Auth/email paths NOT touched -> standard post-push audit (no pre-impl design-ping needed). Mars hands over: severity-convention doc, MARS-81 analyzer query, MARS-84 digest format. AWAITING Elazar go.
-
FULL-LOOP requirement (pm-mars, cross-app standard): the port must close the loop, not just capture. Step 3 = the weekly miner cron DMs pm-pluto-cc directly on the hub (Pluto is hub-reachable, so no agent-relay-row). Step 4 = PM triages the ranked friction (top groups by hits x distinct-users, CRITICAL band >=100) into fix WIs + dispatches the UI fix. So PLUTO-64 deliverables = (a) toast capture, (b) weekly ux-pain miner cron, (c) the PM-DM hook to pm-pluto-cc. Pluto's first digest cadence to mirror Mars (Mon weekly). The routing is what makes it a process, not a dormant table.
-
Routing-mechanism correction (pm-venus/pm-mars): step-3 PM-DM has two valid architectures, pick at build — (A) Vercel cron writes a digest row (category=admin, action=ux-pain-digest) -> a host-side agent reads it + DMs pm-pluto-cc (relay-row indirection, Mars's pattern); or (B) run the weekly miner as a HOST-SIDE cron on the Pluto agent host (venus host — has DATABASE_URL + hub reach) -> DM pm-pluto-cc directly, no relay-row. (B) is cleaner if a host cron with DB creds is acceptable; (A) keeps it in-app. Earlier 'hub-reachable' note was imprecise — Vercel itself can't reach the hub. Delivery to pm-pluto-cc is committed either way; mechanism decided at build (host-cron option B likely preferred for Pluto).
-
Design pinned (coder, 2026-06-12): - ROUTING = Option A (Vercel cron /api/cron/ux-pain-digest, Bearer CRON_SECRET, Mon 13:00 UTC) + self-contained relay row (category=cron, action=ux-pain-digest, detail.report + detail.textReport + relay{channel:llmmsg, recipients:[pm-pluto-cc], delivered:false}) + sendAdminEmail to Elazar. Host-cron rejected (maintainer-lane, out of coder scope). - RELAY OWNER = pm-pluto-cc via a weekly SELF-CRON: PM wakes Monday post-digest, reads the delivered:false row, triages ranked friction into fix WIs, marks relay.delivered=true. No infra WI, no maintainer dependency. PM arms the cron once the endpoint is live (post-PASS). If the self-cron proves flaky, escalate to a host-cron-direct-DM infra WI (venus maintainer + db). - toastCode (DELTA A) = REQUIRED typed param on a new toast.error(msg,{code}) wrapper (@/lib/toast) → tsc enforces 100% coverage; no error toast ships without a stable code. Sweep = 102 toast.error sites across ~30 files (import swap + add code); 52 success/info sites pass through unchanged. - NO pager-exclusion needed: Pluto's failure pager polls the securityAlerts table (not appEvents broadly) + toast rows are level=info → toasts can't trip it (differs from Mars). - ETA first push ~50-60min (the 102-site sweep dominates). Single coherent commit, shared-tree-serialized behind PLUTO-34/62.
-
DELIVERY-HOP design revised (pm-mars MARS-102 standardization, 2026-06-12) — supersedes the relay-row + PM-self-cron decision: - Hub BUFFERS messages for offline agents -> digest DM'd to pm-pluto-cc waits in unread buffer; PM drains on reconnect. NO PM self-cron, NO always-on relay agent, NO db/coder-host cron (those agents aren't 24/7). - Cross-app STANDARD target = Vercel weekly cron -> authenticated POST into the hub addressed to pm-pluto-cc -> PM drains from buffer. Gated on 2 open maintainer Qs: (1) can Vercel POST through the reverse tunnel with an auth key, (2) hub buffer TTL must exceed a week. - BUILD NOW (ungated): capture + weekly miner + sendAdminEmail to Elazar + self-contained digest payload (detail.report ranked groups + detail.textReport). - Delivery hop = PLUGGABLE abstracted sendDigestToPM seam; no-op/leave-row for now, becomes the auth hub-POST once MARS-102 maintainer Qs resolve. Do NOT hardcode the relay-row. - Net: Elazar gets the weekly email immediately; PM auto-DM lands when the hub-POST hop is wired (shared MARS-102, not Pluto-only). coder pinging coder-mars for the seam shape. - PM action DROPPED: no Monday self-cron to arm post-ship (hub-buffer supersedes it).
-
Toast UX-friction capture loop shipped. toast.error wrapper requires tsc-enforced {code} (45s per-code,path client dedup) + /api/log-toast ingest (level=info category=toast, all fields server-pinned, message cap 500c, route sanitized to [id]) + 102 toast.error sites swept to stable dotted codes. Weekly miner ux-pain-digest (GROUP BY toastCode,route) + cron Mon 13:00 UTC (timingSafeEqual Bearer) + sendAdminEmail to Elazar + pluggable sendDigestToPM seam. Shipped 489109f, live v1.67.30. Audit PASS:489109f: build green=102 sites type-covered, zero runtime errors, log-toast non-forgeable+bounded, digest escapeHtml'd, no XSS-to-email. Residual anon-flood rate-limit → PLUTO-66.