Skip to content

feat(plot): save and open projects, on this device or on the plotter - #78

Merged
BernardJen merged 1 commit into
mainfrom
feat/projects
Sep 12, 2026
Merged

BernardJen merged 1 commit into
mainfrom
feat/projects

Conversation

@BernardJen

Copy link
Copy Markdown
Contributor

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

  • A project 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. 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'll add a version when we need one" means the first file that needs it can't be read. A file from a newer version is refused with a message rather than opened with its unknown fields silently dropped. readProject returns the reason instead of throwing, because every caller wants to show it.
  • The name is a security boundary, checked twice. It arrives over a socket with no authentication and becomes a filesystem path on the Pi: it goes through a pure, unit-tested sanitiser shared with the client, and the daemon checks the resolved path is inside the projects directory. Either alone would do; both means a bug in one isn't a way out of the directory.
  • Saves are atomic — a project half-written by a power cut is unopenable, and the operator wouldn't find out until they came to plot it.
  • A load is addressed to the client that asked. Opening a project replaces what's on screen, which isn't something to do to another operator's session because someone else clicked a name.
  • One session builder, shared by the persist effect and the project saver. 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.

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 ci green — 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; ../escaped lands 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

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>
@BernardJen
BernardJen merged commit ace2e10 into main Sep 12, 2026
2 checks passed
@BernardJen
BernardJen deleted the feat/projects branch September 12, 2026 11:03
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.

Save and load projects (local machine and Raspberry Pi)

1 participant