# CLAUDE.md — `web/src/components/in/`

The **inbox cockpit** — `/u/[slug]/in`. Where a business runs marketing, sales, and service: every customer + channel in one realtime stream. Spec: `text/inbox.md` · plan: `text/inbox-todo.md`.

## The one rule: `in` operates, never builds

Every verb here is **operate** (reply, tag, assign, connect, archive, complete). Every **build** affordance (configure an agent, author a macro, edit availability, deep analytics) is an **outbound `<a href>` to Studio** (`/u/{slug}/{agents,settings,analytics}`), never an in-place editor. Studio builds; `in` runs.

## Shell

`Inbox.tsx` is the hub. `InboxRail.tsx` renders a 220px left rail: **`GroupSwitcher`** (top, where am I) · **`DEST_RAIL`** buttons (middle, dest-based routing via `inbox-data.ts`) · **`AuthButton`** (bottom, who am I). `FunctionTabs` spans the right column as a top bar (fn lenses: marketing/sales/service/all).

Dest-based routing (`Dest` type from `@/data/in-types`):

| dest | right pane |
|------|-----------|
| `messages` / `tasks` / `companies` | EntityList → EntityDetail |
| `people` | EntityList → PersonScreen |
| `agents` | EntityList → AgentSupervisionScreen |
| `funnel` | LifecycleFunnel + KPI strip (full width) |
| `board` | PeopleKanban (full width, ContactDrawer overlay) |
| `calendar` | AppointmentCalendar (sales) / ContentCalendar (marketing) / dated EntityList |
| `analytics` | `window.location.assign(…/analytics)` via `useEffect` — never rendered in-place |

`ContextSlideOver` is the right-edge overlay for person context (linked person, quick CRM).

Hypothesis cards (learning dimension → ADVANCED → Insights lens) render a confidence bar in `EntityCard` from `entity.confidence` (C8). The rail rows live in `inbox-data.ts` `RAIL_ORDER`, not `lib/in/spaces.ts`.

## Hooks — `src/hooks/use-inbox-*.ts`

| Hook | What |
|------|------|
| `use-inbox-data` | SSE + fetch + entity state + unread badge + actorMap |
| `use-inbox-url-state` | fn/dest/owner/tagFilters/query/selectedId ↔ URL (`history.replaceState`) |
| `use-inbox-actions` | run(verb, entity) dispatcher — archive/save/assign/tag/complete/etc. |
| `use-inbox-counts` | unreadByFn + openTasks derived from live entities |
| `use-inbox-keyboard` | ⌘K spotlight, ? help overlay, j/k navigation, keyboard shortcuts |
| `use-inbox-selection` | multi-select set + bulk-tag input ref |
| `use-inbox-refresh` | stable setInterval helper (no stale-closure) |

## Pure modules — `src/lib/in/*` (no fetch, no JSX)

| Module | What |
|--------|------|
| `channel.ts` | `CHANNEL_META` · `deriveChannel` · `replyTarget` |
| `draft-review.ts` | `pendingDraft` · `visibleMessages` |
| `assign.ts` | `buildAssign` · `isAssignedTo` · `assignedToMe` |
| `spaces.ts` | `filterByTags` · `FOCUS_PRESETS` · `applyFocus` · URL round-trip |
| `views.ts` | `funnelStages` · `calendarItems` · `kpiStrip` |
| `overview.ts` | `canViewOverview` · `overviewKpis` |
| `participants.ts` | `resolveParticipant` · `add/removeParticipant` · `parseMentions` · `isInternalSpace` |
| `session.ts` · `push.ts` | reopen-where-you-left-off · unread badge + web push |

These modules are unit-tested and will be re-wired to UI components in future cycles.

## Action dispatcher — every verb is real (no stub toasts)

`use-inbox-actions.run(verb, entity)` wires every action-bar verb to an existing primitive (`ask`/`signal`/`warn`/`mailto`). Verb→receiver map: see `use-inbox-actions.ts`. Security: every write receiver scopes D1 to `ctx.ownerSlug` — never `body.slug`/`body.actorId` (IDOR rule, `text/roles-authority-plan.md`).

## Overlay stores (D1, per-workspace)

| Overlay | Shape | Receiver | Migration |
|---------|-------|----------|-----------|
| `entity_tags` | set of tags per entity | `entity:tag` | 0098 |
| `entity_links` | one linked person per entity (Connect) | `entity:connect` | 0099 |
| `entity_participants` | set of actors (human/agent) per entity | `entity:participant` | 0100 |

**Security (non-negotiable).** Every overlay receiver scopes its D1 WHERE to `ctx.ownerSlug` — the server-attested workspace — and throws on empty. It NEVER reads `body.slug` / `body.actorId` (the IDOR rule, `text/roles-authority-plan.md`). Cross-tenant write is structurally impossible.

## Realtime

Live arrival rides the **ANALYTICS_HUB Durable Object** (`api/events.ts`): the `inbox:{slug}` SSE topic broadcasts `inbox:refresh` → debounced refetch, no poll (60s heartbeat is the safety net only). SSE connect/reconnect lives in `use-inbox-data`; unread badge + web push is `use-inbox-data` + `src/lib/in/push.ts`.

## Don't

- Don't add an in-place build editor — link out to Studio.
- Don't authorize an overlay write off `body` — only `ctx.ownerSlug`.
- Don't reintroduce `ProfileHeader` or legacy `/in/{slug}/*` routing.
See also: `text/inbox.md` · `text/roles-authority-plan.md` · `.claude/rules/design.md` · `.claude/rules/ui.md`
