18 KiB
Phase 6: Analyzer Feed (NEW) - Context
Gathered: 2026-05-03 (auto mode) Status: Ready for planning
## Phase BoundaryReplace 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 DecisionsFeed scope & ordering
- D-01: Source table is
analyzer_analysesfiltered tostatus = '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 theLEFT JOIN LATERAL ... ORDER BY analysis_version DESC LIMIT 1pattern inapp/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 DESCfor stability. Tie-breaker onidmakes pagination deterministic. - D-04: Apply the same
kiosk_settingscompany scoping helper Phase 4 uses (getMobileCompanyFilter()inapp/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 }. WhennextCursoris null, list is exhausted. Mirrors Phase 4MobileTicketListResponse.
Infinite scroll trigger (mirrors Phase 4)
- D-08:
IntersectionObserveron a sentinel<div ref={sentinelRef} />at end of list,rootMargin: '200px'. Guard against duplicate fetches: no-op ifloadingMore || !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
Cardwith 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
summaryfield (text-xs text-muted-foreground, 2-line clamp). Ifsummaryis 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.
- Line 1 (header): ticket number (mono
- 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_usedbooleans 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 areh-1.5 w-1.5 rounded-full, label between them istext-[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 astext-[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. Theidis 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
(
ArrowLeft→router.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-semiboldheading:- Summary — render
summaryfield (long-form text). Usewhitespace-pre-wrapto preserve paragraph breaks. If null, render "Summary not available." - Next Step — render
next_stepfield. Same null fallback. - Next Step Rationale — render
next_step_rationalefield. Same null fallback.
- Summary — render
- D-22: Footer: "View full analysis" link with
ExternalLinkicon pointing to the desktop analyzer at/analyzer/analysis/[id](existing route). Use the sameExternalLinkhint 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_analyses→tickets(fortitle) →companies(forcompany_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 includingsummary,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 viaimport type(Phase 3/4 precedent). - D-27: Detail page reuses the existing
PersistedAnalysistype fromlib/types/analyzer.ts— no parallel type.
Loading & error states
- D-28: Initial load: 5 row skeletons (
Skeletonfromcomponents/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
ExternalLinkhint pointing to/analyzer/tickets. Reusecomponents/ui/empty-state.tsxif its props fit; otherwise mirror its shape inline.
Typography & spacing (mirror Phase 4 UI-SPEC)
- D-32: Two font weights only —
font-normal(400) andfont-semibold(600). Nofont-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 byspace-y-3inside 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
AnalyzerFeedRowcomponent (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 atapp/mobile/analyzer/page.tsxis 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_settingscompany 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 andgetMobileCompanyFilter()(kiosk_settingsscoping helper at lines 3–26).app/api/mobile/finance/route.ts— additional pattern reference for/api/mobile/*shape and snake_case→camelCase transform.lib/types/analyzer.ts—PersistedAnalysistype used by the detail page;DeepAnalysisResponseconfirmssummary,next_step,next_step_rationaleshapes.migrations/069_create_analyzer_tables.sql—analyzer_analysesschema: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()fromlib/auth-utils.ts— auth gate for all/api/*routes.getMobileCompanyFilter()inline helper inapp/api/mobile/tickets/route.ts(lines 3–26) — apply identical scoping to the analyzer feed; do not bypass.- shadcn primitives:
Card,Badge,Skeleton,EmptyState,Button— all incomponents/ui/. lucide-reacticons (Sparkles, ArrowLeft, ExternalLink, ChevronRight, Loader2) — all in deps.IntersectionObserver— browser-native, no dep.relTime()helper inline inapp/mobile/tickets/page.tsx:29-37— keep the pattern; extract a shared helper only if a third caller appears.PersistedAnalysistype fromlib/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 typefrom 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 fromapp/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_analysestable — read-only access; the analyzer worker continues to write to it untouched.
</code_context>
## Specific Ideas- Mirror Phase 4's API style precisely: same
MobileTicketListResponseenvelope 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.
- 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
completerows 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