Skip to content

UX critique improvements, server mode over the tailnet, and phone layout - #28

Merged
erwin-wee merged 18 commits into
mainfrom
feat/ux-critique-improvements
Sep 24, 2026
Merged

erwin-wee merged 18 commits into
mainfrom
feat/ux-critique-improvements

Conversation

@erwin-wee

Copy link
Copy Markdown
Owner

Summary

Three bodies of work, 18 commits:

UX critique improvements

  • In-app help panel and contextual guidance; condensed toolbar and banners with a help trigger
  • Two-line commit rows, lighter file rows, history filter cheat-sheet
  • Searchable shortcuts, clearer discard copy, labelled diff controls
  • Interaction states, browser-surface theming, shared copy constants; commit-success moment
  • Pull request list kept under GitHub GraphQL cost limits

Server mode (web access over the tailnet)

  • Electron-free core (handlers, event bus, HostCapabilities) mounted by both the desktop app and a new node:http + WebSocket server (src/server/)
  • Loopback-only behind tailscale serve; bearer token; every forwarded request must carry the allowed Tailscale-User-Login (default: node owner); host allowlist against DNS rebinding; per-method path confinement; per-repo mutation lock; rate limit by source address
  • Per-client repository watcher and history load, so a desktop and a phone don't interfere; server-side trash under <userData>/trash/
  • Desktop app can run as a client of a server (server-url / GITGOOD_SERVER_URL), warns on version mismatch
  • npm run build includes the server (installers ship it); npm run install:service writes a systemd user unit; CI builds the server bundle
  • Spec archived to openspec/specs/server-mode/spec.md

Phone/tablet layout (web only)

  • One pane at a time with a bottom tab bar, back-gesture navigation, bottom-sheet pickers/dialogs/menus, long-press context menus, touch-sized targets, touch drag-select; installable web manifest
  • The desktop app keeps its layout at any window width or zoom

Fixes found along the way

  • Push progress shows hook output instead of sitting on "Starting…"
  • Launch watched-folder scan waits for git discovery (it registered nothing when it raced it)
  • Smoke scenarios wait for renderer bootstrap (6 scenarios also failed on main)

Review

Security and code reviews ran against the server/mobile work; all 15 findings are fixed in the last five commits (path confinement bypass via option objects, discard-to-trash hard-deleting in web mode, clients stopping each other's watchers and history loads, unbounded WebSocket buffering, rate-limit bypass, reconnect resync, and others).

Testing

  • npm run typecheck: clean
  • npm test: 1117 passed, 4 skipped
  • npm run test:smoke: 35/35
  • Manual: served app over the real tailscale serve ingress (status, diff, commit, history, live events); forged host/identity headers refused (421/403); two browser tabs on different repositories keep their own live updates, including after a server restart; iPhone 13 emulation for drill-in/back, long-press menu and touch drag-select; packaged Linux build runs the server with ELECTRON_RUN_AS_NODE=1

Not verified: a real phone (touch behaviour was tested in emulation only), and the desktop-layout guard in an actual narrowed/zoomed Electron window (verified in a browser by toggling the marker).

gh pr list --limit 100 with 'commits' requested up to 1M nodes (cap 500k), so every PR list load failed. Drop 'commits' and the unused commitsCount; skip per-PR mergeability in the bulk list (it times out on busy repos). Single-PR view/forBranch still fetch mergeable/mergeStateStatus.
Non-progress stderr lines (a pre-push hook, remote messages) become the
progress description, so a long hook no longer leaves the UI on
"Starting…"; the percentage stays empty until git reports a transfer phase.
Extracts an Electron-free core (handlers, event bus, HostCapabilities) that
both the desktop app and a new node:http + WebSocket server mount. The server
binds 127.0.0.1 behind `tailscale serve`, requires a bearer token, holds every
forwarded request (page and bridge included) to the node owner's Tailscale
login, answers only loopback/*.ts.net hosts, confines paths to allowlisted
roots, serializes per-repo mutations and rate-limits clients.

The desktop app can run as a client of a server (server-url /
GITGOOD_SERVER_URL) and warns once on a version mismatch (GET /version).
`npm run build` now builds the server so installers ship it;
`npm run install:service` writes a systemd user unit; CI builds the bundle.
The launch watched-folder scan now waits for tool discovery, since
registering candidates runs git.
Below 768px the UI shows one pane at a time with a bottom tab bar;
tapping a row drills in and the browser back gesture returns. Pickers,
dialogs and context menus become bottom sheets, long-press opens context
menus (iOS Safari never fires contextmenu), touch targets grow to 44px and
line selection works with touch. A web manifest and icons make it
installable to the home screen. Desktop layouts are unchanged.
Scenarios started acting while the renderer was still waiting for tool
discovery, so settings, update and watched-folder scenarios saw
unloaded state and failed (on main too).
- Path confinement follows each method's signature: repository paths, path
  parameters and path fields of option objects (repos.create/clone
  directory, worktree path, settings folders) are checked, `..` is resolved
  and relative paths are refused; file contents, .gitignore lines and
  comments starting with "/" are no longer rejected.
- Trash works on the server: items go to <userData>/trash/, so discards
  that asked for the trash no longer hard-delete, and repos.remove trashes
  before unregistering.
- Per-client state: each client (X-GitGood-Client, stable per tab) has its
  own repository watcher and history load, so a desktop and a phone no
  longer stop each other's live updates or cancel each other's history.
  A disconnected client releases its watcher after 60s.
- The browser bridge reports focus/visibility and, after a reconnect,
  re-opens its repository and refreshes.
- Long network/AI calls (fetch, push, AI review/triage) no longer hold the
  per-repository lock.
- WebSocket: unmasked frames or frames over 125 bytes close the connection
  before anything is buffered.
- Malformed URLs get 400; an uncaught exception exits for systemd to restart.
History entries now record how many entries were pushed instead of
inferring it from the pane's depth; the review view drills from the list
straight to a file in one entry, so a forward swipe followed by a tab
switch rewound past the app's own page.
The tablet and phone layouts are for browsers. Their media rules are now
scoped to html:not([data-desktop]); the preload marks both desktop modes
(gitgoodDesktop), which also turns off the phone drill-down in JS. CSS
nesting is flattened at build time (cssTarget) for older phone Safari.
Touch pointers are implicitly captured by the cell they start on, so no
other cell saw pointerenter; the capture is released on pointerdown.
… paths

- The invoke rate limit is keyed by X-Forwarded-For (set by tailscale serve)
  or the socket address instead of the caller-chosen X-GitGood-Client, so
  rotating the header no longer bypasses it.
- In desktop client mode, open/reveal/trash of a path the server's page
  asks for is first checked against the server's path confinement.
@erwin-wee
erwin-wee merged commit 56ae03c into main Sep 24, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant