Portable UI: roots page declares free-space fields it never fills, and task actions stream no progress #1

Closed
opened 2026-08-11 23:36:39 +02:00 by julian · 1 comment
Owner

Two gaps in the module's portable UI surface, found while reviewing core PR OpsDeck/core#31 for mobile impact. Both are module-side fixes — the schema already supports what's needed.

1. Dead free-space fields on the roots page

backend/ui.ts declares a "Free" field and a used/total progress bar on the roots list:

  • ui.ts:83{ label: "Free", path: "freeBytes", format: "bytes" }
  • ui.ts:86progress: { used: "usedBytes", total: "totalBytes" }

but the route that feeds it hardcodes all three to null (ui.ts:361-363):

usedBytes: null,
totalBytes: null,
freeBytes: null,

So the Android roots page (and the schema renderer anywhere) promises free space and a usage bar and always renders them empty. The df helper already exists — fsops.ts:322-343 (df -Pk, kB→bytes) — and the web routes use it with caching (routes.ts:58: "df is a subprocess; a listing must not pay for it on every keystroke").

Either wire the cached df result into /s/roots, or drop the three bindings from the schema. Wiring it is the better outcome: the README already documents the degradation story ("a deployment without coreutils shows no usage bar rather than a wrong one"), which maps cleanly to leaving the fields null when df is absent.

2. Scan and delete actions stream no progress on mobile

The schema's actions block supports progressTopic (core packages/sdk/ui.ts:305) — an SSE topic whose msg payloads stream into a progress log until a message starts with done or error. The module's scan and delete actions (ui.ts:147-171, ui.ts:248-263) don't use it: they start a task run, return runId, and the mobile client sees nothing until the next refresh tick (15 s on the scan page).

Wiring a topic gives mobile live progress with no schema change. One decision to make: the module's live topics are currently admin-gated (README: "Live topics are admin-gated"), but the scan action is available at readRole and delete at writeRole — a non-admin who can start the run wouldn't see its stream. Either a per-topic role matching the action's role, or an explicitly progress-only topic that carries no path data beyond what the actor already submitted.

Context

  • The four remaining mobile gaps (typed-phrase confirm, download verb, file input, breadcrumb nav) are schema/renderer additions, tracked in OpsDeck/mobile.
  • Related: core PR OpsDeck/core#31 (component kit this module's web frontend needs), OpsDeck/core#32 (module-builder tsconfig, merged).
Two gaps in the module's portable UI surface, found while reviewing core PR OpsDeck/core#31 for mobile impact. Both are module-side fixes — the schema already supports what's needed. ## 1. Dead free-space fields on the roots page `backend/ui.ts` declares a "Free" field and a used/total progress bar on the roots list: - `ui.ts:83` — `{ label: "Free", path: "freeBytes", format: "bytes" }` - `ui.ts:86` — `progress: { used: "usedBytes", total: "totalBytes" }` but the route that feeds it hardcodes all three to null (`ui.ts:361-363`): ```ts usedBytes: null, totalBytes: null, freeBytes: null, ``` So the Android roots page (and the schema renderer anywhere) promises free space and a usage bar and always renders them empty. The df helper already exists — `fsops.ts:322-343` (`df -Pk`, kB→bytes) — and the web routes use it with caching (`routes.ts:58`: "df is a subprocess; a listing must not pay for it on every keystroke"). Either wire the cached df result into `/s/roots`, or drop the three bindings from the schema. Wiring it is the better outcome: the README already documents the degradation story ("a deployment without coreutils shows no usage bar rather than a wrong one"), which maps cleanly to leaving the fields null when `df` is absent. ## 2. Scan and delete actions stream no progress on mobile The schema's `actions` block supports `progressTopic` (core `packages/sdk/ui.ts:305`) — an SSE topic whose `msg` payloads stream into a progress log until a message starts with `done` or `error`. The module's scan and delete actions (`ui.ts:147-171`, `ui.ts:248-263`) don't use it: they start a task run, return `runId`, and the mobile client sees nothing until the next refresh tick (15 s on the scan page). Wiring a topic gives mobile live progress with no schema change. One decision to make: the module's live topics are currently admin-gated (README: "Live topics are admin-gated"), but the scan action is available at `readRole` and delete at `writeRole` — a non-admin who can start the run wouldn't see its stream. Either a per-topic role matching the action's role, or an explicitly progress-only topic that carries no path data beyond what the actor already submitted. ## Context - The four remaining mobile gaps (typed-phrase confirm, download verb, file input, breadcrumb nav) are schema/renderer additions, tracked in OpsDeck/mobile. - Related: core PR OpsDeck/core#31 (component kit this module's web frontend needs), OpsDeck/core#32 (module-builder tsconfig, merged).
Owner

Both fixed in 1bedb0c on main.

1. Free-space fields. Wired rather than dropped, as you suggested. The df
helper moved out of routes.ts into fsops.ts as a shared UsageCache, so
/s/roots and the web routes go through the same 15-second cache instead of
each starting one — df is a subprocess and both surfaces ask about the same
roots. Absent df still yields null, so the documented degradation holds: no bar
rather than a wrong one.

Against a running server:

media   free 27934715904  used 4510720000  total 32445435904
photos  free 27934715904  used 4510720000  total 32445435904
backups free 27934715904  used 4510720000  total 32445435904

2. Progress topics. scan-progress and delete-progress, named on Delete,
Scan this directory and Start scan. A real scan watched over SSE:

event: filebrowser:scan-progress
data: {"msg":"13 entries · /tmp/srv/media"}
data: {"msg":"done · 13 entries"}

The done prefix is what ends the stream for a progressTopic client, so it
is the first word rather than merely present.

On the role decision. Core's gate is binary — ctx.events.adminOnly(topic)
and nothing finer — so "a per-topic role matching the action's role" is not
expressible directly. The streams follow the read role instead:
admin-only exactly when reads are, which is the default deployment. Pinning
them admin unconditionally would mean an operator allowed to start a scan
cannot watch the one they started, which seemed like the wrong half to keep.

What makes that safe is the payload rather than the gate, so that is where the
rule lives: a line carries counts and a name the subscriber could already list
— never file content, never a path outside the roots. Both your options
collapse into the same discipline once the gate cannot be finer than binary.
README updated to say this instead of the old flat "live topics are
admin-gated".

Not touched here: the up/:p redirect page stays until the Android renderer
can render parent (core PR OpsDeck/core#35 adds the field). Removing it now
would take the up-arrow off the phone.

Both fixed in `1bedb0c` on main. **1. Free-space fields.** Wired rather than dropped, as you suggested. The df helper moved out of `routes.ts` into `fsops.ts` as a shared `UsageCache`, so `/s/roots` and the web routes go through the *same* 15-second cache instead of each starting one — df is a subprocess and both surfaces ask about the same roots. Absent df still yields null, so the documented degradation holds: no bar rather than a wrong one. Against a running server: ``` media free 27934715904 used 4510720000 total 32445435904 photos free 27934715904 used 4510720000 total 32445435904 backups free 27934715904 used 4510720000 total 32445435904 ``` **2. Progress topics.** `scan-progress` and `delete-progress`, named on Delete, Scan this directory and Start scan. A real scan watched over SSE: ``` event: filebrowser:scan-progress data: {"msg":"13 entries · /tmp/srv/media"} data: {"msg":"done · 13 entries"} ``` The `done` prefix is what ends the stream for a `progressTopic` client, so it is the first word rather than merely present. **On the role decision.** Core's gate is binary — `ctx.events.adminOnly(topic)` and nothing finer — so "a per-topic role matching the action's role" is not expressible directly. The streams follow the **read role** instead: admin-only exactly when reads are, which is the default deployment. Pinning them admin unconditionally would mean an operator allowed to start a scan cannot watch the one they started, which seemed like the wrong half to keep. What makes that safe is the payload rather than the gate, so that is where the rule lives: a line carries counts and a name the subscriber could already list — never file content, never a path outside the roots. Both your options collapse into the same discipline once the gate cannot be finer than binary. README updated to say this instead of the old flat "live topics are admin-gated". Not touched here: the `up/:p` redirect page stays until the Android renderer can render `parent` (core PR OpsDeck/core#35 adds the field). Removing it now would take the up-arrow off the phone.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
OpsDeck/module_filebrowser#1
No description provided.