First-run onboarding UX review — prioritized backlog
TL;DR. A new user can get from zero to a published page, but the path fights them at four points: the README quickstart is a dead end (docker compose up starts no trip2g), the README's own "Getting started" link 404s on the live site, the docs describe a manual BRAT+API-key setup while the product ships a one-click preconfigured vault the docs never mention, and every sync failure after setup is silent (console-only). Fixing those four removes most first-user pain.
Method. Walked the real path on an isolated memcli instance (--name uxreview, port 24981): boot → anonymous empty state → HAT login → vault download → plugin data.json → push a note → anon/admin visibility. Mined the demo-recorder reproducers and gotchas (docs/dev/2026-07-03_hero_demo_v1_scenario.md, _v1_optB_, _v2_; autoproducer/scripts/hero*). Cross-checked every claim against code (file:line below) and the live trip2g.com.
Biggest wins (do these first)
- Fix the README quickstart dead end.
README.mdsaysgit clone … && docker compose upunder "Run your own hub" — but the rootdocker-compose.yamlstarts only MinIO + a 2.3 GB embedding server + a data mock, no trip2g server. The first thing a motivated dev tries, fails. Ship a compose file that boots the hub (thedocs/en/user/selfhosted.mdcompose works), or point the README at it. - Fix broken README links.
https://trip2g.com/en/user/getting-started→ 404 (live slug is/en/user/getting_started);…/two-way-sync→ 404 (live:two_way_sync). The single most-clicked onboarding link is dead. Verified 2026-07-03 with curl against all READMEtrip2g.comlinks; all others 200. - Document the starter-vault path in Getting started. The product's real first-run flow (empty instance → "Download archive" → open in Obsidian → plugin already configured → save → live) is not mentioned anywhere in
docs/en/user/getting-started.md, which instead walks users through BRAT + manual API key + manual plugin config — 4 steps the vault makes unnecessary. Onlyselfhosted.md:560hints at it. Lead Getting started with the vault path; keep BRAT as the "existing vault" path. - Make background sync failures visible. After setup,
autoSyncOnSavepush errors go toconsole.warnonly (obsidian-sync/src/main.ts:294), live-pull errors too (main.ts:715), background change-polling swallows errors silently (main.ts:601). A user whose API key expired or server went down keeps "saving" into the void with zero feedback. Surface a Notice + red ribbon state on repeated auto-push failure. - One dev sign-in code, everywhere. The code is
111111only (cmd/server/auth.go:199,250), butdocs/en/user/local-quickstart.md:66,141claims "000000also works" (it does not), and the demo scenario had to leave a "verify the dev code" note (2026-07-03_hero_demo_v2_scenario.mdbeat 8–14). Fix the doc; even the team can't tell which code is real.
CLI
memcli
| # | Finding | Why confusing | Sev | Fix |
|---|---|---|---|---|
| C1 | Every up prints NOTE: image must include feat/filestorage local-storage backend. (cli/memcli/src/cli.ts:1857, also in --help NOTES) |
Internal branch jargon on the happy path; a new user can't act on it and wonders if something is wrong | MED | Drop the note (the default image includes it), or turn it into a post-failure diagnostic |
| C2 | Happy path otherwise excellent: up boots, mints a key, writes plugin config, prints memory live — web: http://localhost:24981 (verified live). Port-busy error is actionable (cli.ts:1847) |
— | — | Keep; this is the best onboarding surface in the product — consider promoting memcli up + memcli open as the local try-it path in README |
| C3 | memcli exists only as node cli/memcli/dist/memcli.js inside a repo checkout; docs (docs/en/user/memcli.md:15) reflect that |
"Install" = clone a whole monorepo; long invocation deters copy-paste | LOW | Publish to npm (npx trip2g-memcli up) or a curl-install like kanban's |
trip2g-sync CLI (obsidian-sync/dist/trip2g-sync.mjs)
| # | Finding | Why confusing | Sev | Fix |
|---|---|---|---|---|
| C4 | docs/en/user/cli.md has drifted from the binary: pins release 0.3.5 (cli.md:26,139) while repo manifest is 0.6.1 and GitHub latest release is 0.5.0; documents -f short flag and default endpoint http://localhost:8081/graphql (cli.md:66-67) while the real help says positional <folder>, -u default …/_system/graphql, and /graphql is deprecated (docs/dev/search_next_steps.md:25); omits --watch/--include/--exclude/--state-file entirely |
Users following the doc download an old binary and use a deprecated endpoint | HIGH | Regenerate cli.md from the current --help; link "latest" release instead of a pinned tag |
| C5 | Help usage line says npx ts-node src/sync/cli/cmd.ts [options] <folder> — a dev-repo invocation, and mixed positional/flag styles (error says --folder is required while examples use positional; both actually work) |
First contact with the tool is its help text; it currently describes running from source | LOW | Usage line trip2g-sync [options] <folder>; make the required-arg error mention both forms |
| C6 | Fatal errors dump raw stack traces: ❌ Fatal error: Error: ENOENT … at Module.readdirSync (node:fs:1570:26) … (verified: nonexistent folder). Exit codes are correct (1) |
Stack trace reads as a crash, not "folder not found" | MED | Catch known errors (missing folder, 401 bad key, ECONNREFUSED) → one-line actionable message; keep stack behind --verbose |
Docs
| # | Finding | Why confusing | Sev | Fix |
|---|---|---|---|---|
| D1 | README quickstart docker compose up starts no server (root docker-compose.yaml: minio + embedding + data_mock only) |
Advertised 30-second start fails; user has no fallback except digging into docs | HIGH | See Biggest win 1 |
| D2 | README links 404: en/user/getting-started, en/user/two-way-sync (live slugs use underscores) |
Dead getting-started link right after a failed quickstart = bounce | HIGH | Fix links; add a link-check to CI (a landing-link audit doc already exists: docs/dev/2026-07-02_landing_link_audit.md) |
| D3 | getting-started.md never mentions the onboarding-vault download; teaches the long path (BRAT → API key → paste URL+key → Test All Connections) |
New users do 6 manual steps the product automates; docs and product tell different stories | HIGH | Restructure: "Path A — starter vault (recommended, 2 steps)" / "Path B — into your existing vault (BRAT)" |
| D4 | getting-started.md:45-53 instructs enabling Show Draft Versions as a first step, but its default is already true (internal/configregistry/registry.go:111-114) |
First admin-panel task the doc gives is a no-op; erodes trust in the doc | MED | Remove the step, or reframe: "drafts are visible by default; turn OFF to use releases" |
| D5 | local-quickstart.md:66,141 claims dev code "000000 also works" — false (cmd/server/auth.go:250 accepts only 111111) |
User pastes 000000, gets rejected, doubts the whole setup | HIGH | Correct to 111111 only |
| D6 | getting-started.md:120: "Open the site in an incognito window and the page is not accessible." Reality: a non-free note returns 200 with the title rendered and a JS sign-in wall; body hidden (verified on isolated instance) |
User checking "is it private?" sees the page half-exists and can't tell if privacy works | LOW | Reword: "visitors see the title and a sign-in wall, not the content" |
| D7 | hosting.md:26 references image ghcr.io/trip2g/trip2g:0.2 while everything else uses :latest |
Version pin drift | LOW | Use :latest or a documented release policy |
| D8 | Sign-in with no/broken SMTP: requestEmailSignInCode enqueues and returns success (internal/case/requestemailsignin/resolve.go:114-119); with no SMTP the code is only logged (email.go:14-17, sendsignincode/resolve.go:24). selfhosted.md:171 documents the journalctl fallback for bare-metal, but the Compose section and getting-started don't |
The #1 self-host failure mode ("code never arrives") has its answer buried in one variant of one doc | MED | Put "code didn't arrive? check docker compose logs trip2g — the code is printed there" into getting-started's Sign in section and the Compose section |
Plugin / sync setup
| # | Finding | Why confusing | Sev | Fix |
|---|---|---|---|---|
| P1 | Auto-push/auto-pull/background errors are console-only (obsidian-sync/src/main.ts:294,601,715); red ribbon badge fires only on content conflicts, not connection failures |
Silent data-not-syncing; the worst failure UX in the funnel | HIGH | Notice + persistent ribbon error state after N consecutive failures; "last successful push HH:MM" in settings |
| P2 | Settings empty state (main.ts:1401-1407) links https://trip2g.com/docs/onboarding — which serves a Russian page ("Начало работы") to every user |
English user's first in-plugin doc link lands on RU content | HIGH | Link …/en/user/getting_started (or lang-negotiated) |
| P3 | autoSyncOnSave defaults to false (src/types.ts:29-34) and getting-started never mentions it; the "magic" moment in all demos (save → live) is off by default and undocumented |
Users manually click sync forever, missing the product's headline behavior | MED | Mention the toggle in getting-started; consider default-on for vault-download path (ship it in the vault's data.json — currently skipPushConfirmation: false, no autoSync, verified in downloaded zip) |
| P4 | Sync state lives in Obsidian localStorage (main.ts:31-32 sync-state/published-urls), not in the vault; copy vault to a new machine without .obsidian internals → state lost → first sync triggers migration modal (main.ts:945-949). Reset exists only as a settings button (main.ts:1478-1485) |
Invisible state with surprising cross-machine behavior; demo rig needed CDP surgery to clear it (hero_demo_v1_scenario.md gotchas) |
MED | Document the behavior + the Reset button; consider persisting state in the plugin data dir instead |
| P5 | No-op save gives no feedback on auto-sync (Notice only when pushed > 0, main.ts:339); manual sync says "All files are up to date" (main.ts:1010-1012) |
After editing-and-reverting, user saves and sees nothing — is sync broken? (also cost the demo recorder retakes) | LOW | Optional subtle status-bar tick "up to date" on auto-sync no-op |
| P6 | Plugin not in the official Obsidian catalog; BRAT-only (obsidian-sync/README.md:18) and BRAT "Latest" fetches GitHub release 0.5.0 while repo is at 0.6.1 |
Extra installer plugin, plus users get a stale build | MED | Cut a 0.6.x release; pursue the official catalog listing |
| P7 | Failure text on Test All Connections is good (per-dir red error, main.ts:1501-1508; summary "X successful, Y failed", main.ts:1446) — but bad-URL errors are raw fetch messages |
Raw "Failed to fetch" doesn't say "check the URL / your key" | LOW | Map common errors (401 → "API key rejected", ENOTFOUND → "check the site URL") |
Admin UI / first-run
| # | Finding | Why confusing | Sev | Fix |
|---|---|---|---|---|
| A1 | Anonymous empty state says "Log in as administrator to get started" with no login link in that block (internal/case/rendernotepage/view.html:288-295); login lives in the JS-mounted $trip2g_user_space widget top-right |
Fresh owner opening their new instance must hunt for how to log in at the exact moment of first contact | HIGH | Make "Log in" in that sentence a link/button that opens the auth widget |
| A2 | Onboarding page copy is hardcoded RU-first bilingual (both languages stacked, view.html:253-295; sidebar item hardcoded "Главная", view.html:236), bypassing the i18n system (langs/en.toml exists) |
First screen looks half-translated/unfinished | MED | Localize via ctx.T() like the rest of the template |
| A3 | Onboarding page ends at "Download archive" — nothing about the next steps (open in Obsidian → Trust author and enable plugins → press sync). The trust dialog is a known stumble (demo rig one-time setup, hero_demo_v1_optB_scenario.md:60) |
User downloads a zip, opens it, and hits an Obsidian security dialog no one warned them about; if they decline, the plugin never runs and nothing syncs | HIGH | Add 3 numbered steps under the button, explicitly including the trust dialog and the sync button |
| A4 | Dev-mode login pre-fills the code field with 111111 (assets/ui/auth/auth.view.ts:245) with no hint text; dev mode also self-activates on any localhost hostname (assets/ui/settings/settings.ts:3-5) |
Pre-filled value with no label reads as a bug; and on a non-DEV localhost server the pre-filled code is wrong | MED | Show "(dev instance — code is 111111)" caption; gate the prefill on server-reported is_dev_mode, not hostname |
| A5 | Each visit to /_system/onboarding-vault mints a new API key "Onboarding vault" (downloadonboardingvault/resolve.go:107-124) |
Curious admin clicks 3× → 3 live admin keys, no cleanup, no warning | LOW | Reuse/revoke prior onboarding keys, or name them with a timestamp |
| A6 | ?enable_admin_graphql download option exists only as an undocumented query param (downloadonboardingvault/endpoint.go:35-37); UI toggle is a button buried in Integrations → API Keys → key (mcpadmintools.view.ts:22) |
Agent-oriented users can't discover the capability | LOW | Checkbox on the onboarding page ("include admin access for agents") |
| A7 | API Keys live under Integrations, site config under System with raw config_id rows (show_draft_versions, no friendly labels) (assets/ui/admin/-/node.view.tree:4469; admin/menu/system/system.view.tree:9) |
Getting-started sends users to "Settings → API Keys"-style paths that don't match the nav; config table is developer-facing | MED | Friendly config labels + make doc paths match nav ("Integrations → API Keys") |
| A8 | Turning show_draft_versions off makes every future sync invisible until a release, with no UI warning (rendernotepage/resolve.go:208-220) |
"I synced and my page vanished" support trap | LOW | Confirmation dialog explaining the release workflow when toggling off |
| A9 | HAT login is clean server-side: token consumed, ?hat= stripped via 302 (cmd/server/server.go:71-83, verified: 302 Location: / + cookie). But the original ?hat= URL remains in browser history/omnibox suggestions (demo had to clear history, hero_demo_v1_optB_scenario.md:60) |
Short-TTL mitigates it, but a signed login token in shareable history is a footgun | LOW | Note in memcli open output that the link is one-time/short-TTL; consider POST-based handoff page |
Errors (cross-cutting)
| # | Finding | Sev | Fix |
|---|---|---|---|
| E1 | Email send failures are invisible end-to-end: GraphQL returns success before the job runs (requestemailsignin/resolve.go:114-119), SMTP-missing is a log-warn no-op (email.go:14-17) |
MED | Admin-visible mail health (banner in admin panel: "email is not configured — sign-in codes appear in server logs") |
| E2 | Broken custom layout falls back silently to the default layout with HTTP 200 (local-quickstart.md:143 documents it as a gotcha) |
LOW | Admin-only inline warning banner on fallback-rendered pages |
| E3 | Missing S3 object for a header logo generates a presigned URL without existence check → silent broken <img> (internal/miniostorage/storage.go:182-207) |
LOW | Skip the logo when the object is missing; log once |
Automation-only quirks (not user-facing — don't spend product effort)
From the demo-recorder gotchas (2026-07-03_hero_demo_v1_scenario.md:58-66, _optB_:60-67): cold-start Obsidian dropping first keystrokes after Ctrl+N; stale xdotool window ids vs dwm client list; picom/Electron black-window ordering; feh vs ImageMagick root pixmap; session-restored Chromium carrying a dead SSE subscription (real-ish, but only bites parked long-lived tabs; live_follow is an owner/demo feature). The per-take server-side content scramble exists only because of the (real, listed above as P5) no-op push behavior; the localStorage CDP surgery exists because of (real, P4) sync-state placement.
Verification notes
- Isolated instance
trip2g-memory-uxreview(port 24981,trip2g:local) was booted and torn down; the recording rig on:99and itsdemo-v1/demoV2instances were not touched. - Live-site link checks ran against
trip2g.comon 2026-07-03. - Not tested for real: the BRAT install flow inside a GUI Obsidian and the admin panel's visual navigation (analyzed from
assets/uisource + agent reports instead); the resend email path (analyzed from code).