Skip to content

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 with per-namespace levels. Each subsystem gets its own logger; toggle them independently.

Namespaces

NamespaceEmitted by
oxi:uploadDelta + direct upload pipeline (lib/api/endpoints/deltaUpload.ts, static/workers/deltaWorker.js)

New namespaces should follow the oxi:<subsystem> 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:<namespace> — 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=<hash>)
[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 lineMeaningWhat to try
worker fallback: wasm unavailable: <detail>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 threwnew Worker(...) failed — usually CSP worker-srcCheck CSP worker-src allows blob: if the worker uses one, or the origin
worker: chunk PUT failed (HTTP 413)worker requested fallbackServer (or a proxy in front of it) rejected the PUT body as too largeLower oxi.UPLOAD_BATCH_BYTES; check proxy body caps
worker: chunk PUT failed: <NetworkError / timeout>Cloudflare (or another proxy) cut the request mid-flightLower 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 stageServer logs; grep for the emitted request_id
delta worker went silent for 20s — disabling delta for this tabWorker 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 uploadWall-clock timeout (120s + 90s per GB) — usually means chunk PUTs are stallingLook 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 uploadCommit 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 507Storage quota exceededUser 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: <name>. 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

ConcernFile
oxi.* global attach + settersfrontend/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 registryfrontend/src/lib/upload/interruption.ts
Reload-time toast wiringfrontend/src/routes/+layout.svelte (onMount)

Server-side counterpart for delta upload: Delta upload protocol.

Released under the MIT License.