follow-up-sla-report.md16.4 KBView on GitHub
# Follow-up SLA Report

## 1) Introduction — goal, present state, future state

We want a new Cedar Doc — "Follow-up SLA" — that is exclusively about response speed and follow-up cadence, modeled on the existing "What is your top rep doing differently?" report (UUID `1dd36b57-9e9d-4570-8eca-fa63584f203d`) but narrowed to the SLA story: how fast reps respond, how consistently they follow up, how that correlates with winning, and what breaks a no-reply streak. Today that report is authored as markdown with `\`\`\`dashboard` fences (rendered by the dashboard grammar in [apps/mail/modules/dashboards](apps/mail/modules/dashboards)) and seeded into a user's knowledge base by [seed-dashboard-doc.ts](apps/server/src/scripts/seed-dashboard-doc.ts); the grammar already supports radial duration stats, grouped bar/line charts, comparison bars, tables with doc links, and repeaters, but it has no way to embed the `ConversationActivityOverview` timeline. The change adds one net-new block type — `timeline` — that maps a mock event array to [ConversationActivityOverview](apps/mail/modules/conversations/components/timeline/ConversationActivityOverview.tsx), then authors the all-mock SLA report markdown and seeds it to <email>.

## 2) Present state

### 2.1 Architecture diagram

```text
 playbook-docs.json            seed-dashboard-doc.ts            documents table
 [{path,title,markdown}]  ──►   writeDocument(upsert)     ──►   content (md) + contentYjs
                                                                      │
                                                                      ▼
                                                            markdown editor (TipTap)
                                                                      │
                                                       DashboardFenceNode (```dashboard)
                                                                      │
                                                                      ▼
                                                            DashboardViewer (spec)
                                                                      │
                                                          LayoutNode dispatcher (blocks.tsx)
                                                                      │
              ┌──────────────┬──────────────┬───────────────┬────────┴───────┐
            stat           chart           table          comparison        repeater …
          (radial)     (bar/line/pie)   (docLink/rep)   (row vs team)    (entityCard grid)

          ConversationActivityOverview  ◄── NOT reachable from the dashboard grammar
```

### 2.2 Step-by-step walkthrough

1. **Seed entry point** — `main` at [seed-dashboard-doc.ts:37](apps/server/src/scripts/seed-dashboard-doc.ts) reads `apps/mail/docs/playbook-docs.json` (an array of `{ path, title, markdown }`), resolves the target user/org, and on `CONFIRM=1` upserts each doc.
   - Data per doc:
     ```json
     { "path": "user/files/...", "title": "...", "markdown": "# ...\n\n```dashboard\n{...}\n```" }
     ```
2. **Document write** — `writeDocument` at [apps/server/src/services/documents](apps/server/src/services/documents) persists `content` (markdown) and syncs `contentYjs` so the editor hydrates the same fences losslessly.
3. **Fence parse** — `DashboardFenceNode` at [DashboardFenceNode.tsx:48](apps/mail/modules/conversations/components/tiptap-extensions/DashboardFenceNode.tsx) tokenizes a `\`\`\`dashboard` block, keeps the raw JSON on `node.attrs.spec`, and renders `DashboardFenceView`, which calls `parseDashboardSpec`.
4. **Spec validation** — `parseDashboardSpec` at [dashboard.ts:416](apps/mail/modules/dashboards/types/dashboard.ts) runs the Zod `dashboardSpecSchema`; on failure it renders an error card instead of crashing. The `layoutNodeSchema` union at [dashboard.ts:87](apps/mail/modules/dashboards/types/dashboard.ts) enumerates every legal block — `timeline` is absent today, so any such node fails validation.
5. **Source resolution** — `DashboardViewer` at [DashboardViewer.tsx:27](apps/mail/modules/dashboards/components/DashboardViewer.tsx) resolves each `dataSources` entry. Mock sources resolve inline via `applyComputations`; query sources hit `dashboard.execute`. Resolved rows are provided through `SourcesProvider`.
   - Resolved source shape:
     ```json
     { "headline": { "rows": [{ "response": 4320, "postMeeting": 21600, "followup": 155520 }], "isLoading": false } }
     ```
6. **Layout dispatch** — `LayoutNode` at [blocks.tsx:66](apps/mail/modules/dashboards/components/blocks.tsx) switches on `node.type` and recurses. Relevant leaves for SLA already exist:
   - `StatBlockView` at [blocks.tsx:230](apps/mail/modules/dashboards/components/blocks.tsx) — `style: 'radial'` draws a ring filled to `numeric / max`; `format: 'duration'` renders seconds via `secondsToDuration`.
   - `ChartBlockView` at [blocks.tsx:298](apps/mail/modules/dashboards/components/blocks.tsx) — `bar` (grouped via `series`, `orientation: 'horizontal'` grows height per row), `line`, `pie`.
   - `ComparisonView` at [blocks.tsx:645](apps/mail/modules/dashboards/components/blocks.tsx) — two bars per field (subject `HIGHLIGHT` vs team `NEUTRAL`); inside a repeater it auto-uses `scope.row` vs `scope.team` (team aggregate from `teamAggregate`).
   - `TableBlockView` at [blocks.tsx:386](apps/mail/modules/dashboards/components/blocks.tsx) — `docLink` and `rep` column types, `duration` formatting.
   - `RepeaterView` at [blocks.tsx:774](apps/mail/modules/dashboards/components/blocks.tsx) — renders `template` once per row with `ScopeProvider`, supplying `team` for comparison blocks.
7. **Timeline component (currently standalone)** — `ConversationActivityOverview` at [ConversationActivityOverview.tsx:319](apps/mail/modules/conversations/components/timeline/ConversationActivityOverview.tsx) takes `events: ConversationEvent[]` + optional `timeRange`, positions colored dots, and draws gap badges (e.g. "3 weeks"). It derives each dot's type/title from `getEventType`/`getEventTitle` at [helpers.tsx:6](apps/mail/modules/conversations/components/timeline/helpers.tsx), which key off the **presence of sub-objects** (`emailEvent`, `meetingEvent`, `callEvent`…) and `direction`, and it filters out `external_crm` events.
   - Minimum fields the component actually consumes per event:
     ```ts
     { id: string; occurredAt: Date; direction?: string;
       emailEvent?: { subject?: string } | null;
       meetingEvent?: { title?: string } | null;
       callEvent?: {} | null }
     ```

## 3) Designed state

### 3.1 Architecture diagram

```text
 follow-up-sla.md ──► playbook-docs.json ──► seed-dashboard-doc.ts ──► documents table
 (new fixture)        (new entry appended)        (CONFIRM=1)                │
                                                                            ▼
                                                                  DashboardViewer → LayoutNode
                                                                            │
        ┌──────────┬──────────┬───────────┬────────────┬──────────┬────────┴─────┐
      stat       chart      table     comparison    repeater    timeline (NEW)
    (radial)  (bar/line)  (docLink)  (vs team)   (rep cards)        │
                                                                    ▼
                                          adapt mock rows → ConversationEvent[]
                                                                    ▼
                                              ConversationActivityOverview
```

### 3.2 Step-by-step walkthrough

1. **New schema member** — add `timelineBlock` to [dashboard.ts](apps/mail/modules/dashboards/types/dashboard.ts) and include it in the `layoutNodeSchema` union at [dashboard.ts:87](apps/mail/modules/dashboards/types/dashboard.ts), plus a matching `TimelineBlock` interface in the manual `LayoutNode` union at [dashboard.ts:391](apps/mail/modules/dashboards/types/dashboard.ts).
   - Block shape (mock-source friendly; one event per source row):
     ```ts
     interface TimelineBlock {
       type: 'timeline';
       source?: string;        // named source whose rows are events
       rowField?: string;      // or an array on the current repeater row
       title?: string;
       rangeStart?: string;    // ISO; optional fixed window start
       rangeEnd?: string;      // ISO; defaults to now
     }
     ```
   - Expected event row shape (authored in `dataSources[*].mock`):
     ```json
     { "kind": "outbound_email", "at": "2026-03-04T15:00:00Z", "title": "Recap + single CTA" }
     ```
   - `kind` ∈ `inbound_email | outbound_email | meeting | call` (matches the dot colors the component already knows).
2. **Dispatcher case** — add `case 'timeline': return <TimelineView block={node} />;` to `LayoutNode` at [blocks.tsx:66](apps/mail/modules/dashboards/components/blocks.tsx).
3. **TimelineView adapter** — new component in [blocks.tsx](apps/mail/modules/dashboards/components/blocks.tsx). It reads rows via the existing `useBlockRows({ source, rowField })`, then maps each row to the minimum `ConversationEvent` the timeline consumes, branching on `kind`:
   - `inbound_email`/`outbound_email` → `{ emailEvent: { subject: title }, direction: kind === 'inbound_email' ? 'inbound' : 'outbound' }`
   - `meeting` → `{ meetingEvent: { title } }`; `call` → `{ callEvent: {} }`
   - Common fields: `id` (index), `occurredAt: new Date(row.at)`, plus required-but-unused `ConversationEvent` fields cast/defaulted so TS is satisfied.
   - Renders `<ConversationActivityOverview events={adapted} timeRange={rangeStart ? { start, end } : undefined} className="..." />`, wrapped with an optional title label, matching the other blocks' header style.
   - Data after adapt (one row):
     ```ts
     { id: '0', occurredAt: Date('2026-03-04T15:00:00Z'),
       direction: 'outbound', emailEvent: { subject: 'Recap + single CTA' } }
     ```
4. **Report markdown** — author `apps/mail/docs/follow-up-sla.md` (human-readable fixture, same role as `playbook-doc.md`) containing the sections below as `\`\`\`dashboard` fences with all-`mock` data:
   1. **TL;DR** — `text` intro + a single highlight `text`/callout ("Won deals get a first reply in 1.2h; lost deals in 5.4h").
   2. **The SLA standard** — `grid` of radial `stat`s: response time, post-meeting follow-up, follow-up gap (each `format: 'duration'`, `style: 'radial'`, `max` = the SLA *target* in seconds so the ring reads as budget consumed) + a `percent` radial for "% of first replies within target".
   3. **Why it matters** — grouped horizontal `bar` (won vs lost: first-response & follow-up gap) + a `line` chart of win rate vs response-speed bucket (`<1h, 1–4h, 4–24h, 1–3d, >3d`).
   4. **Per-rep leaderboard** — sorted horizontal `bar` of SLA-compliance % by rep, then a `repeater` (grid) of `entityCard`s: `profile` + a `comparison` block (rep durations vs team avg for response/follow-up/post-meeting) + a `stat` win rate beside it.
   5. **What good looks like** — a 2-column `grid` of two `timeline` blocks: a tight-cadence won deal vs a gap-ridden lost deal (the gap badges carry the story).
   6. **Breaking the no-reply streak** — `repeater` (list) of pattern-breaker emails (name · context · reply rate, subject, body) mirroring the existing report's `breakers` section.
   7. **Extras** — response-time distribution `bar` histogram; post-meeting follow-up split (same-day / next-day / >2d) `bar` with win impact; a live-style "deals breaching SLA now" `table` with `docLink` columns; a cadence/touches-to-reply `table` (won vs lost).
5. **Seed entry** — append one `{ path, title, markdown }` object to [playbook-docs.json](apps/mail/docs/playbook-docs.json) with `path: "user/files/follow-up-sla"`, `title: "Follow-up SLA"`, and `markdown` = the file body from step 4.
6. **Seed run** — `CONFIRM=1 pnpm tsx src/scripts/seed-dashboard-doc.ts` (from `apps/server`) upserts the doc into <email> via `writeDocument` at [seed-dashboard-doc.ts:68](apps/server/src/scripts/seed-dashboard-doc.ts); open `/brain → Files → "follow-up-sla"`.

## 4) Implementation phases

### Phase 1 — Add the `timeline` dashboard block

**Goal:** The dashboard grammar validates and renders a `timeline` block from mock events via `ConversationActivityOverview`.

- [x] Add `timelineBlock` Zod schema (`type: 'timeline'`, optional `source`, `rowField`, `title`, `rangeStart`, `rangeEnd`) in [dashboard.ts](apps/mail/modules/dashboards/types/dashboard.ts).
- [x] Add `timelineBlock` to the `layoutNodeSchema` union at [dashboard.ts:87](apps/mail/modules/dashboards/types/dashboard.ts).
- [x] Add the `TimelineBlock` interface and include it in the `LayoutNode` union at [dashboard.ts:391](apps/mail/modules/dashboards/types/dashboard.ts).
- [x] Add `case 'timeline'` to the `LayoutNode` dispatcher at [blocks.tsx:66](apps/mail/modules/dashboards/components/blocks.tsx).
- [x] Implement `TimelineView` in [blocks.tsx](apps/mail/modules/dashboards/components/blocks.tsx): read rows via `useBlockRows`, adapt `{ kind, at, title }` rows → `ConversationEvent[]` (branch on `kind` for `emailEvent`/`meetingEvent`/`callEvent` + `direction`), render `ConversationActivityOverview` with optional `timeRange` and title.

**Tests:**

- [x] Unit test in `apps/mail/tests/modules/dashboards/timeline-block.test.tsx`: `parseDashboardSpec` accepts a spec containing a `timeline` block (and still rejects an unknown block type).
- [x] Render test: a `timeline` block with mock event rows mounts `ConversationActivityOverview` and renders one dot per event (no crash on missing optional sub-fields).
- [x] `cd apps/mail && npx jest tests/modules/dashboards` (workspace `@zero/mail`)

### Phase 2 — Author the Follow-up SLA report

**Goal:** A complete, all-mock markdown report exists as a fixture and renders every section without validation errors.

- [x] Create `apps/mail/docs/follow-up-sla.md` with sections 1–7 from §3.2 step 4, each as a `\`\`\`dashboard` fence with `mock` data sources.
- [x] Section 2: radial duration `stat`s with `max` set to SLA targets + a compliance `percent` radial.
- [x] Section 3: won-vs-lost outcome `table` (duration-formatted) + win/reply-rate-vs-speed-bucket `line` chart.
- [x] Section 4: per-rep `repeater` of `entityCard` → `profile` + win-rate `stat` + `comparison` + an inline per-rep `timeline` (`rowField: "events"`) showing a sample deal's cadence.
- [x] Section 6: pattern-breaker `repeater` (list) mirroring the existing `breakers` block.
- [x] Section 7 extras: distribution histogram `bar`, post-meeting split `bar`, "breaching SLA now" `table` with `docLink` columns, cadence/touches `table`.

**Tests:**

- [x] Add a fixture test (`apps/mail/tests/modules/dashboards/follow-up-sla-fixture.test.ts`) that parses every `\`\`\`dashboard` fence in `apps/mail/docs/follow-up-sla.md` through `parseDashboardSpec` and asserts each returns non-null (guards against authoring typos).
- [x] `cd apps/mail && npx jest tests/modules/dashboards` (8 cases green)

### Phase 3 — Seed the document

**Goal:** The report is seeded into <email> and visible under /brain.

- [x] Append a `{ path: "user/files/follow-up-sla", title: "Follow-up SLA", markdown }` entry to [playbook-docs.json](apps/mail/docs/playbook-docs.json) (markdown = body of `follow-up-sla.md`).
- [x] Dry-run: `pnpm tsx --env-file=../../.env src/scripts/seed-dashboard-doc.ts` (from `apps/server`) and confirm the new path is listed. (Script needs `--env-file` to load `DATABASE_URL`.)
- [x] Seed: `CONFIRM=1 SEED_DOCS_JSON=<one-entry.json> pnpm tsx --env-file=../../.env src/scripts/seed-dashboard-doc.ts`. Scoped to only the new doc via `SEED_DOCS_JSON` so the other 25 seeded docs (and any live edits to them) are not overwritten.

**Tests:**

- [x] Headless verification: seeded doc `cbb26ac5-3a61-47b0-889f-6b9227181f0a` confirmed in DB (13.8k md, Yjs state present, 7 `\`\`\`dashboard` fences); timeline block render-tested via jest. Browser walkthrough of `/brain → Files → "follow-up-sla"` left to the user.
- [x] `npx tsc --noEmit -p apps/mail/tsconfig.json` clean for the touched files (only the pre-existing repo-wide `@jest/globals` resolution warning remains, shared by 18 other tests).