Files
tapir/docs/ux-review/UX-REVIEW-stage0-recency.md
T
mathiasandClaude Opus 4.8 9c7e3be984 docs(ux): add Stage-0 recency-bounded heuristic review
Prioritized UX findings for the product as it actually is — sparse feed,
respected caption rate limit, recency-bounded auto-summarize (incoming),
single-user. 15 findings, NOW/LATER tagged. P0s target the first-contact
return-cliff that the Stage-0 gate depends on.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 13:11:41 +02:00

12 KiB
Raw Blame History

Tapir — Heuristic Review (Stage-0, sparse + recency-bounded)

Findings document, not a build spec. The maintainer filters; a spec follows separately. Reviewed against VISION.md (Stage-0 gate), docs/ui-spec.md, internal/web/views.templ + view.go, the prior UX-REVIEW.md pass, and the current screenshots. Written for the product as it actually is: ~283 discovered, ~15 summarized, ~256 in queue behind a respected per-IP caption rate limit, ~12 no-captions; single-user (maintainer) with friends pending; recency- bounded auto-summarize about to ship (auto = recent ~7d, older browsable + manual on demand).

Reviewer stance

The Stage-0 gate is return usage. So every finding is judged by one question: does this make the maintainer (or a friend) come back to an honestly-sparse feed? The visual layer is already decent — dark mode, cards, the constrained reader were all fixed in the prior pass. The open problems are expectation-setting, honesty-of-scale, and the recency feed — not pixels.

The single biggest risk: a new user connects, sees 15 summarized · 256 fetching soon, nothing visibly moves, and never returns. The whole gate dies at that moment. Most P0s below attack that one moment.

Tagging

  • [NOW] — improves the product as it is today (sparse, recency-bounded, single-user). Ships in the upcoming bundle.
  • [LATER] — improves the product we hope it becomes (abundance, engagement, multiple users). Valuable but premature until real usage validates the core loop.

Severity: 🔴 breaks the core loop · 🟠 hurts it · 🟡 noticeable · 🔵 polish.


P0 — fix before/with the recency ship

1. 🔴 Empty-connected state tells the user to run a CLI command they can't run — [NOW]

Problem. After connecting YouTube, the empty list says: "Run tapir run to discover your subscriptions." A friend on the web has no shell. And post-ADR-018 discovery is an in-process scheduled loop — so the instruction is wrong even for the maintainer. It is the first thing a newly-onboarded user sees, and it is an impossible, stale instruction. Principle. Match between system and the real world; help users recognize, not be blocked (Nielsen #1, #2, #9). Proposal. Replace with a passive, honest "we're working" state: "Your account is connected. Tapir is finding your subscriptions and fetching captions — summaries appear here gradually. Check back later." No command. No imperative the user can't satisfy. Evidence. views.templ summaryList.empty-connected.

2. 🔴 No expectation set for gradual fill — the return loop breaks at the cliff — [NOW]

Problem. Captions are rate-limited by design; the backlog trickles over days. Nothing tells the user this. A first visit shows few/no summaries and no "come back" framing. The Stage-0 gate is literally about returns, and the product never asks for one or explains why patience is warranted. Principle. Visibility of system status (#1). And: the gate can't be cleared if the UX doesn't survive first contact. Proposal. One honest sentence near the pipeline bar / empty state: "Tapir fetches captions slowly on purpose, to respect YouTube's limits. New summaries land gradually — usually best to check back tomorrow." Turns confusing emptiness into intentional design. Highest-leverage change in the review.

3. 🟠 "256 fetching soon" overstates imminence — a lie of scale — [NOW]

Problem. The pipeline bar and rate-limited cards both say "fetching soon." For 256 items behind a per-IP throttle, "soon" is false — they trickle over days/weeks. Exactly the abundance- implying language the mandate forbids, inverted: it makes the queue look imminent. Principle. Honesty of scarcity (project mandate); #1. Proposal. Relabel by scale. Bar: 15 ready · 256 in queue · 12 no captions. Card state: "In queue" / "Waiting its turn", not "Fetching soon…". Reserve "soon" for items actually next. Evidence. views.templ pipelineBar; card State 4.

4. 🟠 Recency boundary is invisible in the feed — [NOW, ships with recency]

Problem. Once auto-summarize is bounded to ~7d, a 6-month-old pending video and a 2-day-old pending video render identically ("Not summarized" + button). But only one is in the auto path; the other will never process unless clicked. The user can't tell "be patient, this is coming" from "this is yours to trigger or ignore." Principle. Visibility of system status; predictability (#1). Proposal. Bucket the list into two sections: Recent (auto, will fill itself) and Older — browse / summarize on demand. Card status language should encode which side of the line it's on, not the pipeline internals. Central IA decision of the recency change.

5. 🟠 The un-summarized mass buries the ~15 readable summaries — [NOW]

Problem. ~268 of 283 cards are not readable (pending / queued / no-captions). Summarized-first ordering helps, but the page is still 95% noise below the fold. "Attention is the scarce resource" is the product's own principle — and the default view violates it. Principle. Aesthetic/minimalist design; signal-to-noise (#8). Proposal. Default view = readable summaries + the Recent bucket. Collapse the older un-summarized mass behind "Show 256 older un-summarized videos." Collapse the 12 no-caption videos into a single line: "12 videos have no captions" (terminal, never readable — they don't deserve 12 full cards).


P1 — high, near-term

6. 🟠 "Summarize now" overpromises against the rate gate — [NOW]

Problem. Button says "Summarize now" (title: "Summarize this one now"). Backend queues it behind the shared per-IP gate. Click 10 older videos and they all sit at "Fetching soon…". The verb sells immediacy the system can't honor. Principle. Honesty; match system/reality (#1). Proposal. Drop "now" → "Summarize". On click the card should honestly become "Queued" (it already can). Optionally show queue position once the queue is real. Don't engineer the gate harder — just stop the verb from lying. Evidence. views.templ card States 4 & 5; handlers.go handleRetryNow, handleRequestSummarize.

7. 🟠 Welcome-page copy is stale and misleading post-ADR-019 — [NOW]

Problem. Landing sub-copy: "If you have an invite link, it will set up your account automatically." Invites moved to Authentik (ADR-019); Tapir no longer handles invite links. The "Get Started" button goes straight to OIDC. The copy promises a flow that no longer exists. Principle. Match between system and reality (#2); honesty. Proposal. Rewrite: "Tapir is invite-only right now. If you've been invited, sign in below." Single CTA. Also set the gradual-fill expectation here (ties to #2) so it lands before the wall, not after. Evidence. views.templ WelcomePage.welcome-sub.

8. 🟡 "Queued · waiting for the next run" leaks system jargon — [NOW]

Problem. "the next run" exposes the discovery-loop concept; a user doesn't know what a "run" is. Principle. Speak the user's language (#2). Proposal. "Queued — summarizing shortly." Hide the scheduler. Evidence. views.templ card State 3.

9. 🟡 Filters render before there's anything to filter — [NOW]

Problem. The fresh/empty state shows the full Channel/From/To/Filter bar above "Connect YouTube." Power tooling stacked on top of the one action that matters. Principle. Progressive disclosure; minimalist design (#8). Proposal. Hide the filter bar when there are 0 rows (and arguably below ~20). Show the connect CTA alone. Evidence. fixes/03-empty-fresh.png; ListPage renders filterForm unconditionally.

10. 🟡 Date-range filters are dead weight at this scale — [NOW] demote / [LATER] rebuild

Problem. From/To date pickers + a free-text exact-match Channel field are corpus-scale tools. With 15 summaries they're noise; channel-as-freetext is unguessable. (Confirm the prior review's #7 is resolved — that Channel shows a real channel name, not the provider string; seeded screenshots suggest it is, live data may differ.) Principle. Match tool to task; minimalist design. Proposal. [NOW]: reduce to the "Summarized only" toggle (+ maybe channel chips derived from present rows). Drop date pickers until the corpus justifies them. [LATER]: real channel facets + search when there's volume.


P2 — reading experience & polish

11. 🟡 Detail leads with Summary; the attention-saving payload (Takeaways) is last — [NOW]

Problem. Product promise is "decide what's worth your time." The element that answers that — Takeaways / verdict — sits at the bottom. The user reads a full summary to reach the point. Principle. Lead with the user's actual job-to-be-done. Proposal. Reorder or add a one-line TL;DR/verdict at top. Takeaways → Highlights → Summary, or a "Worth watching?" lede. Data already exists; a reorder, not new machinery. Evidence. views.templ DetailPage.

12. 🔵 No "back to Summaries" on detail — [NOW]

Problem. Only the brand returns home, losing any filter context. Proposal. Explicit "← Summaries" link on the detail page.

13. 🔵 Auto/manual toggle copy will be wrong after recency — [NOW, with recency]

Problem. Account copy: "Automatic summarizes every new video as it is discovered." Becomes false once auto is bounded to ~7d. Proposal. "Automatic summarizes new videos from the last ~7 days. Older videos stay browsable — summarize them on demand." Evidence. views.templ AccountPage summarization section.

14. 🔵 Register step asks acceptance of nonexistent terms — [NOW]

Problem. "I accept the terms of use" — no terms linked. For a friends-only tool, ceremony accepting nothing. Proposal. Either link real terms or drop the checkbox at this stage. Evidence. views.templ RegisterPage.

15. 🔵 watched/skipped mutual exclusivity unsignaled — [NOW] low

Problem. Three independent-looking buttons; watched↔skipped are exclusive (carryover from prior review #14). Proposal. Segmented control for watched/skipped; keep Saved separate.


[LATER] — premature until the core loop is validated

(abundance / engagement / multiple users)

  • Return-nudges (digest email / push). 🔴 Caution, not just defer. A notification that drives returns contaminates the exact signal the gate measures — VISION wants unprompted returns and admits it can't distinguish prompted from organic. Building a nudge now poisons the experiment. Defer until after the gate reads. [LATER]
  • Full-text search across summaries — needs volume to matter. [LATER]
  • Channel facets / saved filters / sorting — corpus-scale tooling (#10). [LATER]
  • Read/unread + "new since last visit" — genuinely helps returns, but only meaningful once there is throughput to be "new." [LATER]
  • Saved/queue view, collections — engagement surface; no payoff at 15 items. [LATER]
  • Richer card previews (top-takeaway as preview, 23 lines) — triage aid that only pays off with many cards to triage. [LATER]
  • Backlog progress heartbeat ("N summarized this week", queue burn-down) — rewards returning, but needs real throughput to show motion; a static "last updated X ago" is the only [NOW]-worthy slice. [LATER]
  • Onboarding tour / multi-step welcome — over-built for one user + a few friends. [LATER]

What's already right (don't regress)

Pipeline-bar concept, the honest "No transcript available" terminal state, the queued/waiting spinner instead of a fake-imminent one, the Unavailable-channels surface, the constrained-width reader, dark mode, the account danger-zone behind a disclosure. The honesty instincts are present — the P0s are about making that honesty legible and correctly-scaled, not adding it.

Two judgment calls to settle first

  1. #4 (Recent/Older split) and #5 (collapse the older mass) are one decision viewed twice. Settle the recency-feed IA once and both fall out.
  2. The return-nudge caution ([LATER] list, item 1) is the one to put in writing now — before someone "helpfully" ships an email digest to juice the gate and destroys the signal.