feat(analyzer): browse-tickets page + analysis-view typography

- /analyzer/tickets — period chips (today/yesterday/this+last
  week/30d/60d/all), client + issue-type Selects, debounced search,
  per-row Analyze/Re-analyze plus View shortcut when an analysis
  already exists.
- API: /api/analyzer/tickets/list (period/companyId/issueType/search,
  paginated via COUNT(*) OVER) and /filter-options (companies that
  actually have tickets, active issue types).
- ProseText helper in analysis-view splits on blank lines and renders
  each chunk with leading-7 — Summary, Next Step, rationale, and
  Post-Resolution now have proper paragraph rhythm. Next Step card
  re-styled with bg-primary/5 tint, ArrowRight icon, and an indented
  rationale block.
- Top-level "Analyzer" nav menu (Browse Tickets + Needs Review).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
lorentz 2026-04-29 13:25:16 -04:00
parent 966376e6b6
commit b20c94ea1a
6 changed files with 879 additions and 19 deletions

View file

@ -414,6 +414,85 @@ This file is updated after each phase ships.
---
## Phase 9 — Ticket browser + analysis-view formatting
Reactive to first-use feedback: the analysis page Summary / Next Step
text was bare and dense, and there was no way to discover tickets to
analyze without typing the URL.
**Delivered**
- `<ProseText>` helper inside `analysis-view.tsx` — splits text on blank
lines, renders each chunk as a separate `<p>` with `leading-7` and
`whitespace-pre-line`. Applied to Summary, Next Step, Next Step
rationale, and Post-Resolution Analysis. Single-paragraph text still
renders cleanly.
- Summary + Post-Resolution headers got an uppercase tracking-wide
treatment to act as section dividers, and body text bumped to
`text-base text-foreground` so it reads as a finding rather than a
caption.
- Next Step card now has a subtle `bg-primary/5` tint, an `ArrowRight`
icon next to "Recommended Next Step", a stronger separator before
the rationale collapsible, and the rationale itself renders in a
bordered indented block.
- New `/analyzer/tickets` browse page — pill-style period chips
(Today, Yesterday, This week, Last week, Last 30/60 days, All time),
a client (company) Select, an issue-type Select, and a debounced
free-text search across ticket_number/title. Compact table with
per-row Analyze/Re-analyze button (reusing `<AnalyzeButton>`) and a
"View" button shortcut to the existing analysis when one is
recorded. Active-filter count + clear-all in the filter card header.
- New API `GET /api/analyzer/tickets/list` — filters by `period`
(computed in Postgres against `last_activity_date`), `companyId`,
`issueType`, `search`. Returns 50 rows + total via `COUNT(*) OVER ()`,
plus `latestAnalysisId` from a LATERAL join into `analyzer_analyses`.
- New API `GET /api/analyzer/tickets/filter-options` — companies that
have at least one non-deleted ticket (drops dormant accounts) +
active issue types ordered by `sort_order, label`.
- Top-level "Analyzer" nav menu added to `app-navigation.tsx`, with
"Browse Tickets" + "Needs Review". Earlier phases left this off
intentionally; this phase opts in.
**Decisions worth flagging**
- **Period filters on `last_activity_date`, not `create_date`.**
"Today" surfaces tickets that had activity today (new tickets,
re-opened, status churn) — much more useful for an analyzer-driven
triage flow than tickets created today. A new ticket created today
also has activity today, so we don't lose those.
- **Period math runs in Postgres via `date_trunc('day', NOW())` etc.**
Database server-clock = app-process clock for an internal Docker
stack, so naive timestamps and naive `NOW()` agree. If users
complain about edge-of-day drift, swap to
`NOW() AT TIME ZONE 'America/New_York'` — Pulse's primary user base.
- **Default period is `last_30d`.** "All time" pulls many thousands of
rows; defaulting wide-open hurts first-page latency. 30 days hits
~7K rows in our DB, paginates cleanly.
- **Per-row analyze button reuses `<AnalyzeButton>` directly.** Each
row gets its own component instance — no shared state, the running
state lives per-button. The button navigates on completion, which
feels right: click Analyze, watch the stages, land on the analysis
page.
- **`force=true` is set automatically on tickets that already have an
analysis.** Re-analyze should re-run, not short-circuit to the
cached row. The "View" button covers the cached path.
- **No Linear/JIRA-style multi-select filters.** Single-value Selects
are simpler and match the rest of Pulse.
**Deliberately left out**
- **No saved views.** A filter URL is shareable, but there's no
bookmark / saved-view UX. Add when someone asks.
- **No `latest_analysis_status` exposure.** A failed analysis doesn't
show up — the LATERAL join filters by `status='complete'`. So a
ticket whose only analysis failed looks like an un-analyzed ticket.
Acceptable: re-running is the intended action there anyway.
- **Search is `ILIKE '%...%'`.** No tsvector / trigram index. 7K-row
scans are sub-100ms in this DB; if the corpus grows past low six
digits, swap in `pg_trgm`.
---
## Status after each phase
| Phase | Tests | tsc | Notes |
@ -426,3 +505,4 @@ This file is updated after each phase ships.
| 6 | 128 | clean | frontend (no FE tests) |
| 7 | 128 | clean | share email via existing SMTP transport |
| 8 | 128 | clean | operator runbook + README link |
| 9 | 128 | clean | browse page + analysis-view formatting + nav entry |