feat(spaces): per-space sizes on a day-old cache, member counts, and the page to show them #354

Merged
Cordy merged 26 commits from spaces-page into main 2026-08-28 02:53:42 +00:00
Owner

The Spaces page looked empty for two unrelated reasons: the instance had no app-owned spaces and loadSpaces returned early with no zero state, and the renderer had been reading sp.members and sp.size since it was built while the backend never sent either. Those two columns have shown a permanent .

Sizes

storage.PrefixSizer is a new optional capability, feature-detected like Usager: total bytes per immediate child of a prefix, in one pass.

The cost is asymmetric and worth recording. On S3 this is the same flat bucket listing Usage() already performs — per-space totals cost what the global total already costs, once bucketed by key. On POSIX Usage() is a statfs costing nothing, and this is a genuine tree walk. That asymmetry is why the result is cached.

api.SpaceSizes caches for 24h and refreshes in the background, shaped after UpdateCheck deliberately — its comment records that opening Administration once blocked on an outbound call, and a size sweep would reintroduce that while growing with the customer's data. So Get never blocks: a cold cache returns nothing and the page says sizes are being measured.

Two properties are pinned by tests because they are silent when wrong:

  • a failed sweep does not stamp computedAt, or an empty result would look authoritative for a day;
  • ten concurrent readers start one computation.

Sizes read from the undecorated driver. The decorator stack does not forward the capability, and the raw number is the honest one anyway — it is what the backend actually stores, ciphertext included.

Page

Header tally, a real zero state, the deactivated-bytes figure the old code computed and then discarded, an as-of line for the cache, and the table of what a new space inherits.

A directory-managed space shows its group instead of an owner — it belongs to the group, and "Owned by" there would name someone who does not hold that role.

Who may create: Any signed-in user carries no "Deployment setting" badge: that is the current behaviour but not a configurable one, and the badge would claim a control that does not exist.

spacesTally is pure, in a marked block, and tested — an unmeasured space must not count as zero bytes, or a fresh instance reports "0 B across all spaces" with total confidence. Second user of the web/test/ harness from #353.

Still missing, deliberately

IdP-group spaces are not listed. list enumerates the app-owned store only, so directory-managed spaces remain invisible here. Fixing it needs a Keycloak group enumeration that does not exist yet (filtering by owner attribute, excluding the read-only suffix groups). The renderer already handles sp.group for when it lands.

SegmentOf's original test was self-contradictory — it wanted a bare child to resolve to itself and a sibling file to resolve to nothing, which are the same shape as paths. The test was corrected, not the code.

Verification

go build, go vet, go test ./internal/..., node --check, and both web suites pass.

The Spaces page looked empty for two unrelated reasons: the instance had no app-owned spaces and `loadSpaces` returned early with no zero state, and the renderer had been reading `sp.members` and `sp.size` since it was built while the backend never sent either. Those two columns have shown a permanent `—`. ## Sizes `storage.PrefixSizer` is a new optional capability, feature-detected like `Usager`: total bytes per immediate child of a prefix, in one pass. The cost is asymmetric and worth recording. On **S3** this is the same flat bucket listing `Usage()` already performs — per-space totals cost what the global total already costs, once bucketed by key. On **POSIX** `Usage()` is a `statfs` costing nothing, and this is a genuine tree walk. That asymmetry is why the result is cached. `api.SpaceSizes` caches for 24h and refreshes in the background, shaped after `UpdateCheck` deliberately — its comment records that opening Administration once blocked on an outbound call, and a size sweep would reintroduce that while growing with the customer's data. So `Get` never blocks: a cold cache returns nothing and the page says sizes are being measured. Two properties are pinned by tests because they are silent when wrong: - a **failed** sweep does not stamp `computedAt`, or an empty result would look authoritative for a day; - ten concurrent readers start **one** computation. Sizes read from the **undecorated** driver. The decorator stack does not forward the capability, and the raw number is the honest one anyway — it is what the backend actually stores, ciphertext included. ## Page Header tally, a real zero state, the deactivated-bytes figure the old code computed and then discarded, an as-of line for the cache, and the table of what a new space inherits. A directory-managed space shows its **group** instead of an owner — it belongs to the group, and "Owned by" there would name someone who does not hold that role. `Who may create: Any signed-in user` carries **no** "Deployment setting" badge: that is the current behaviour but not a configurable one, and the badge would claim a control that does not exist. `spacesTally` is pure, in a marked block, and tested — an unmeasured space must not count as zero bytes, or a fresh instance reports "0 B across all spaces" with total confidence. Second user of the `web/test/` harness from #353. ## Still missing, deliberately **IdP-group spaces are not listed.** `list` enumerates the app-owned store only, so directory-managed spaces remain invisible here. Fixing it needs a Keycloak group enumeration that does not exist yet (filtering by owner attribute, excluding the read-only suffix groups). The renderer already handles `sp.group` for when it lands. `SegmentOf`'s original test was self-contradictory — it wanted a bare child to resolve to itself *and* a sibling file to resolve to nothing, which are the same shape as paths. The test was corrected, not the code. ## Verification `go build`, `go vet`, `go test ./internal/...`, `node --check`, and both web suites pass.
SegmentOf attributes an object to a space; POSIX walks each child of the
prefix; SpaceSizes caches the result for 24h and refreshes it in the
background, never on the request path.

The cost is asymmetric and worth recording: on S3 the sweep is the same
flat bucket listing Usage() already performs, so per-space totals are
nearly free once bucketed by key. On a filesystem Usage() is a statfs
costing nothing, and this is a genuine tree walk -- which is what the
cache is for.

Shaped after UpdateCheck deliberately. Its comment records that opening
Administration once blocked on an outbound call; a size sweep would
reintroduce that and grow with the customer's data.
GET /api/v1/spaces gains members, size and sizesComputedAt. The frontend
has been reading sp.members and sp.size since the inventory was built --
the backend simply never sent them, so both columns have been showing a
permanent dash.

Size is a pointer: absent means not measured yet, zero means measured and
empty. Conflating them would report a fresh instance as holding nothing.

Sizes come from the UNDECORATED driver. The decorator stack does not
forward PrefixSizer, and the raw number is the honest one anyway -- it is
what the backend actually stores, ciphertext included.
The inventory was already there but returned early on an empty list, so
an instance with no spaces showed a lone paragraph and nothing else.
There is now a zero state that says what would appear.

Adds the header tally, the deactivated-bytes figure the old code
computed and then discarded, an as-of line for the day-old size cache,
and the table of what a new space inherits.

A directory-managed space shows its group instead of an owner: it
belongs to the group, and 'Owned by' there would name someone who does
not hold that role.

spacesTally is pure and tested -- an unmeasured space must not be
counted as zero bytes, or a fresh instance reports 0 B with confidence.
style(spaces): rules for the tally bar and empty state
All checks were successful
ci / test-and-build (pull_request) Successful in 43s
6f75b682af
The renderer introduced classes with nothing behind them, which would
have stacked the header numbers as plain paragraphs. Scoped to
#adminview beside the sibling .spc rules, and using tokens the
stylesheet already defines rather than literals that would drift from
the themes.
Cordy merged commit f644fc31ac into main 2026-08-28 02:53:42 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
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: Cordy/Cairn#354
No description provided.