Per-user designs with sharing, microversion history and named releases.
The three design tools -- EngineDesign, pid-designer, recovery-calculator --
share this router. They differ in only four things, all of them
:class:`DesignStore` fields: the ``<app>`` path segment, the route prefix, the
payload shape (``{"config": ...}`` vs ``{"nodes": ..., "edges": ...}``), and the
noun used in user-facing strings.
Three tiers of durability:
* **autosave** -> a fast working copy on the volume (``current.json``). Never
hits S3. ``/load`` always returns it -- the freshest state.
* **microversions** -> automatic snapshots pushed to storage over time
(throttled to the store's ``micro_interval`` while editing) plus a best-effort
``/flush`` when the tab closes. The safety net; S3 object versioning + a
lifecycle rule prunes old ones in prod, files on the volume in dev.
* **releases** -> explicit, immutable, user-named milestones ("0.1").
Version history lives in :mod:`stardesign.storage`. This module owns the working
copy and the per-user document index only. Auth is Caddy's job; a missing header
just means the ``local`` user (never a rejection). See :mod:`stardesign.userdata`.
Sharing
-------
A design lives in its creator's folder, but that is only *where it lives* -- it
is not a privilege level. The creator and everyone in the record's ``sharedWith``
list are equally editors; there is no owner-only operation. Any design can also
be read and copied by anyone, which is what the view-only tree in the UI browses.
Every design-scoped handler resolves through ``_resolve_doc``, which is the one
place cross-user access is granted -- and, since this module is shared, the one
place in the whole fleet. It matters that it returns a :class:`DocRef` rather
than a directory name: the write helpers below happily ``mkdir`` whatever path
they are handed, so a handler that *forgot* ``?owner=`` would not 403 -- it would
silently write a real ``current.json`` into the caller's own folder with no index
entry behind it, and the edit would vanish on reload. ``/flush`` rides a
``sendBeacon`` whose response nobody reads, so that failure would be completely
silent on the most loss-prone path. A ``DocRef`` only exists if an index record
was found, so that cannot happen.
Checkouts
---------
Concurrent editing is not resolved, it is prevented. At most one person holds a
design's write token at a time, and only the holder may save. Taking it is
explicit (opening a design never takes it, so viewing never blocks a colleague);
it lapses on its own after ``lock_ttl`` without a save, and on tab close.
Hiding the tab -- switching away, minimising, closing a lid -- does not release
it; only a real close does.
A lapse frees the design for whoever acts first. If that is the holder -- they
came back and saved or touched the canvas, and nobody took it in the meantime --
the hold is simply theirs again (``DesignStore.claim_for_write``); refusing
them was how edits were lost with nobody else anywhere near the design. If
someone else got there first, the holder's saves are refused, and ``/rescue``
keeps what they carried as a new design of the holder's own.
The compare-and-set runs inside ``_index_lock``, the same ``flock`` that already
serialises index writes, so two simultaneous takes cannot both succeed. That
holds across the several workers each API runs.
**A constraint worth knowing:** ``flock`` is per-machine. All three design tools
run on the one apps machine sharing the ``userdata`` volume, so this is sound
today. Running a design tool on a second machine against the same volume would
break the guarantee silently, and needs a different primitive.
Open to all, and the main design
--------------------------------
Opt-in modes, all off by default so the apps that do not set them behave
exactly as before (pid-designer turns both on):
* ``open_to_all`` -- every design is editable by everyone; checkouts are what
stop two people saving at once. ``recent_days`` then keeps the editable list
short: other people's designs not updated within that window move to
``/browse``, where they can still be opened. Your own never age out.
* ``featured`` -- an admin may mark one design as the team's main one. Everyone
can open it, it heads every list and the window never hides it, and **only an
admin may change it** (unless ``main_admin_only`` is off): everyone else takes
a copy. Admins come from ``is_admin`` (see :mod:`stardesign.admins`); the
default is nobody.
The STAR collection
-------------------
``curated`` is the third mode, and the one pid-designer runs (with
``open_to_all`` off and ``main_admin_only`` off):
* Admins choose the **STAR set** (``PUT/DELETE /{id}/star``, kept in the
app's ``star.json``). Everyone sees those; the main design is always one.
* Everyone else sees their own designs and those shared with them. **Admins see
everything**, and may edit everything.
* Editing is the creator, the share list, and admins. Only the creator or an
admin may change the share list -- here it is a real boundary, not
housekeeping.
* Anyone who can see a design may **ask to edit** it (``POST /{id}/access``);
the creator or an admin approves (they join the share list) or denies.
``GET /requests`` lists what the caller may answer.
* "Can read" narrows to "can see": nobody copies a design they cannot open.
There is deliberately no delete endpoint: a design is editable by more than one
person, so a delete button is one misclick away from destroying a group project
with only a server-admin restore behind it. Cleanup is an admin operation on the
volume.