wulf-pulse/.planning/phases/06-analyzer-feed-new/06-CONTEXT.md

18 KiB
Raw Blame History

Phase 6: Analyzer Feed (NEW) - Context

Gathered: 2026-05-03 (auto mode) Status: Ready for planning

## Phase Boundary

Replace the placeholder app/mobile/analyzer/page.tsx (shipped in Phase 2) with the real read-only Analyzer feed. Build a most-recent-first list of completed AI ticket analyses, plus a phone-friendly per-analysis summary view at /mobile/analyzer/[id]. Add a new /api/mobile/analyzer/feed endpoint returning latest complete analyses with cursor pagination.

In scope: list page UI + per-row card (ticket #, title, summary one-liner, confidence badge, stage indicator, optional review flag) + new feed endpoint with cursor pagination + summary detail page reading from the existing /api/analyzer/analyses/[id] endpoint + "View full analysis" link out to desktop.

Out of scope: editing, re-run, prompt tuning, filter strip, search, cross-ticket aggregate views, push notifications, server-sent events, stale- analysis indicators (already on desktop). Read-only on mobile by design (REQUIREMENTS.md ANL-05; PROJECT.md Out of Scope).

## Implementation Decisions

Feed scope & ordering

  • D-01: Source table is analyzer_analyses filtered to status = 'complete'. Pending / running / failed rows are not shown in the mobile feed (a future phase can add a "Failed" filter if needed). Reason: ANL-01 says "most-recent-first stream of AI ticket analyses" — the user value is reading finished output.
  • D-02: Feed shows the latest completed analysis per ticket (latest analysis_version). Mirrors the LEFT JOIN LATERAL ... ORDER BY analysis_version DESC LIMIT 1 pattern in app/api/analyzer/tickets/route.ts. Reason: a ticket re-analyzed three times shouldn't appear three times in the manager's stream.
  • D-03: Ordering: completed_at DESC NULLS LAST, id DESC for stability. Tie-breaker on id makes pagination deterministic.
  • D-04: Apply the same kiosk_settings company scoping helper Phase 4 uses (getMobileCompanyFilter() in app/api/mobile/tickets/route.ts:3-26). The mobile feed must not surface analyses from out-of-scope companies. Move the helper to a shared util only if a third caller appears; otherwise duplicate inline (keep this phase's diff small).

Pagination — cursor model (mirrors Phase 4)

  • D-05: Cursor-based infinite scroll, page size 25, capped server-side at 25. Reason: parity with Phase 4 (TICK-05); ANL spec is silent on page size, so reuse the existing mobile mental model.
  • D-06: Cursor encoding: base64 of { completed_at: ISO string, id: uuid string }. Server decodes and applies (completed_at, id) < (cursor.completed_at, cursor.id) predicate. Inline encoder/decoder in the route file unless a second consumer appears.
  • D-07: API response shape: { analyses: AnalyzerFeedRow[], nextCursor: string | null, hasMore: boolean }. When nextCursor is null, list is exhausted. Mirrors Phase 4 MobileTicketListResponse.

Infinite scroll trigger (mirrors Phase 4)

  • D-08: IntersectionObserver on a sentinel <div ref={sentinelRef} /> at end of list, rootMargin: '200px'. Guard against duplicate fetches: no-op if loadingMore || !hasMore.
  • D-09: Always render a focusable "Load more" button below the sentinel as the accessibility fallback (parity with Phase 4 D-14 / TICK-06). Hide only when !hasMore.

Row presentation

  • D-10: Each row is a Card with two stacked lines plus a footer pip row:
    • Line 1 (header): ticket number (mono text-xs font-semibold) on the left, time-ago (text-[10px] text-muted-foreground) on the right.
    • Line 2 (title): ticket title (text-sm font-semibold, 1-line truncate).
    • Line 3 (summary): analyzer's summary field (text-xs text-muted-foreground, 2-line clamp). If summary is null, render "—".
    • Footer: stage pip row on the left + confidence badge on the right; a small "Review" pill renders inline when needs_human_review = true.
  • D-11: No left-edge color stripe (no priority taxonomy in this domain — do not transplant Phase 4's border-l-4). The Card itself is the surface.
  • D-12: Single-tap navigates to /mobile/analyzer/[id] (segment form, shareable URL). Reason: Phase 8 ENG-06 prefers segment URLs and this matches.

Stage indicator

  • D-13: Stage pip row renders three small dots labeled "Triage → Analyze → Deep Review", driven by haiku_used, sonnet_used, opus_used booleans on the row. Filled (primary tone) when used, muted/outline when unused. Pure CSS — no animation, no library.
  • D-14: Compact horizontal layout: flex items-center gap-1.5, pips are h-1.5 w-1.5 rounded-full, label between them is text-[10px] text-muted-foreground. Optional caret separator between pips for clarity.

Confidence badge

  • D-15: Buckets and tones (matches the spirit of the desktop analyzer treatment without coupling):
    • confidence_score >= 0.85 → label "High", green tone (e.g. bg-green-500/10 text-green-700).
    • 0.65 <= confidence_score < 0.85 → label "Medium", amber tone.
    • confidence_score < 0.65 → label "Low", slate or destructive tone.
    • confidence_score IS NULL → no badge (stage incomplete or absent).
  • D-16: Use shadcn Badge (components/ui/badge.tsx) with a small variant. Render as text-[10px] so it sits flush with the footer row.

Needs-review flag

  • D-17: When needs_human_review = true, render a small "Review" pill beside the confidence badge (destructive tone). When false, render nothing (don't take up footer real estate). Reason: the spec doesn't require it, but managers triaging the feed will value an at-a-glance flag for analyses the pipeline already marked uncertain.

Summary view (/mobile/analyzer/[id])

  • D-18: New segment route app/mobile/analyzer/[id]/page.tsx. The id is the analyzer_analyses UUID. Reason: ANL-03 + ANL-04 require a mobile summary surface; segment URL is shareable.
  • D-19: Header row (in-page, below the shell HeaderBar): back chevron (ArrowLeftrouter.back()) + breadcrumb "Analyzer / #{ticketNumber}". Mirrors Phase 4 D-18 detail header pattern.
  • D-20: Identity block: ticket number (mono small), title (semibold), company name (muted small), completed-at relative time, then the same stage indicator + confidence badge from the row.
  • D-21: Three content sections, in order, each with a text-sm font-semibold heading:
    1. Summary — render summary field (long-form text). Use whitespace-pre-wrap to preserve paragraph breaks. If null, render "Summary not available."
    2. Next Step — render next_step field. Same null fallback.
    3. Next Step Rationale — render next_step_rationale field. Same null fallback.
  • D-22: Footer: "View full analysis" link with ExternalLink icon pointing to the desktop analyzer at /analyzer/analysis/[id] (existing route). Use the same ExternalLink hint convention from Phase 2's More drawer (DRAWER-04). The link uses a relative URL (no domain) — Better Auth + middleware handles desktop/mobile routing the same.
  • D-23: Read-only — no edit, re-run, cancel, prompt-tuning, or share controls (ANL-05). Don't render Buttons that suggest actions are available.

Data layer

  • D-24: New endpoint: app/api/mobile/analyzer/feed/route.ts — GET handler, requireAuth(), returns the cursor-paginated list. Joins: analyzer_analysestickets (for title) → companies (for company_name). Selects only the columns the row card needs to keep payload small.
  • D-25: Detail page reuses existing GET /api/analyzer/analyses/[id] (already returns the full PersistedAnalysis row including summary, next_step, next_step_rationale, confidence_score, haiku_used, sonnet_used, opus_used, needs_human_review). No new detail endpoint.
  • D-26: Export the row interface (e.g., AnalyzerFeedRow) and response type (AnalyzerFeedResponse) from the route file. Page imports them via import type (Phase 3/4 precedent).
  • D-27: Detail page reuses the existing PersistedAnalysis type from lib/types/analyzer.ts — no parallel type.

Loading & error states

  • D-28: Initial load: 5 row skeletons (Skeleton from components/ui/skeleton.tsx). Mirrors Phase 4 D-21.
  • D-29: Subsequent infinite-scroll fetch: small inline spinner above the Load more button.
  • D-30: Fetch error: toast.error() (sonner) + the Load more button flips to "Retry". Mirrors Phase 4 / Phase 5 D-18 patterns.

Empty state

  • D-31: When the feed returns zero rows on first page, render a centered card: heading "No analyses yet", body "Completed AI ticket analyses will appear here.", and a "Open desktop Analyzer" link with ExternalLink hint pointing to /analyzer/tickets. Reuse components/ui/empty-state.tsx if its props fit; otherwise mirror its shape inline.

Typography & spacing (mirror Phase 4 UI-SPEC)

  • D-32: Two font weights only — font-normal (400) and font-semibold (600). No font-medium. Reason: consistency with Phase 4/5.
  • D-33: Three sizes — text-sm (14px) primary, text-xs (12px) secondary/labels, text-[10px] for ticket numbers, badges, time-ago, pip labels.
  • D-34: Page container: px-4 py-4 space-y-4. Rows separated by space-y-3 inside the list. No horizontal overflow at 360px viewport.
  • D-35: <h1>Analyzer</h1> renders in the page body (text-base font-semibold), not in the shell HeaderBar. Phase 2 spec says "no page title in header".

What NOT to change

  • D-36: Existing desktop analyzer routes (/analyzer/*, /api/analyzer/*) are unchanged. The mobile feed only adds one endpoint and replaces the placeholder mobile page.
  • D-37: No edits to lib/services/analyzer/** (pipeline, persistence, worker). The mobile feed is purely a read view.
  • D-38: No new state libraries. 'use client' + useState + useEffect + fetch('/api/mobile/analyzer/feed') (CLAUDE.md rule).
  • D-39: No Zod validation in the new mobile route handler — match surrounding /api/mobile/* style (CLAUDE.md: "no Zod in API routes unless required").

Claude's Discretion

  • Exact pip styling and spacing (match other mobile components' density)
  • Whether to extract a small AnalyzerFeedRow component (probably yes for DRY, internal helper, no public export)
  • Skeleton visual pattern
  • Whether the back chevron + breadcrumb extracts into a shared MobileDetailHeader (Phase 4 has the same shape — DRY only if the diff is trivial; otherwise mirror inline)
  • Whether to memoize row component (only if perf measurement warrants)

<canonical_refs>

Canonical References

Downstream agents MUST read these before planning or implementing.

Phase spec

  • docs/superpowers/specs/2026-05-03-mobile-shell-design.md §6.4 (Analyzer) — primary scope. §3.1 confirms Analyzer is on the bottom bar; §3.2 confirms read-only.
  • .planning/REQUIREMENTS.md (ANL-01 through ANL-06) — locked acceptance criteria.

Project conventions

  • CLAUDE.md — Pulse stack rules (no SWR/react-query, no ORM, fetch-from- clients pattern), /mobile/* boundary, kebab-case files, no Zod in API routes.
  • DESIGN.md — design tokens, navigation IA, component conventions.
  • ARCHITECTURE.md — analyzer pipeline overview (read for context; pipeline itself is unchanged this phase).

Prior phase contracts (patterns to mirror)

  • .planning/phases/02-mobile-shell-more-drawer/02-CONTEXT.md — shell decisions; the analyzer pages dock under this layout. Confirms the Analyzer placeholder file at app/mobile/analyzer/page.tsx is owned by this phase (Phase 2 D-29).
  • .planning/phases/04-tickets-restyle/04-CONTEXT.md — cursor pagination model (D-08..14), API response envelope shape, IntersectionObserver pattern, detail-header reskin pattern (D-18), kiosk_settings company scoping helper.
  • .planning/phases/05-finance-restyle/05-CONTEXT.md — Card/typography scale (2 weights, 3 sizes); inline error/Retry pattern (D-18); empty state convention (D-19).

Existing code (entry points)

  • app/mobile/analyzer/page.tsx — current placeholder, replaced by this phase. Read first to confirm scope.
  • app/api/analyzer/tickets/route.ts — desktop ticket-feed analog. Pattern reference for the latest-analysis-per-ticket join (LEFT JOIN LATERAL ... ORDER BY analysis_version DESC LIMIT 1).
  • app/api/analyzer/analyses/[id]/route.ts — existing detail endpoint, reused as-is by the new mobile detail page.
  • app/api/mobile/tickets/route.ts — pattern reference for cursor pagination implementation and getMobileCompanyFilter() (kiosk_settings scoping helper at lines 326).
  • app/api/mobile/finance/route.ts — additional pattern reference for /api/mobile/* shape and snake_case→camelCase transform.
  • lib/types/analyzer.tsPersistedAnalysis type used by the detail page; DeepAnalysisResponse confirms summary, next_step, next_step_rationale shapes.
  • migrations/069_create_analyzer_tables.sqlanalyzer_analyses schema: summary, next_step, next_step_rationale, confidence_score, needs_human_review, haiku_used, sonnet_used, opus_used, completed_at, status, analysis_version. Reference for column names and types when writing the feed query.
  • components/ui/{card,badge,skeleton,empty-state}.tsx — shadcn primitives.
  • components/mobile/TicketRowSkeleton.tsx, components/mobile/TicketFilterStrip.tsx, components/mobile/FinanceRow.tsx — Phase 4/5 mobile component patterns to mirror for the analyzer row + skeleton.

</canonical_refs>

<code_context>

Existing Code Insights

Reusable Assets

  • requireAuth() from lib/auth-utils.ts — auth gate for all /api/* routes.
  • getMobileCompanyFilter() inline helper in app/api/mobile/tickets/route.ts (lines 326) — apply identical scoping to the analyzer feed; do not bypass.
  • shadcn primitives: Card, Badge, Skeleton, EmptyState, Button — all in components/ui/.
  • lucide-react icons (Sparkles, ArrowLeft, ExternalLink, ChevronRight, Loader2) — all in deps.
  • IntersectionObserver — browser-native, no dep.
  • relTime() helper inline in app/mobile/tickets/page.tsx:29-37 — keep the pattern; extract a shared helper only if a third caller appears.
  • PersistedAnalysis type from lib/types/analyzer.ts — full row shape for the detail page.

Established Patterns

  • Mobile pages are 'use client' + useState + useEffect + fetch('/api/mobile/...'). NO SWR, NO react-query. (CLAUDE.md.)
  • API routes use postgresClient.query() with parameterized SQL; manual snake_case → camelCase transform (no ORM).
  • import type from API route files for response shape (Phase 3/4 precedent).
  • TypeScript interfaces exported from the route file alongside the handler. Pages consume via import type.
  • Latest-version-per-ticket: LEFT JOIN LATERAL (SELECT ... FROM analyzer_analyses WHERE ticket_number = ? ORDER BY analysis_version DESC LIMIT 1) pattern from app/api/analyzer/tickets/route.ts:319-328.

Integration Points

  • app/mobile/layout.tsx (Phase 2) renders the shell — analyzer pages dock inside it; no layout changes needed.
  • BottomNav active-tab detection uses pathname.startsWith('/mobile/analyzer') — already correct (Phase 2). The new /mobile/analyzer/[id] segment highlights Analyzer in the bottom bar — verify on first run.
  • Existing desktop analyzer URL /analyzer/analysis/[id] is the canonical "view full analysis" target. Better Auth + middleware handles auth identically for /analyzer/* and /mobile/analyzer/*.
  • analyzer_analyses table — read-only access; the analyzer worker continues to write to it untouched.

</code_context>

## Specific Ideas
  • Mirror Phase 4's API style precisely: same MobileTicketListResponse envelope shape ({ analyses, nextCursor, hasMore }), same cursor encoding (base64 JSON, opaque to client), same IntersectionObserver
    • Load more pattern. The manager's mental model from Tickets carries over to Analyzer with zero learning cost.
  • Stage pip row should feel like a progress indicator, not a status badge — three small dots that visually suggest "the analysis got this far". Don't over-design with arrows or labels; the column header in the desktop analyzer already trains users on the order.
  • "Review" pill should look the same as a destructive Badge variant from shadcn — single token "Review", no icon, sits inline with the confidence badge.
  • The detail page should feel calm and quick to read — three labelled sections separated by clear vertical space, not a wall of text. The manager opens this on the go to make a decision, not to study a report.
## Deferred Ideas
  • Filter strip on the feed (filter by needs_human_review, by confidence bucket, by date range, by company) — not in ANL scope; a later phase can add a Collapsible filter row mirroring Phase 4 if managers request it.
  • Search across analyses by ticket number / title / summary text — not in ANL scope.
  • Re-run / cancel / prompt-tuning controls on mobile — explicit Out-of-Scope (REQUIREMENTS.md ANL-05; PROJECT.md Out of Scope).
  • Showing failed or pending analyses in the feed — only complete rows in v1; failed handling is a future phase.
  • Stale-analysis indicator (when tickets.last_activity_date > analyses.completed_at) — already surfaced on desktop tickets list; not needed on the mobile feed since it's an analyses-first view.
  • Server-sent events / live feed updates for newly-completed analyses — no streaming this iteration. A pull-to-refresh affordance can come later.
  • Push notifications for completed analyses — out of scope (no service worker this milestone).
  • Cross-ticket aggregate views (themes, repeat issues) — those are desktop reports (/analyzer/reports/*), not in scope.
  • IT Glue references / ITGlue link list on mobile — viewable on desktop via "View full analysis"; mobile stays focused on Summary / Next Step / Rationale per ANL-03.
  • Cost/token usage display on mobile — debug observability; not manager-facing.

Phase: 06-analyzer-feed-new Context gathered: 2026-05-03