Observify

User Tests · latered
← all user tests
USER TEST PLAN

latered

Derived from: commit 4c144fc (master), 2026-08-07, against a local npm run dev

0/75 checked · 0 pass · 0 fail

Flows

check each step and final check
F1

First visit: the pitch, then claim your desk

Proves the funnel entry a brand-new visitor actually sees, and the identity gate under it — which is one step longer than the project's own DOGFOOD.md currently documents.

Persona / precondition: a browser with no latered IndexedDB database yet (fresh profile, private window, or a deleted DB) — first-ever visit.

0/7

Run cd ~/Projects/latered/app && npm install && npm run dev -- --port 5174, then open
http://localhost:5174. Expect: a landing/funnel page, NOT the create-profile form
yet — tagline "Put it off. Properly.", the lede "A to-do list that believes in
tomorrow...", three proof cards ("Tomorrow, in one tap", "The Guilt Meter keeps score",
"Every task has a page"), and the footnote "And yes — the day ends at 4 am here, like
it does everywhere else."

Click Claim your desk (the landing page's button). Expect: the actual
create-profile card replaces it: heading "Claim your desk.", the line "One profile, on
this device only. A passcode locks the app; your items never leave your machine.", and
three fields — "What should we call you?", "Passcode" (placeholder "At least 4
characters"), "Same passcode, once more" (placeholder "Repeat it").

Leave the name blank, type any passcode and matching confirm, click Start putting
things off
. Expect: the error "A name to greet you by — any name works." and no
profile is created.

Fill a name, type a 3-character passcode (with matching confirm), submit. Expect:
"Give the passcode at least 4 characters."

Fill a name, a valid 4+ character passcode, and a DIFFERENT value in the confirm field,
submit. Expect: "The passcodes do not match. Try them again."

Fill all three correctly (matching passcodes, 4+ chars) and submit. Expect: you land
straight on the Today view — no email verification step, because there is no server at
all (the passcode only gates this browser's local storage).

Final check: reload the page in the SAME tab. Expect: you are not asked to sign in
again — the app opens straight to Today (the unlock lives in sessionStorage, which
survives a same-tab reload; F9's final check covers what happens when the tab is closed).

F2

First-login product tour (9 stops)

Proves the guided tour a first-time user gets automatically, and that it never repeats uninvited.

Persona / precondition: F1 just completed for the first time this session — the tour should be opening on its own right now.

0/6

Observe what happens immediately after F1's last step. Expect: the tour opens by
itself, spotlighting the brand mark with title "Meet latered." and body starting "A
to-do list that respects the fine art of not doing...".

Click through all 9 stops with Next. Expect: exactly these 9 titles in order —
"Meet latered.", "Today is the whole game.", "Add something to neglect.", "The Guilt
Meter keeps score.", "The day ends at 4 am.", "All items, one collection.", "Every task
has a page.", "Pro mode, when you want more.", "Now neglect responsibly." — the middle
stops jump you across routes (Today → All items → Settings) and the last brings you
back to Today.

Start the tour again (Settings, see step 5) and press Esc partway through.
Expect: it closes immediately, regardless of which of the 9 stops you're on.

Reload the page. Expect: the tour does NOT auto-open a second time — you land
straight on Today (it only auto-runs "on the FIRST login, and for anyone who never
finished it", per the source comment).

Go to Settings, find "The tour" card ("Nine stops through everything the desk does
— the same walk that ran on your first login."), click Replay the tour. Expect:
the same 9-stop tour reopens on demand.

Final check: finish or Esc out of a replay and confirm you land back on whichever
route you started the replay from, fully interactive (no leftover overlay).

F3

Today: the signature push

Proves the one-tap "push to tomorrow" primitive and its undo — the commission's headline mechanic (SPEC §2.1).

Persona / precondition: signed in, on Today, with at least one open item at Guilt 0 / Pushes 0 (a freshly created item, or run F5's "Set up a demo room" first if Today is empty).

0/6

Note the item's row meta line, which reads "Guilt {n} · Pushes {n}". Expect: a
freshly created item reads "Guilt 0 · Pushes 0".

Click → Tomorrow on that item. Expect: the row animates out (skip judging the
animation itself if your OS has "reduce motion" on — see F10 step 3), a toast appears
at the bottom reading exactly "{title}" is tomorrow's problem now. with an Undo
action, and the row disappears from Today.

Click Undo in the toast before it expires. Expect: the item reappears on Today
at Guilt 0 / Pushes 0 again — Undo fully reverts the push, it does not just re-add the
row.

Push the SAME item again and this time let the toast run its full ~6 seconds without
touching it. Expect: the toast disappears on its own and the push stands — because
step 3's Undo reverted pushCount to 0, this is push #1 again, so Guilt should read 6
(not 12) and Pushes should read 1.

Click Done on a different open item. Expect: it animates out of Today; opening
All items with "Show done" toggled on (F7) shows it struck through with Guilt reset to
0.

Final check: open All items and confirm the item pushed in step 4 shows a scheduled
date of tomorrow — it is deferred, not deleted; it will reappear on Today once the day
rolls over.

F4

Multi-day push and the guilt math

Proves Layer B of the engine — "Later…" — and that distance costs strictly more than a single tap, against the documented formula in SPEC §2.3.

Persona / precondition: Today, one open item at Guilt 0 / Pushes 0 (the math below only lines up starting from a fresh item).

0/7

Click Later… on the item. Expect: a panel opens labeled "Push further (costs
escalate)" with +3 days, Next week, a date field, and Cancel.

Click +3 days. Expect: the toast reads "{title}" pushed 3 days out.; per the
formula round(perPush × (1 + 0.5×(days-1))) with this being push #1
(perPush = 6), the guilt added is round(6 × (1 + 0.5×2)) = round(6×2) = 12 — the row
should now read "Guilt 12 · Pushes 1".

Click Later… again, then Next week (+7 days, now push #2 so perPush =
6+6×1 = 12
). Expect: toast "{title}" pushed 7 days out.; guilt delta
round(12 × (1+0.5×6)) = round(12×4) = 48, landing at Guilt 60 (12+48) — check the
row's "Guilt 60" reading.

With guilt at 60 (tier "guilty", the documented 50–74 band), open the Guilt Meter panel
on Today's right rail. Expect: the meter's fill is visibly pulsing — the
"breathing" animation (CSS class ui-guilt-meter--breathing) is wired to apply at BOTH
guilty and shame, not only at shame — unless your OS has reduced motion on.

Open Later… and pick a specific date via the date field instead of a preset.
Expect: the toast reads "{title}" moved to {date}. and the same escalating
formula applies, using the number of days between today's schedule and your picked
date as deltaDays.

Click Cancel in an open Later… panel. Expect: the panel closes with no push
applied — Guilt and Pushes stay exactly as they were.

Final check: the item's tier badge text matches its guilt band — 0-24 "calm", 25-49
"nagging", 50-74 "guilty", 75-100 "shame" — read the badge's TEXT, not just its color
(color-alone-conveys-meaning was a fixed accessibility finding per
docs/qa/FINDINGS.md).

F5

Inline create and the empty-day path

Proves item creation never uses a browser popup, and that an empty Today offers a real way forward — both were filed and fixed accessibility/UX issues (FINDINGS.md O1).

Persona / precondition: Today, any state.

0/7

Click + New item in the toolbar. Expect: an inline input opens at the top of
the list — NOT a window.prompt() dialog — placeholder "What are you putting off?",
with Add it (disabled while empty) and Cancel.

Press Enter with the field empty. Expect: nothing destructive happens — no
blank item is created.

Type a title and press Enter. Expect: the item appears in the Today list
immediately, and the input stays open and focused for another entry rather than
closing.

Type a second title, then press Escape instead of Enter. Expect: the create row
closes WITHOUT adding that second item.

Push or complete every item currently on Today until the list is empty. Expect: the
empty state appears — heading "Nothing scheduled for today", the line "Either you are
thriving, or everything is already tomorrow's problem.", and two buttons: Add
something to neglect later
and Set up a demo room.

Click Set up a demo room. Expect: a populated set of demo items appears on
Today in one click, no confirmation dialog and no server round-trip — this is explicit,
user-triggered seeding, per the source comment "never injected into a non-empty day
silently".

Final check: refresh the page after step 6. Expect: the demo items are still
there — they were written through the same repository as any manually created item, not
held only in memory.

F6

The unified item editor

Proves the note-taking depth behind every task — one page holding both the task face and the memo face, per SPEC §4.

Persona / precondition: any item open on the editor surface (click its title from Today or All items).

0/7

In the toolbar, click B, Highlight, H, the checkbox glyph, Callout,
Code, and Table in turn — each inserts its own Markdown snippet at the cursor.
Expect: the live preview pane (toggle: Hide preview / Show preview) renders
each as it lands: bold text, a highlighted span, a heading, a checkbox list item, a
titled callout box, a fenced code block, and a real <table> — not raw Markdown
syntax.

Type [[ followed by a few letters of another item's title. Expect: a suggestion
list of matching items appears; picking one inserts [[Full Title]], and the preview
turns it into a clickable wikilink.

Click that wikilink in the preview. Expect: the editor switches to the target item —
or, if you typed a title with no match, a new stub item is created and opened
("unresolved links create stub items", SPEC §4.2).

Type # followed by a few letters. Expect: a tag-suggestion list appears; the
finished tag shows up as a colored chip both in the preview and in the editor footer's
tag row.

Click Markdown, Plaintext, and JSON in the editor footer in turn.
Expect: three separate file downloads ({title}.md, {title}.txt,
{title}.json), each containing your content in that format.

Click Print / PDF in the header. Expect: a new window opens with a print-ready
rendering of the item and the browser's native print dialog opens automatically — this
is a real OS print boundary, not an in-app PDF generator (per the source comment
"browsers and Tauri own the native PDF dialog").

Final check: navigate to a different item and back. Expect: every formatting
element from steps 1-4 is still exactly there — the editor is not losing your draft on
navigation.

F7

All items: the real list

Proves the second surface — search-and-browse over everything, task or memo, done or not, with no row cap.

Persona / precondition: several items exist (a mix of open/done, some tagged, ideally including work from F6).

0/7

Click All items in the sidebar. Expect: heading "Tasks, memos, and everything
between.", a badge with the total item count, and EVERY open item listed — no 6-row cap
like Today's guilt-sorted slice.

Type a word from one item's memo body into "Search titles, memos, #tags…". Expect:
the list narrows to matching items (fuzzy match on title + body text).

Clear the search and instead type a scoped query, tag:work is:open (swap work for a
real tag you've used). Expect: only open items carrying that tag (or a sub-tag like
work/anything) remain — this exercises the documented scoped-operator search (SPEC
§4.4: tag:, is:, due:, guilt:), not plain text matching.

Search for guilt:>=50. Expect: only items with guilt 50 or higher appear.

Clear the search and click the Show done (N) toggle. Expect: completed items
appear, struck through, and the button's own label flips to "Hiding nothing"; click it
again and they disappear, the label reverting to "Show done (N)".

Search for gibberish, e.g. zzzqqqxx. Expect: "Nothing matches that search." with
the hint "Try fewer words, or a #tag." — not a blank white area.

Final check: click any row's title. Expect: it opens on the editor surface to the
right, the same as clicking a Today row.

F8

Pro mode: additive, never destructive

Proves G8 — the advanced tier only ever ADDS surfaces; switching it off never removes, hides destructively, or replaces base data.

Persona / precondition: Settings, General mode, at least one item carrying a wikilink and a tag from F6.

0/7

On Settings, under "Advanced mode", confirm the sidebar has no Pro entry while in
General. Expect: only Today / All items / Settings show.

Click Pro. Expect: the sidebar gains a fourth entry, "Pro" (hint "Backlinks,
properties, graph & more"); the Settings card's Pro button becomes the accent
(pressed) one.

Open the Pro section. Expect: a tab strip with exactly five tabs — Backlinks,
Properties, Graph, Guilt analytics, Export — opening on Backlinks by
default.

With focus on the tab strip, press the Right arrow key repeatedly. Expect:
focus AND the active tab both advance across all five (roving-tabindex — only the
active tab sits in the page's Tab order); Home/End jump to the first/last tab.

Visit Graph. Expect: a node/edge diagram reflecting your wikilinks, with an
aria-label reading "Graph of {n} items and {n} links". Visit Export and click
Export vault (N). Expect: a bulk download of your items as an
Obsidian-compatible vault (one .md file per item).

Switch back to General in Settings. Expect: the Pro nav entry disappears
immediately, but every item you touched in Pro (tags, wikilinks, titles) is unchanged
in Today/All items — General "hides pro-only surfaces" per SPEC §3, it does not delete
pro data.

Final check: flip back to Pro once more. Expect: any backlinks/graph data tied
to your items from step 5 is still there — the additive data survived the round-trip.

F9

Appearance and the lock

Proves the theme system is genuinely live (not just painted-on buttons) and that the local passcode gate actually gates.

Persona / precondition: signed in, Settings visible.

0/7

Under "Appearance", click Dark. Expect: the whole app switches instantly to a
warm brown-black paper look (never a cold navy/slate dark theme), and the choice
survives a reload.

Click System, then flip your OS or browser's emulated light/dark setting.
Expect: the app's theme follows it live, with no manual reload needed.

Click Light. Expect: the third option also applies immediately.

Click Lock the desk in the sidebar footer. Expect: you land on "Welcome back,
{your name}." with a single Passcode field — NOT the "Claim your desk" create form,
since your profile already exists.

Type a WRONG passcode, submit. Expect: the error "That passcode does not open this
workspace." — the field clears and refocuses, with no hint about the real passcode.

Type the correct passcode, submit. Expect: you're back exactly where you left off
(same route, same items) — locking never touched Today's data.

Final check: close the browser TAB entirely (not just reload) and reopen
localhost:5174. Expect: you land on "Welcome back" again, locked — the unlock lives
in sessionStorage, which a closed tab clears, unlike F1's final check (a same-tab
reload).

F10

Keyboard-only and reduced motion

Proves the accessibility pass recorded in docs/qa/FINDINGS.md actually holds in the running app — and re-exposes the one gap that pass left open on purpose.

Persona / precondition: Today, at least 2 open items; a way to enable OS/browser "reduce motion".

0/5

Using only Tab / Shift+Tab / Enter / Space, reach an item's title, then Tab to
→ Tomorrow and press Enter. Expect: the push fires exactly as a mouse click
would, with a visible focus ring at every stop — real <button> elements throughout,
per FINDINGS.md's keyboard-navigation table.

Tab to Later…, press Enter to open the panel, then keep pressing Tab past the
date field and its submit control. Expect (documented, currently failing —
FINDINGS.md O3, filed as "minor", not fixed):
ideally focus stays trapped inside the
open panel until it closes. The shipped behavior is that focus is free to leave the
panel while it remains open on screen — confirm which one you actually see, and record
a FAIL only if you'd consider this newly severe, since it is a known, accepted gap.

Enable OS-level "reduce motion", then push an item to tomorrow. Expect: the push
applies INSTANTLY with no slide animation, and the Guilt Meter's breathing pulse (F4
step 4) also stops animating — nothing conveys information through motion alone.

Inspect the Guilt Meter with a screen reader or the accessibility-tree devtools panel.
Expect: role="progressbar" with aria-valuetext reading like "{n} percent,
{tier}" — the tier name is announced, not conveyed by color alone.

Final check: tab all the way through the Pro tab strip (F8) using only arrow keys plus
Home/End — confirm the roving-tabindex pattern holds (only the active tab is a Tab stop).

F11

Restart survival

Proves this is genuinely local-first: everything written through the repository outlives the process, not just the React tree in memory.

Persona / precondition: a live session with at least 3 items in assorted states (open, done, pushed).

0/4

Note the exact Guilt/Pushes numbers on 2-3 items, and which are marked done.

Stop the dev server (Ctrl-C in the terminal running npm run dev), start it again, and
reload the browser tab. Expect: every item, its guilt/push numbers, its done state,
and your theme/mode settings are byte-for-byte the same as step 1 — nothing lived only
in memory.

This flow could not be exercised live while authoring this document — no Node.js/npm
were available in that sandbox. Treat this as the flow with the least prior confidence
and run it carefully.

Final check: open DevTools → Application → IndexedDB → latereditems, and
confirm the row count matches what All items shows on screen.

F12

Operational checks: the three shells' config

Proves the packaging surfaces beyond the browser — latered ships a Tauri desktop config and a Capacitor mobile config alongside the PWA — are at least structurally sound.

Persona / precondition: a terminal in ~/Projects/latered/app.

0/5

Run npm run cap:config-check. Expect: Capacitor config: valid JSON — this exact
check (JSON.parse on capacitor/capacitor.config.json) was independently re-run in
this authoring session via Python's json.load, which succeeded; the npm/Node form
itself was not re-executed here (no Node.js in the authoring sandbox).

Run npm run tauri:config-check. Expect: Tauri config: valid JSON — same caveat:
[sensitive content omitted]

In the running web app, open DevTools → Application → Manifest. Expect: name
"latered", short name "latered", display: standalone, and two icons (icon.svg,
icon-512.png) — matching public/manifest.webmanifest verbatim; all six brand/icon
files it and the service worker reference exist on disk (icon.svg, icon-512.png,
wolf-mark.svg, empty-desk.svg, desk-bg.jpg, logo-lockup.png).

Check DevTools → Application → Service Workers. Expect: a worker named
latered-shell-v4 (or newer) is active; reload while online and you should see fresh
content, not a stale cached shell — the worker's own source comment says documents are
served "NETWORK-FIRST... so a new deploy is never held hostage by an old cache".

Final check: run npm test. Expect: all suites pass. The last recorded run
(docs/qa/FINDINGS.md, pre-dating this document) reported "20 files, 94 tests" passing —
a materially lower pass count here means something regressed since that audit and should
be filed, not waved through.

Not covered and why

  • Live execution of any flow above: this document was authored in a sandbox with no

Node.js, npm, or browser — every Expect: is sourced from reading the exact strings and
logic in the shipped source at commit 4c144fc, not from watching the app run.

  • Tauri desktop shell and Capacitor mobile shell as RUNNING apps (native menus, mobile

gestures, install flow): only their JSON configs were checked (F12); launching the
actual native shells needs platform toolchains (Xcode/Android Studio, Rust) not
available here.

  • GitHub/cloud sync: SPEC §1.6 explicitly marks sync "out of scope for this unit" and

src/storage/sync/ exists only as a scaffold — nothing user-facing to test yet.

  • The literal 4 am rollover crossing real midnight: time-dependent, cannot be forced

on demand; walk it passively by leaving a tab open past 4 am local time and watching the
date line and the daily-rollover job fire on their own.

  • Attachment/image embedding end-to-end (the editor's Attach button, wired to

AttachmentRepository): present in source but not walked step-by-step here; add a flow
once file-size/type limits are documented.

  • Multi-day date-picker edge cases (leap years, DST boundaries) in the push engine:

src/core/engine/dates.ts carries its own unit tests, not re-verified in this pass.

Results

FlowVerdictFailures → tickets
F1 landing + claim your desk
F2 first-login tour
F3 the signature push
F4 multi-day push + guilt math
F5 inline create + empty day
F6 the unified editor
F7 all items
F8 pro mode
F9 appearance + lock
F10 keyboard + reduced motion
F11 restart survival
F12 operational config checks
updated just nownext 3m 00s