Skip to content

fix: verify membership writes, adapt the toolbar to its window, and give panes their own state - #12

Merged
TheJakeBraun merged 1 commit into
mainfrom
fix/audit-remainder
Aug 16, 2026
Merged

TheJakeBraun merged 1 commit into
mainfrom
fix/audit-remainder

Conversation

@TheJakeBraun

Copy link
Copy Markdown
Contributor

The remaining audit findings, plus the toolbar complaint that started this session.

Membership writes are now proved, not assumed

Every configuration write reads back, because Apple acknowledges an out-of-spec profile with a 2xx and silently drops it. Membership writes had no such check — "attached" was reported on the response code alone, and a member inlined into a blueprint create was reported attached purely because the create returned 2xx.

ApplyBlueprints now re-reads the relationships it wrote and confirms the tenant holds what each write claimed.

The cost is per relationship, not per member: attaching twenty configs to one blueprint is one extra GET. A verification whose cost scaled with the number of writes would discourage its own use on an API that rate-limits this hard — there's a test pinning that.

A read that fails downgrades the row to NOT VERIFIED rather than failing it: a GET that errored says nothing about the POST, the same posture the configuration read-back takes.

get blueprint was the last caller swallowing relationship errors

rels[rel], _ = ... rendered a 429 or 403 as an empty member list — in the table and in the --json abgui's inspector consumes. An affirmatively false answer from the screen you open to see what a blueprint carries. ListPackages is permission-gated and legitimately 403s, so this fired on a real condition.

The toolbar adapts instead of always shouting

Labelling every control was the first answer to "hovering says no text", and it traded one problem for another — the toolbar outgrew the window and clipped its last item at ~1090px.

Width is now spent where a word resolves an ambiguity:

titled icon + tooltip
Verify Configs, Apply… — near-identical checkmarks, one reads local files, the other writes the tenant Refresh, Workspace, Edit, Membership
New, Delete — generic glyphs on tenant writes

Every control has help text either way, which is what the original complaint was actually about.

Panes own their load state

One isLoading/loadError shared by every screen meant one screen's fetch was another screen's spinner and another screen's error: a slow Devices fetch spun Configurations, and an OS Releases failure — raisable from inside a device detail sheet — rendered on whatever list sat behind it. A generation counter existed purely to stop overlapping reads clobbering each other's message.

Each pane now answers loading(_:)/failure(_:) for itself. The shared isLoading is gone entirely — the plan has isPlanning, the seed has isSeeding. The dashboard reads back the failure of the load it just ran rather than whatever last touched a shared slot.

Main-actor file I/O

ArchiveFileView read the profile with a synchronous String(contentsOf:) inside the view body — up to Apple's 1 MiB cap, re-executed on every body evaluation. RunLogsView scanned the directory, probed fifty file tails and read the selected transcript synchronously from .task. Both now read off the main actor into state, and the log reader drops a result whose selection changed while it was loading.

Verification

Go: build, vet, gofmt, full suite green. 3 new tests — an unapplied membership write must not read as done, the one-read-per-relationship budget, and a failed re-read being "not verified" rather than failed. Swift: macOS CI is the gate.

🤖 Generated with Claude Code

…ive panes their own state

The remaining audit findings, plus the toolbar complaint that started this.

Membership writes are now proved, not assumed
---------------------------------------------
Every configuration write reads back, because Apple acknowledges an out-of-spec
profile with a 2xx and silently drops it. Membership writes had no such check:
"attached" was reported on the response code alone, and a member inlined into a
blueprint create was reported attached purely because the create returned 2xx.

ApplyBlueprints now re-reads the relationships it wrote and confirms the tenant
holds what each write claimed. The cost is per RELATIONSHIP, not per member —
attaching twenty configs to one blueprint is ONE extra GET. A verification whose
cost scaled with the number of writes would discourage its own use on an API
that rate-limits this hard. A read that FAILS downgrades the row to NOT VERIFIED
rather than failing it: a GET that errored says nothing about the POST, the same
posture the configuration read-back takes.

get blueprint was the last caller swallowing relationship errors
----------------------------------------------------------------
`rels[rel], _ = ...` rendered a 429 or a 403 as an empty member list, in the
table AND in the --json abgui's inspector consumes — an affirmatively false
answer from the screen you open to see what a blueprint carries. ListPackages is
permission-gated and legitimately 403s, so this fired on a real condition.
`status config`, `status device` and `apply -f` were fixed for this already.

The toolbar adapts instead of always shouting
---------------------------------------------
Labelling every control was the first answer to "hovering says no text", and it
traded one problem for another: the toolbar outgrew the window and clipped its
last item at ~1090px. Width is finite, so it is spent where a word resolves an
ambiguity — `Verify Configs` and `Apply…` wear near-identical checkmarks while
one reads local files and the other writes the tenant, and `New`/`Delete` are
generic glyphs on tenant writes. Refresh, Workspace, Edit and Membership keep a
legible glyph plus a tooltip. Every control has help text either way, which is
what the original complaint was actually about.

Panes own their load state
--------------------------
One `isLoading`/`loadError` shared by every screen meant one screen's fetch was
another screen's spinner and another screen's error: a slow Devices fetch spun
Configurations, and an OS Releases failure — raisable from inside a device
detail SHEET — rendered on whatever list sat behind it. A generation counter
existed purely to stop overlapping reads clobbering each other's message.

Each pane now answers `loading(_:)`/`failure(_:)` for itself. The shared
`isLoading` is gone entirely: the plan has `isPlanning`, the seed has
`isSeeding`. The dashboard's pass reads back the failure of the load it just
ran instead of whatever last touched a shared slot.

Main-actor file I/O
-------------------
ArchiveFileView read the profile with a synchronous String(contentsOf:) INSIDE
the view body — up to Apple's 1 MiB cap, re-executed on every body evaluation.
RunLogsView scanned the directory, probed fifty file tails and read the selected
transcript (capped at 5 MiB) synchronously from `.task`. Both now read off the
main actor into state, and the log reader drops a result whose selection changed
while it was loading.

Go: build, vet, gofmt, full suite green, 3 new tests covering an unapplied
membership write, the one-read-per-relationship budget, and a failed re-read
being "not verified" rather than failed. Swift: macOS CI is the gate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@TheJakeBraun
TheJakeBraun merged commit 4c61ae7 into main Aug 16, 2026
6 checks passed
@TheJakeBraun
TheJakeBraun deleted the fix/audit-remainder branch August 16, 2026 01:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant