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>
12 KiB
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
- #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.
- 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.