feat(plot): save and open projects, on this device or on the plotter - #78
Merged
Merged
Conversation
Drawings were transient. The session persists — on the Pi, shared with every client — but there is exactly one of it: importing the next job overwrites the last, and there is no way to put a plot aside and come back to it. A headless machine in a workshop should hold a library of plots, not the most recent one. A project is the whole job, and it carries its pens. Artwork names pens by id, so a project opened on another machine — or after the library has been edited — would otherwise render in whatever pens happen to exist there. Opening merges them in rather than replacing the library, so nothing the operator defined disappears. Versioned from the first release: these files outlive the app that wrote them, and "we will add a version when we need one" means the first file that needs it cannot be read. A file from a newer version is refused with a message rather than opened with its unknown fields silently dropped, and `readProject` returns the reason instead of throwing, because every caller wants to show it. The name is a security boundary. It arrives over a socket with no authentication and becomes a filesystem path on the Pi, so it goes through a pure, unit-tested sanitiser shared with the client — and the daemon then checks the *resolved* path is inside the projects directory anyway. Either check would do; having both means a bug in one is not a way out of the directory. Saves are atomic, because a project half-written by a power cut is unopenable and the operator would not find out until they came to plot it. A load is addressed to the client that asked: opening a project replaces what is on screen, which is not something to do to another operator's session because someone else clicked a name. The persist effect and the project saver now share one session builder. Two copies of that field list would drift, and the field someone forgets to add to the second one is a field that quietly vanishes from every saved project. Coverage floors raised to 72/68/62/72 — the pure core is at 75% and a floor that cannot fail is not a gate. Verified against the real daemon: save writes into the projects directory and broadcasts the listing, another client sees it on attach, a load reaches only the asking client, `../escaped` lands *inside* the directory under a sanitised name, a nameless project is refused, a missing one reports it, delete removes and re-broadcasts, and the library survives a restart. And in the browser: save to the plotter, clear the page, reopen from the list, drawing back; opening a file merges its pens; a stray JSON and a future-version file are both refused with the right message. Closes #8 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
10 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #8 — the last of the epic's feature issues.
Why
Drawings were transient. The session persists — on the Pi, shared with every client — but there's exactly one of it: importing the next job overwrites the last, and there's no way to put a plot aside and come back to it. A headless machine in a workshop should hold a library of plots, not the most recent one.
Decisions
readProjectreturns the reason instead of throwing, because every caller wants to show it.Also
Coverage floors raised 50/48/40/50 → 72/68/62/72; the pure core is at 75% and a floor that can't fail isn't a gate.
Verification
mise run cigreen — 425 tests.Against the real daemon (spare port, temp directories): save writes into the projects directory and broadcasts the listing; a second client sees it on attach; a load reaches only the asking client;
../escapedlands inside the directory under a sanitised name and nothing is written to the parent; a nameless project is refused; a missing one reports it; delete removes and re-broadcasts; and the library survives a daemon restart. 13/13 checks.In the browser: save to the plotter → ⌘A + Delete → reopen from the list → the drawing is back with its pens; opening a project file merges its pen into the library; a stray JSON and a future-version file are each refused with the right message.
🤖 Generated with Claude Code