From 9c7e3be984577a83ec2155b11ee71139317b5130 Mon Sep 17 00:00:00 2001 From: Mathias Date: Mon, 8 Jun 2026 13:11:41 +0200 Subject: [PATCH] docs(ux): add Stage-0 recency-bounded heuristic review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- docs/ux-review/UX-REVIEW-stage0-recency.md | 207 +++++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 docs/ux-review/UX-REVIEW-stage0-recency.md diff --git a/docs/ux-review/UX-REVIEW-stage0-recency.md b/docs/ux-review/UX-REVIEW-stage0-recency.md new file mode 100644 index 0000000..2647b06 --- /dev/null +++ b/docs/ux-review/UX-REVIEW-stage0-recency.md @@ -0,0 +1,207 @@ +# 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, 2–3 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.