From 55a3890ffa43e223f7e934cb26e6e6b025982e42 Mon Sep 17 00:00:00 2001 From: Edouard Vanbelle Date: Fri, 14 Aug 2026 01:41:13 +0200 Subject: [PATCH] doc: add UI diagnostics help --- docs/.vitepress/config.mts | 1 + docs/architecture/ui-diagnostics.md | 209 ++++++++++++++++++++++++++++ 2 files changed, 210 insertions(+) create mode 100644 docs/architecture/ui-diagnostics.md diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index e1700864..fbd9da52 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -175,6 +175,7 @@ export default defineConfig({ { text: "Authentication model", link: "/architecture/auth-model" }, { text: "Magic-link auth", link: "/architecture/magic-link-auth" }, { text: "Background jobs", link: "/architecture/jobs" }, + { text: "UI diagnostics", link: "/architecture/ui-diagnostics" }, ], }, { text: "FAQ", link: "/faq" }, diff --git a/docs/architecture/ui-diagnostics.md b/docs/architecture/ui-diagnostics.md new file mode 100644 index 00000000..10562115 --- /dev/null +++ b/docs/architecture/ui-diagnostics.md @@ -0,0 +1,209 @@ +# UI Diagnostics + +Runtime-tunable knobs and namespaced logging exposed to the browser +DevTools console so upload and session issues can be diagnosed without +a rebuild, a config flag, or a page reload. + +All entry points live under a single global — `window.oxi` — attached +during the client init hook (`frontend/src/hooks.client.ts`). Type +`oxi.` in DevTools with autocomplete to discover what's available; the +sections below name what each surface does. + +## The `oxi.*` helper + +```js +oxi.log // raw `loglevel` module +oxi.setLogLevel(ns, level) // toggle a namespace's level +oxi.listLogLevels() // enumerate persisted overrides +oxi.UPLOAD_BATCH_BYTES // get/set per-PUT byte cap for delta upload +``` + +`oxi.setLogLevel` returns a confirmation string (`"oxi:upload → debug"`) +so the DevTools echo is a positive signal instead of the confusing +`undefined` a naked `void`-returning setter would produce. + +## Log levels + +The frontend uses [`loglevel`](https://github.com/pimterry/loglevel) +with per-namespace levels. Each subsystem gets its own logger; toggle +them independently. + +### Namespaces + +| Namespace | Emitted by | +| ----------- | ------------------------------------------------------------------ | +| `oxi:upload` | Delta + direct upload pipeline (`lib/api/endpoints/deltaUpload.ts`, `static/workers/deltaWorker.js`) | + +New namespaces should follow the `oxi:` shape so a wildcard +filter across the whole app remains meaningful. + +### Levels + +`trace` < `debug` < `info` < `warn` < `error` < `silent`. + +Default per namespace is `info` — phase transitions and error paths +surface without any opt-in. Bump to `debug` for per-chunk / +per-batch verbose trace during a failure hunt. + +### Runtime toggle + +```js +// Deep dive into upload internals +oxi.setLogLevel('oxi:upload', 'debug') +// → 'oxi:upload → debug' + +// Quiet mode — only warnings and errors +oxi.setLogLevel('oxi:upload', 'warn') + +// Full silence +oxi.setLogLevel('oxi:upload', 'silent') + +// See every namespace's current override +oxi.listLogLevels() +// → { 'oxi:upload': 'DEBUG' } + +// Everything (including future namespaces) to debug +oxi.log.setLevel('debug') +``` + +Changes persist to `localStorage` under the key +`loglevel:` — the choice survives page reloads and browser +restarts until you explicitly change it back or clear localStorage. + +Worker context: `deltaWorker.js` runs in a Web Worker and can't +`import 'loglevel'` (the worker is served from `/static` without +bundler resolution). Instead it emits log events via `postMessage` and +the main-thread orchestrator relays them through the shared logger, so +`oxi.setLogLevel('oxi:upload', 'debug')` filters worker output too. + +## Upload batch tuning + +`oxi.UPLOAD_BATCH_BYTES` controls the target size of each `PUT +/api/files/delta/chunks` body — the delta worker groups missing chunks +into that size before sending. Default is 8 MiB; can go up (fewer, +larger requests) or down (more, smaller requests). + +```js +oxi.UPLOAD_BATCH_BYTES // read current value +// → 8388608 + +oxi.UPLOAD_BATCH_BYTES = 1024 * 1024 // 1 MiB per PUT +// → 1048576 + +oxi.UPLOAD_BATCH_BYTES = 8 * 1024 * 1024 // back to default (removes the override) +``` + +Persisted to `localStorage['oxi:upload:batchBytes']`. Setting the value +back to the default clears the entry so the storage stays clean. + +### When to lower it + +Behind reverse proxies with tight per-request timeouts. The classic +case is **Cloudflare Tunnel**: 100-second absolute per-request +timeout on the Free/Pro plans. A user on a slow uplink (say, hotel +Wi-Fi at 512 Kbps) can't complete an 8 MiB PUT in that window and gets +their request cut mid-flight. Lower to 1 MiB (~16 seconds at 512 Kbps) +and it fits comfortably. + +Cost: about 8× more HTTP requests per file. TCP keep-alive amortises +most of the connection setup; the extra CPU is negligible. + +### Read timing + +Read once per upload at worker spawn time. Change from the console → +the NEXT upload picks up the new value; the currently-running upload +finishes with the old value. No reload required. + +## Delta upload trace + +A healthy fresh upload at `info` level looks like: + +``` +[3f7a2b] delta start {file: "vacation.mp4", size: 524288000} +[3f7a2b] worker: worker start {file: "vacation.mp4", size: 524288000} +[3f7a2b] worker: wasm loaded +[3f7a2b] worker: hashed — blake3=<64-hex> (512 chunks) +[3f7a2b] worker: negotiate: 256 hashes → 240 missing, 16 dedup'd +[3f7a2b] worker: negotiate: 256 hashes → 256 missing, 0 dedup'd +[3f7a2b] worker: ✅ committed — uploaded 501346304 B, reused 22941696 B (4% dedup, blake3=) +[3f7a2b] worker: commit HTTP 201 {blake3, uploadedBytes, reusedBytes, totalBytes, attempt} +[3f7a2b] delta done {file, blake3, savedBytes, uploadedBytes} +``` + +At `debug` level the worker additionally emits one line per chunk PUT +(`chunk PUT: 28 chunks, 8825338 bytes`) — expect roughly one line per +`UPLOAD_BATCH_BYTES` worth of body sent. + +Every line prefixes a short 6-hex upload id (`3f7a2b`) so concurrent +uploads stay distinguishable in the console. The `blake3` field is the +whole-file BLAKE3 hash the commit call carried — same value as +`storage.file_blobs.hash` on the server, so log lines correlate +directly to server-side blob rows. + +### Common failure signatures + +| Log line | Meaning | What to try | +| --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| `worker fallback: wasm unavailable: ` | Browser blocked WebAssembly (CSP, extension, disabled in prefs) | Check CSP `script-src 'wasm-unsafe-eval'` allows it; disable content-blocking extensions on the tab | +| `delta disabled for this tab: Worker constructor threw` | `new Worker(...)` failed — usually CSP `worker-src` | Check CSP `worker-src` allows `blob:` if the worker uses one, or the origin | +| `worker: chunk PUT failed (HTTP 413)` → `worker requested fallback` | Server (or a proxy in front of it) rejected the PUT body as too large | Lower `oxi.UPLOAD_BATCH_BYTES`; check proxy body caps | +| `worker: chunk PUT failed: ` | Cloudflare (or another proxy) cut the request mid-flight | Lower `oxi.UPLOAD_BATCH_BYTES` so each PUT fits inside the proxy's per-request timeout | +| `worker: negotiate failed (HTTP 5xx)` | Server-side error during the negotiate stage | Server logs; grep for the emitted `request_id` | +| `delta worker went silent for 20s — disabling delta for this tab` | Worker stopped emitting progress — WASM hang, DoS, or extreme main-thread contention. Poisons this tab. | Reload to reset the poison flag; check `console` for errors emitted by the WASM module or the worker itself | +| `delta timeout after Xs — falling back to direct upload` | Wall-clock timeout (`120s + 90s per GB`) — usually means chunk PUTs are stalling | Look for the last `chunk PUT` line; if none appeared for many seconds, network is stalled at the tunnel | +| `delta done with non-2xx (HTTP 500) — falling back to direct upload` | Commit rejected server-side — chunk verification, quota, name conflict, etc. | Server logs; look for `delta_upload.rejected` audit line with a `reason` field | +| `worker: commit HTTP 507` | Storage quota exceeded | User needs to free space or admin needs to increase quota | + +The generic pattern for user reports: ask them to open DevTools → +Console tab (filter: `oxi:upload`), run `oxi.setLogLevel('oxi:upload', +'debug')`, retry the failing upload, and share the output. The last +line before the "Upload failed" toast names the actual failure. + +## Interrupted uploads + +Two coordinated behaviours help users recover from an accidental page +reload during an upload (details in +`frontend/src/lib/upload/interruption.ts`): + +### `beforeunload` guard + +While any upload is in flight, the browser prompts *"Leave site? +Changes may not be saved"* on refresh / tab close. Deliberate leave +(user clicks Leave) proceeds; accidental Cmd-R is cancelled. + +Installed lazily: the listener is added when the first batch acquires +the guard, removed when the last batch releases it. No cost outside +active uploads. + +### sessionStorage register + +Every `uploadBatch` writes a record to +`sessionStorage['oxi:upload:interrupted']` while it runs and removes +it on completion. If a reload survives them, the root layout's +`onMount` reads and clears the register, then toasts: + +> Upload interrupted: ``. Re-drop to resume — already-uploaded +> chunks are reused. + +Chunks landed in `storage.file_blobs` before the reload persist on the +server. A re-drop lets the delta worker's `negotiate` stage discover +them as "already on server", so a resumed upload only transfers what +was in flight when the reload hit — not everything from scratch. + +`sessionStorage` (not `localStorage`) on purpose: entries clear when +the tab closes entirely, so a user who closed the tab hours ago +doesn't get nagged on reopen. + +## Code entry points + +| Concern | File | +| ----------------------------------- | --------------------------------------------------------------- | +| `oxi.*` global attach + setters | `frontend/src/hooks.client.ts` | +| Delta orchestrator (main thread) | `frontend/src/lib/api/endpoints/deltaUpload.ts` | +| Delta worker (CDC + BLAKE3 + PUTs) | `frontend/static/workers/deltaWorker.js` | +| Direct upload (fallback path) | `frontend/src/lib/api/endpoints/files.ts::uploadFileWithProgress` | +| Interrupted-upload registry | `frontend/src/lib/upload/interruption.ts` | +| Reload-time toast wiring | `frontend/src/routes/+layout.svelte` (`onMount`) | + +Server-side counterpart for delta upload: +[Delta upload protocol](../delta-upload-protocol.md).