- Status: done
- Date: 2026-06-05
- Specs touched: No design change —
APP_STORE.md# Catalog schema already promises the coarse per-appfootprint, andBRAIN_UI_PROTOCOL.md# install-plan already specs the box-specificfootprintblock; this realizes both in the skeleton. Two doc reconciliations:BRAIN_HOST_PROTOCOL.md'sGET /v1/system/statusexample now shows thedata_disk_free_bytes/data_disk_total_bytesfields the footprint reads, andBRAIN_UI_PROTOCOL.md's install-plan "makes no host calls" line is corrected to "no mutating host calls" (the footprint does read-only host + Docker queries).
Closes issue #70 (the middle link of #68, "on-disk footprint before install"). #69 landed the data model — object-form images with per-image sizes, storage.estimated_size, and the derived (*Manifest).Footprint(). This issue serves that data on two surfaces: the coarse footprint on every catalog browse Entry (the store grid's upper-bound number), and the box-specific install-plan footprint (sharper — images already pulled on this box are subtracted, plus the live free-space reading). #71 (the UI that renders both) was blocked on this.
Entry.Footprint manifest.Footprint—List()now sets it fromman.Footprint()(the derived sum from #69). Pure data hoist; the catalog list already parses each manifest, so there is no extra I/O. This is the same number for every box — the catalog-grid upper bound, before any local-cache subtraction.
(Storage) EstimatedSizeBytes() (int64, bool, error)— parsesstorage.estimated_size("10GB", "1.5GB", "512MB", "4096") to bytes with a tri-state return: unset →(0, false, nil), valid →(n, true, nil), malformed →(0, false, err). Theboolis what lets the install plan omitestimated_state_bytes(rather than report a misleading 0) when the manifest declares no estimate. Binary units throughout (GB = 2³⁰), matching the spec exampleestimated_size: "10GB"→ 10 737 418 240.
Free space on the data drive is a genuine host concern (statfs on /srv/malmo) — the one piece of the footprint the brain can't compute itself. Rather than a new endpoint, the existing disk-summary carrier grew two fields:
SystemStatus.DataDiskFreeBytes/DataDiskTotalBytes— advisory;0means "not measured". Doc comment ties them toBavail × Bsize/Blocks × Bsize.hostagent.DiskReporterseam +Agent.Diskfield — consumer-side interface (CLAUDE.md), mirroring theRAMReporterpattern from #38. The system-status handler reads it nil-guarded.internal/hostagent/diskusage— the reallinux-taggedsyscall.Statfsreporter on/srv/malmo, fails open to(0, 0)+slog.Errorso a missing data drive never bricks status.FakeDiskReporter(canned 412 GiB free / 1 TiB) wires the fake agent.
The transaction owner computes the box-specific estimate (it owns both the Docker driver and the host client; the API layer must not reach past it):
InstallFootprint{DownloadBytes, ImageDiskBytes, EstimatedStateBytes, HasEstimate, FreeBytes}+(*Manager) InstallFootprint(ctx, *manifest.Manifest). It walksman.Images, and for each subtracts images already present on this box:imagePresentinspects the digest-pinned ref (repo@sha256:…) viaDockerDriver.ImageInspect— present (no error) ⇒ skip, absent or blank-digest ⇒ count. So an app that reuses a base image you already have reads as cheaper.- Granularity is image-level, not layer-level — forced by the data shape: the catalog carries per-image sizes, not per-layer. A safe over-estimate (shared base layers across two new images are double-counted), documented in the function comment.
- Degrades, never fails — a malformed estimate is swallowed (→
HasEstimate false), a host-status error swallows toFreeBytes 0, and the image figures survive either. The footprint is advisory; it must never fail an install plan. HostDriverinterface grewSystemStatus(thehostclient.Clientalready satisfied it).
InstallPlanDTO.Footprint InstallPlanFootprint—{download_bytes, image_disk_bytes, estimated_state_bytes?, free_bytes}.estimated_state_bytesis a*int64withomitempty, set only whenHasEstimate, so it is absent (not 0) when the manifest declares no estimate.buildInstallPlanstays pure (permissions + scope menus, trivially unit-testable); the box-specific footprint is attached in theinstallPlanhandler vias.life.InstallFootprint(ctx, man), because it queries Docker + the host.- Regenerated
api/openapi.{json,yaml}+web-ui/src/generated/openapi.ts— the wire types are committed artifacts;make openapi+npm run gen:api(nonpm install) keep CI's freshness gates green. Mechanical; the UI that consumes them is #71.
APP_STORE.md# Catalog schema — the coarse per-appfootprintnow rides every browseEntry.BRAIN_UI_PROTOCOL.md# install-plan — the box-specific, incrementalfootprintblock (image bytes minus what's already pulled, the parsed state estimate, live free space) is realized; the "advisory" framing held (read-only, degrades to zeros).BRAIN_HOST_PROTOCOL.md# system/status —data_disk_free_bytes/data_disk_total_bytesadded to the example.APP_MANIFEST.md# Storage —storage.estimated_sizeis now parsed to bytes.
- Image-level, not layer-level subtraction. Two new images sharing a base layer count that layer twice — a safe over-estimate. Layer-level would need per-layer catalog data (
docker manifest inspectblob sizes), which #69 deliberately didn't carry. free_bytesfrom the fake agent is canned (412 GiB / 1 TiB). The realdiskusagereporter doesstatfson/srv/malmo, but that path only exists once the real host-agent + data-drive assembly land (still VM-deferred).- Footprint sub-failures are silent to the user. A Docker or host hiccup degrades the numbers to zeros (logged at the brain) rather than surfacing "couldn't estimate" — the consent screen just shows a conservative figure. Whether the UI should distinguish "0 because cached" from "0 because we couldn't measure" is a #71 question.
- No caching. Each install-plan fetch re-inspects every image against Docker. Fine at this scale (install-plan is a deliberate user action, not a poll); revisit only if it shows up.
- #71 — done in PR #95 (
install-footprint-ui.md). The store-card~sizeand the consent-dialog Storage block (download line, space line, not-enough-space warning) landed on this branch before this PR merged. - Real
diskusageexercise — once the VM host-agent +/srv/malmoassembly exist, confirm thestatfsreporter against a real data drive.