Clean successor to V3 (forge: lilith/atlilith). Seeded from local Mac working tree at ~/Code/@projects/@cocottetech/. node_modules and build artifacts excluded via .gitignore. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
18 KiB
Web portals · screen
The three desktop power-user portals on vps-0: content-portal, engagement-portal, and the agent-actions log viewer. Pairs with brief G §G1–§G3. §G4 (web companion) is covered separately in cross-device-handoff.flow.md.
Voice register: plain — operator surfaces, not hearth.
G1 · content-portal (content.cocotte.maison)
Three primary views, switched via top-left segmented control: Calendar · Assets · Personas.
G1a — Calendar (month grid, drag-to-reschedule)
┌──────────────────────────────────────────────────────────────────────┐
│ Cocotte · content [Calendar] Assets Personas 🔍 Q ▾ │
├──────────────────────────────────────────────────────────────────────┤
│ ‹ May 2026 › surface: all ▾ stakes: any ▾ [+ new plan] │
├──────┬──────┬──────┬──────┬──────┬──────┬──────────────────────────┤
│ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ Sun │
├──────┼──────┼──────┼──────┼──────┼──────┼──────────────────────────┤
│ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │ 10 │
│ ●OF │ ●X │ │ ●OF │ ●OF │ ●X │ ●OF ●X │
│ 9pm │ 11a │ │ 9pm │ 9pm │ 11a │ 9pm 11p │
├──────┼──────┼──────┼──────┼──────┼──────┼──────────────────────────┤
│ 11 │ 12 │ 13◐ │ 14 │ 15 │ 16 │ 17 ← today │
│ ●OF │ ●X │ draft│ ●OF │ ●OF │ ●X │ ●OF │
│ 9pm │ 11a │ hold │ 9pm │ 9pm │ 11a │ 9pm ──[drag]──▶ │
├──────┴──────┴──────┴──────┴──────┴──────┴──────────────────────────┤
│ legend: ● scheduled ◐ draft awaiting approval ✕ failed ⏸ paused │
└──────────────────────────────────────────────────────────────────────┘
- Click a chip → side-panel detail (asset thumb, caption diff, surfaces, scheduled time, specialist).
- Drag chip to another cell → reschedule (writes through
producer/publisher; emitsagent_actionrow). - Shift-click → multi-select for batch approve / batch reschedule.
- Right-click → "approve · edit · set aside · open in chat (deeplink CocotteAI)".
G1b — Asset library (batch-tag grid)
┌──────────────────────────────────────────────────────────────────────┐
│ Calendar [Assets] Personas tag: ▾ surface: ▾ unused-only □ │
├──────────────────────────────────────────────────────────────────────┤
│ [drop files or paste — presigned upload to MinIO] │
├──────────────────────────────────────────────────────────────────────┤
│ ☑[img]☑[img]☐[img]☐[img]☐[img]☐[img]☐[img]☐[img] │
│ berlin tour-tease studio studio outdoor outdoor selfie selfie │
│ ☐[img]☐[img]☐[img]☐[img]☐[img]☐[img]☐[img]☐[img] │
├──────────────────────────────────────────────────────────────────────┤
│ 2 selected · [+ tag] [− tag] [archive] [open in plan] │
└──────────────────────────────────────────────────────────────────────┘
Grid is virtualized; thumbnails lazy-load from MinIO via presigned URLs. Batch-tag bar appears on first selection.
G1c — Persona editor (side-by-side preview)
┌──────────────────────────────────────────────────────────────────────┐
│ persona: quinn-of-default ▾ [+ new] [duplicate] [archive] │
├───────────────────────────────────┬──────────────────────────────────┤
│ facets.json (keyboard editor) │ preview · surface: OnlyFans ▾ │
│ { │ │
│ "name": "Quinn", │ Quinn — Berkeley CA │
│ "voice": "steady, dry-witty", │ ───────────────────── │
│ "do_not": ["corporate", │ steady, dry-witty. shows │
│ "infantilizing"], │ up. doesn't oversell. │
│ "shows": ["tour dates", │ │
│ "studio process"], │ tour · berlin · oct 3–7 │
│ ... │ │
│ } │ [regenerate preview] │
└───────────────────────────────────┴──────────────────────────────────┘
Left pane is a typed JSON editor (schema-aware autocomplete on voice, do_not, shows). Right pane re-renders via producer on save (debounced 800ms).
G2 · engagement-portal (engagement.cocotte.maison)
CRM-shaped. Three primary views: Prospects · Funnel · Threads.
G2a — Prospect table (sortable)
┌──────────────────────────────────────────────────────────────────────┐
│ Cocotte · engagement [Prospects] Funnel Threads 🔍 Q ▾ │
├──────────────────────────────────────────────────────────────────────┤
│ stage: all ▾ surface: all ▾ warmth: ▾ since: 30d ▾ [export csv] │
├────┬────────────┬─────────┬───────────┬──────────┬──────────┬──────┤
│ ▢ │ handle ↑ │ surface │ stage │ last msg │ value 90d│ ⚠ │
├────┼────────────┼─────────┼───────────┼──────────┼──────────┼──────┤
│ ▢ │ @sf_mark │ Tryst │ booked │ 2h │ $1,400 │ │
│ ▢ │ @j.benson │ TS4Rent │ warm-lead │ 4h │ $0 │ blk? │
│ ▢ │ @rafael99 │ OnlyFans│ subscriber│ 1d │ $89 │ │
│ ▢ │ @kai.sf │ Tryst │ cold-lead │ 3d │ $0 │ │
│ ▢ │ @— │ Email │ inquiry │ 6d │ $0 │ k1? │
└────┴────────────┴─────────┴───────────┴──────────┴──────────┴──────┘
Click handle → prospect detail page. Click column header → sort. Multi-select → batch tag / move stage / open in audit slice.
G2b — Prospect detail page
Two-column. Left: identity card (handle, surface(s) seen on, K1 blocklist status, coop-intel chip if N5 hits, value timeline sparkline). Right: cross-surface message thread, interleaved by (ts, surface), with draft-but-unsent replies shown inline as ◐ rows. Footer: stage-change history + every agent_action row that touched this prospect (deeplinks to G3).
G2c — Funnel charts
Stacked area: cold-lead → warm-lead → booked → repeat, per-surface tabs + aggregate. Time-series sourced from TimescaleDB hot tier on vps-0. Hover scrubs a tooltip; click a band → table pre-filtered to that cohort.
G3 · agent-actions log viewer
Reverse-chrono table of every agent_action row. Power-user inspection surface — the desktop counterpart to iOS audit drawer (brief I).
┌──────────────────────────────────────────────────────────────────────┐
│ Cocotte · audit 🔍 Q ▾ │
├──────────────────────────────────────────────────────────────────────┤
│ specialist: ▾ action: ▾ stakes: ▾ outcome: ▾ range: 24h ▾ │
│ auto-only ☐ has-correction ☐ has-error ☐ [replay mode] │
├──────┬────────────────┬──────────────┬────────┬──────────┬──────────┤
│ ts │ specialist │ action │ stakes │ outcome │ target │
├──────┼────────────────┼──────────────┼────────┼──────────┼──────────┤
│ 11:02│ bookings-tryst │ bump │ low │ ok │ tryst │
│ 11:00│ producer │ draft_caption│ medium │ proposed │ of/p_881 │
│ 10:58│ triage │ classify │ low │ ok │ @sf_mark │
│ 10:47│ publisher │ post │ medium │ ok │ of/p_880 │
│ 10:41│ bookings-ts4r │ bump │ low │ failed ✕ │ ts4rent │
│ 10:40│ strategist │ note │ low │ ok │ — │
└──────┴────────────────┴──────────────┴────────┴──────────┴──────────┘
47 rows · last refresh 11:03 · [stream live ▶]
Click row → split-pane outcome_json viewer (left: raw JSON tree with diff highlighting against prior state for the same target; right: rendered summary — what changed, who saw it, downstream rows it triggered).
Replay mode (toggle top-right): time-scrub bar across the filter range. Drag scrubber → table snaps to "what was the agent-actions state at T?" with a ghost overlay showing rows that hadn't happened yet. Useful for postmortems.
Cross-cutting
- Auth: same SSO as CocotteAI iOS. Forbidden state on expired token → kick to SSO with deeplink back.
- Shared chrome: top bar (Cocotte wordmark · portal title · primary tabs · global search · user menu). Breadcrumbs only on detail pages (
Prospects / @sf_mark / Thread). - Global search (per brief U):
⌘Kopens U's typed grouped overlay; supports@handle,#tag,type:,surface:,since:,specialist:. Results land in the originating portal. - API plane: single — every portal reads from prod
platform.apion black over HTTPS. No dev API, no local API. Cache invalidation listens to Redis pub/sub via SSE from vps-0 (see G open-Q). - Cross-portal deeplinks: prospect row → audit slice for that prospect; calendar chip → audit row for the publish; audit row → prospect detail / calendar chip / asset view.
States per portal
| State | G1 content | G2 engagement | G3 audit log |
|---|---|---|---|
| empty | "No plans this month. [+ new]" | "No prospects in this slice yet." | "No rows in range." |
| loaded | grid / assets / persona populated | table sorted, default warmth=any | reverse-chrono, default 24h |
| error | inline banner above grid, retry button | toast + table dimmed, retry | toast + frozen table, retry |
| offline | top banner "Offline — last synced 11:02" | same | same; replay mode disabled |
| forbidden | full-page SSO kick | same | same |
| batch-mode | sticky action bar (approve / reschedule) | sticky bar (tag / stage / export) | n/a (read-only) |
Keyboard shortcuts
| Key | Action |
|---|---|
⌘K |
Global search overlay (brief U) |
⌘/ |
Toggle filter bar focus |
⌘. |
Cancel current selection / close detail sheet |
j / k |
Next / previous row (G2 table, G3 log) |
x |
Toggle row selection |
a |
Approve selected (G1 calendar drafts) |
r |
Reschedule selected → date picker |
e |
Edit selected → opens in chat handoff to iOS |
⌘↵ |
Confirm batch action |
[ / ] |
Prev / next month (G1) · prev / next range (G3) |
g c / g e / g a |
Jump portal: content / engagement / audit |
? |
Shortcut cheatsheet |
In-the-wild copy
Operational, plain.
- G1 empty calendar: "No plans this month. Draft one or ask Cocotte."
- G1 drag-reschedule confirm: "Move OF/p_881 from May 13 9pm → May 14 9pm?
rto confirm,escto cancel." - G2 prospect empty filter: "Nothing in this slice yet. Loosen a filter or check the funnel."
- G2 export: "Exporting 247 rows as csv. Download starts when ready."
- G3 filter chip: "specialist: content-onlyfans · last 24h · auto-only · 47 rows"
- G3 replay banner: "Replay at 2026-05-14 10:41. Rows after this point are hidden."
- Forbidden: "Session expired. Sign in to continue."
- Offline: "Lost connection. Showing last synced state from 11:02."
Edge cases
- Calendar conflict (two plans for same surface at same minute) → both chips render with a hatched border; click either opens a conflict resolver sheet (drop one / shift one / merge into a single multi-asset post).
- Drag across DST boundary → confirm sheet shows both old and new local times explicitly.
- Asset upload mid-flight, browser closed → presigned upload resumes on next page load via the resume token in IndexedDB; orphan parts older than 24h are GC'd server-side.
- Persona save with breaking facet (e.g. removed
name) → save blocked inline with the offending key highlighted; no silent partial write. - Engagement table stale (TimescaleDB hot tier lag > 60s) → top-bar chip "data ~2m behind" instead of silently showing stale.
- Audit replay across a kill-switch window (per brief K) → replay renders the kill-switch gap as a translucent striped band; rows during the gap are absent by design, with a footer link "why is this gap here?" → kill-switch flow.
- Batch action partial failure (4 of 5 rescheduled, 1 failed) → toast lists the failure; selection persists so retry is one keystroke.
- Coop-intel hit on a prospect → ⚠ chip in G2 row; click jumps to coop-intel detail (per brief N) rather than prospect detail.
- Forbidden mid-session (token expired while looking at G3) → modal preserves the current filter+scroll so re-auth lands back where Quinn left off.
Related
- brief G — web surfaces overview.
- brief A — iOS chat-home; every G action reachable here too.
- brief B — iOS drawers (calendar B1, assets B2, prospect, persona B5) that these portals mirror.
- brief I — audit trust loop; G3 is its desktop face.
- brief T — analytics; complementary to G2 funnel charts (T2).
- brief U —
⌘Ksearch overlay used by all three portals. cross-device-handoff.flow.md— covers G4 web companion.