T9.8 - D7: archiving stops reading as deletion - for project admins

Archiving already froze a project (the server refuses every write); what did
not exist was the way back in. Now:

- GET /api/projects?archived=only|all filters the answer BY PER-PROJECT ROLE:
  a project admin (or super/app admin) on THAT project sees it; everyone else
  receives an empty list from the same request - archived projects appear
  nowhere for them, counts and pickers included (the default listing already
  excluded them for everyone; asking is what got gated). Admin-on-Job-A does
  not surface archived Job B.
- The launcher gains a visibly separate, labelled "Archived projects"
  section (dashed border, read-only stated in words), rendered only when the
  server returns rows. Opening one makes it active; the launcher's reconcile
  learned that an active project whose stored summary says archived:true was
  opened ON PURPOSE and keeps it, while a project archived out from under
  someone still drops with the existing explanation.
- The creator shows ARCHIVED - READ-ONLY where the project is named (both
  ctx-bar branches, from the SERVER's answer - the page's project comes from
  the URL, so a stale local summary is not trusted) and refuses saves with a
  reason before the round trip. The courtesy; the server's refusal is the
  rule, verified by calling the endpoints directly (wp upsert AND the
  material-list write both refuse with "archived" even for an admin).
- No unarchive button, no second mechanism, and it fits at 390px.

Verification (each probe run alone): NEW tests/archived_check.py 15/15.
Regressions: launcher_check 58/58, sample_check 10/10, export_check 20/20,
frame_check 38/38.

Items: D7

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 13:58:51 -07:00
parent 6201fcfb4a
commit 771672273d
6 changed files with 264 additions and 4 deletions

View File

@@ -52,6 +52,15 @@
.catch(function () { return readLocal(); });
},
// D7 / T9.8: the way back in, for project admins. The server filters the
// answer by per-project role; everyone else simply receives [].
listArchivedProjects: function () {
return fetch(API + '/projects?archived=only', { headers: { 'Accept': 'application/json' } })
.then(function (r) { if (!r.ok) throw 0; return r.json(); })
.then(function (rows) { return (rows || []).filter(function (p) { return p.archived; }); })
.catch(function () { return []; });
},
get: function (id) {
return fetch(API + '/projects/' + encodeURIComponent(id))
.then(function (r) { if (!r.ok) throw 0; return r.json(); })