QA re-verification run: confirm the fixes for the #656 findings (post-fix pass for Opus) #697

Open
opened 2026-09-23 18:15:47 +00:00 by Cordy · 17 comments
Owner

This is the post-fix companion to the #656 QA pass. The findings (#657–#695) are being fixed in batches; each batch that ships gets a section below with the deployed version and per-issue verification steps. Do not start until Nikola points you here — batches are still landing and each section names the version it needs.

Protocol (same as #656 unless stated)

  • Hosts: files.c0rdyceps.ch (enc/Keycloak) and files-bao.c0rdyceps.ch (OpenBao/local). Sign-ins as in #656: nikola-test via Chrome on both hosts, sharer1 in-app on files-bao only.
  • Work inside _qa/ folders. Screenshots as filename references. One comment per finding here (PASS/FAIL + evidence); do NOT open new issues for regressions of these fixes — a FAIL comment referencing the original issue number is enough. Genuinely new findings unrelated to a fix still get their own issue on the v0.7 milestone.
  • Check the version footer/admin Updates panel first and confirm it is at least the version the section names. Redeploys log everyone out — expect fresh sign-ins.

Batch 1 — sharing correctness, v0.6.248 (#680 #676 #677 #678 #682 #679)

  1. #680 (Enter mints a public link): share dialog on a _qa file → type into "Search people and groups" → press Enter. Expect: NO public link created (GET /api/v1/shares unchanged), no Public chip. Enter with a picked candidate should act as Add. Creating a link still works via the Create link button.
  2. #676 (shared-with-me file rows): as sharer1, open Shares → Shared with me → click a FILE row. Expect: parent listing opens with the details pane on that file, and the row icon is a file icon, not a folder. A folder row still navigates into it.
  3. #677 (internal links): as nikola-test copy an internal link (details pane or share dialog) — it now looks like #loc:nikola-test:/…. (a) Open it in a fresh tab as nikola-test: folder loads, or file opens parent + details pane. (b) Paste it into an ALREADY-OPEN tab: same result via hashchange, no reload needed. (c) As sharer1 (files-bao, for something shared to them) the link should land in their /shared/nikola-test/… view — note: deep paths inside a shared folder resolve to the shared root's basename only, by design. (d) An old bare-path link still works for the owner.
  4. #678 (anonymous drop upload): create an upload-enabled public link on a _qa folder on the ENCRYPTED instance (files), open it signed-out, drop a file. Expect: upload succeeds (was: silent 500). The written file belongs to the link creator.
  5. #682 (existing links in the dialog): create a public link, close and reopen the share dialog on the same file. Expect: the Public-link section lists the existing link with expiry, badges (password/file-drop), Copy and Revoke. Revoke removes it and the row chip updates.
  6. #679 (read-only empty folder): as sharer1, open an empty folder shared view-only. Expect: empty state says view-only and shows NO upload/create-folder actions. A writable empty folder keeps them.

(further batches will be appended as they ship)

This is the post-fix companion to the #656 QA pass. The findings (#657–#695) are being fixed in batches; each batch that ships gets a section below with the deployed version and per-issue verification steps. **Do not start until Nikola points you here** — batches are still landing and each section names the version it needs. ## Protocol (same as #656 unless stated) - Hosts: `files.c0rdyceps.ch` (enc/Keycloak) and `files-bao.c0rdyceps.ch` (OpenBao/local). Sign-ins as in #656: nikola-test via Chrome on both hosts, sharer1 in-app on files-bao only. - Work inside `_qa/` folders. Screenshots as filename references. One comment per finding here (PASS/FAIL + evidence); do NOT open new issues for regressions of these fixes — a FAIL comment referencing the original issue number is enough. Genuinely new findings unrelated to a fix still get their own issue on the v0.7 milestone. - Check the version footer/admin Updates panel first and confirm it is at least the version the section names. Redeploys log everyone out — expect fresh sign-ins. ## Batch 1 — sharing correctness, **v0.6.248** (#680 #676 #677 #678 #682 #679) 1. **#680 (Enter mints a public link):** share dialog on a `_qa` file → type into "Search people and groups" → press Enter. Expect: NO public link created (`GET /api/v1/shares` unchanged), no Public chip. Enter with a picked candidate should act as Add. Creating a link still works via the Create link button. 2. **#676 (shared-with-me file rows):** as sharer1, open Shares → Shared with me → click a FILE row. Expect: parent listing opens with the details pane on that file, and the row icon is a file icon, not a folder. A folder row still navigates into it. 3. **#677 (internal links):** as nikola-test copy an internal link (details pane or share dialog) — it now looks like `#loc:nikola-test:/…`. (a) Open it in a fresh tab as nikola-test: folder loads, or file opens parent + details pane. (b) Paste it into an ALREADY-OPEN tab: same result via hashchange, no reload needed. (c) As sharer1 (files-bao, for something shared to them) the link should land in their `/shared/nikola-test/…` view — note: deep paths inside a shared folder resolve to the shared root's basename only, by design. (d) An old bare-path link still works for the owner. 4. **#678 (anonymous drop upload):** create an upload-enabled public link on a `_qa` folder on the ENCRYPTED instance (files), open it signed-out, drop a file. Expect: upload succeeds (was: silent 500). The written file belongs to the link creator. 5. **#682 (existing links in the dialog):** create a public link, close and reopen the share dialog on the same file. Expect: the Public-link section lists the existing link with expiry, badges (password/file-drop), Copy and Revoke. Revoke removes it and the row chip updates. 6. **#679 (read-only empty folder):** as sharer1, open an empty folder shared view-only. Expect: empty state says view-only and shows NO upload/create-folder actions. A writable empty folder keeps them. *(further batches will be appended as they ship)*
Author
Owner

Batch 2 — admin and departed-owner correctness, v0.6.249 (#686 #687 #688 #689 #690-partial #658 #657)

All on files-bao unless noted; needs at least v0.6.249 on the footer.

  1. #686 (liveness disagreement): as nikola-test admin, (a) share panel people search for sharer2 (or any departed name from the Departed tab) — expect it NOT offered as a recipient anymore (live accounts still are); (b) Administration → Users & access → Accounts → Directory accounts — the departed account carries a "Departed — the directory no longer resolves this account" chip and offers NO Make-admin action. Live accounts unchanged.
  2. #687 (group Used by): Groups tab, qa656 (holds the _qa/drop view grant) — Used by now reads "1 share" (plus any other bits), not "Not used". A group with no grants still reads Not used.
  3. #688 (Active links): Administration → Sharing → Active links. Expect: card-styled rows, NO group-grant rows, every user's links listed with owner shown — including sharer2's frozen /home/sharer2/README.md link flagged "Frozen — owner departed" with no Copy button. Revoke works for links you don't own (admin revoke) — please only exercise it on _qa links.
  4. #689 (transfer dialog): Departed owners → sharer2 → Transfer ownership. The full-handover line resolves to either real counts ("N files, size") or "contents could not be measured — …" — never a permanent "computing…". New owner is a styled canon field. With 0 grants the shares-only option's refusal now reads "No person or group shares to move — public links stay frozen with the estate…".
  5. #690 (partial): Spaces admin — sizes stamp no longer says "ago ago"; IdP/managed rows show "—" in Members instead of 0; New group dialog carries the hint "Members also see a shared space with this name under /spaces for now." The structural question (should app groups provision spaces at all) is deliberately still open on #690 — do not fail this batch on it.
  6. #658 (Change password for IdP accounts): avatar menu as nikola-test (OIDC) on both hosts — no "Change password" entry. On files-bao as sharer1 (local account) the entry is still there and works.
  7. #657 (initials): top-bar avatar and the menu-header avatar show the SAME initials for nikola-test (both NT under the split-on-separators rule). Check both hosts.
## Batch 2 — admin and departed-owner correctness, **v0.6.249** (#686 #687 #688 #689 #690-partial #658 #657) All on **files-bao** unless noted; needs at least v0.6.249 on the footer. 1. **#686 (liveness disagreement):** as nikola-test admin, (a) share panel people search for `sharer2` (or any departed name from the Departed tab) — expect it NOT offered as a recipient anymore (live accounts still are); (b) Administration → Users & access → Accounts → Directory accounts — the departed account carries a "Departed — the directory no longer resolves this account" chip and offers NO Make-admin action. Live accounts unchanged. 2. **#687 (group Used by):** Groups tab, `qa656` (holds the `_qa/drop` view grant) — Used by now reads "1 share" (plus any other bits), not "Not used". A group with no grants still reads Not used. 3. **#688 (Active links):** Administration → Sharing → Active links. Expect: card-styled rows, NO group-grant rows, every user's links listed with owner shown — including sharer2's frozen `/home/sharer2/README.md` link flagged "Frozen — owner departed" with no Copy button. Revoke works for links you don't own (admin revoke) — please only exercise it on `_qa` links. 4. **#689 (transfer dialog):** Departed owners → sharer2 → Transfer ownership. The full-handover line resolves to either real counts ("N files, size") or "contents could not be measured — …" — never a permanent "computing…". New owner is a styled canon field. With 0 grants the shares-only option's refusal now reads "No person or group shares to move — public links stay frozen with the estate…". 5. **#690 (partial):** Spaces admin — sizes stamp no longer says "ago ago"; IdP/managed rows show "—" in Members instead of 0; New group dialog carries the hint "Members also see a shared space with this name under /spaces for now." The structural question (should app groups provision spaces at all) is deliberately still open on #690 — do not fail this batch on it. 6. **#658 (Change password for IdP accounts):** avatar menu as nikola-test (OIDC) on both hosts — no "Change password" entry. On files-bao as sharer1 (local account) the entry is still there and works. 7. **#657 (initials):** top-bar avatar and the menu-header avatar show the SAME initials for nikola-test (both NT under the split-on-separators rule). Check both hosts.
Author
Owner

Needs at least v0.6.250. Use files-bao _qa/ for the live tests; the two bogus holds live on files.

  1. #685 (resolve reports holds): on files-bao place a hold on an existing _qa file, then Retention & holds → Retention → "What applies to a path" → resolve that exact path, its PARENT folder, and a path INSIDE a held folder. Expect each answer to include a bordered "Legal hold: <path> — placed by … · date · reason" line. A path with no hold shows none.
  2. #685 (placement validation): try to place a hold on /Fall 1 (or any nonexistent path) — expect a 400-style inline error "no such path … check the namespace (/home//… or /spaces//…)" and NO new hold in the list. A real storage path still gets a hold (201, appears in the list).
  3. #685 (files cleanup + regression evidence): on files, the two pre-existing bogus holds ("/Fall 1", "/Spaces/Fall 1") are still listed — lift them via the new dialog and note it in your cleanup comment. Re-placing them must now be refused.
  4. #683 (lift confirmation): Lift on any hold now opens a dialog with a warning naming the path, a fact line (placed by, when, original reason), and a "Reason for lifting" field. Cancel leaves the hold. Confirm lifts it, and the audit log's hold-lift event carries the reason (check Audit log → the event's note, or the raw JSONL if the viewer doesn't render notes — the field is note).
  5. Regression: enforcement itself unchanged — a held file still refuses delete/rename until lifted.
## Batch 3 — legal holds, **v0.6.250** (#685 #683) Needs at least v0.6.250. Use files-bao `_qa/` for the live tests; the two bogus holds live on **files**. 1. **#685 (resolve reports holds):** on files-bao place a hold on an existing `_qa` file, then Retention & holds → Retention → "What applies to a path" → resolve that exact path, its PARENT folder, and a path INSIDE a held folder. Expect each answer to include a bordered "Legal hold: `<path>` — placed by … · date · reason" line. A path with no hold shows none. 2. **#685 (placement validation):** try to place a hold on `/Fall 1` (or any nonexistent path) — expect a 400-style inline error "no such path … check the namespace (/home/<user>/… or /spaces/<name>/…)" and NO new hold in the list. A real storage path still gets a hold (201, appears in the list). 3. **#685 (files cleanup + regression evidence):** on files, the two pre-existing bogus holds ("/Fall 1", "/Spaces/Fall 1") are still listed — lift them via the new dialog and note it in your cleanup comment. Re-placing them must now be refused. 4. **#683 (lift confirmation):** Lift on any hold now opens a dialog with a warning naming the path, a fact line (placed by, when, original reason), and a "Reason for lifting" field. Cancel leaves the hold. Confirm lifts it, and the audit log's `hold-lift` event carries the reason (check Audit log → the event's note, or the raw JSONL if the viewer doesn't render notes — the field is `note`). 5. Regression: enforcement itself unchanged — a held file still refuses delete/rename until lifted.
Author
Owner

Batch 4a — protocol honesty, v0.6.251 (#665 #670 #671 #672)

Needs at least v0.6.251.

  1. #670 (version minting on S3/Garage): on files (enc), edit any _qa office file in Collabora, save, Back. Expect: server log shows NO "versions: mint failed … Invalid signature", details pane → Versions lists at least one previous version with Restore/Download, and GET /api/v1/versions is non-empty for the file. Note: versions only exist for edits made AFTER this deploy — the old silent failures are unrecoverable.
  2. #665 (WebDAV collection time): PROPFIND /dav/home/_qa/ Depth:1 — the collection's own response either carries a real getlastmodified or omits the property entirely; no "Mon, 01 Jan 0001" anywhere in the multistatus.
  3. #671 (post-edit refresh): edit an office file, type, save, press the editor bar's Back. Expect the row AND the open details pane to show the post-save Modified time and size immediately, no manual reload.
  4. #672 (spreadsheet hero): select qa-sheet.xlsx — the pane hero renders the first page via the converter on BOTH hosts; GET /api/v1/preview/formats now includes xlsx, xls, ods, csv. A csv preview is worth one spot check.
  5. Regression: docx/pptx heroes and the rclone interop should behave as before (the S3 signing change also covers every normal object path — a quick upload/download/list pass on files confirms nothing regressed).

(#659 plaintext sizes and #664 preserved Modified are deliberately NOT in this batch — they ship next as batch 4b.)

## Batch 4a — protocol honesty, **v0.6.251** (#665 #670 #671 #672) Needs at least v0.6.251. 1. **#670 (version minting on S3/Garage):** on files (enc), edit any `_qa` office file in Collabora, save, Back. Expect: server log shows NO "versions: mint failed … Invalid signature", details pane → Versions lists at least one previous version with Restore/Download, and `GET /api/v1/versions` is non-empty for the file. Note: versions only exist for edits made AFTER this deploy — the old silent failures are unrecoverable. 2. **#665 (WebDAV collection time):** `PROPFIND /dav/home/_qa/ Depth:1` — the collection's own response either carries a real `getlastmodified` or omits the property entirely; no "Mon, 01 Jan 0001" anywhere in the multistatus. 3. **#671 (post-edit refresh):** edit an office file, type, save, press the editor bar's Back. Expect the row AND the open details pane to show the post-save Modified time and size immediately, no manual reload. 4. **#672 (spreadsheet hero):** select `qa-sheet.xlsx` — the pane hero renders the first page via the converter on BOTH hosts; `GET /api/v1/preview/formats` now includes xlsx, xls, ods, csv. A csv preview is worth one spot check. 5. Regression: docx/pptx heroes and the rclone interop should behave as before (the S3 signing change also covers every normal object path — a quick upload/download/list pass on files confirms nothing regressed). *(#659 plaintext sizes and #664 preserved Modified are deliberately NOT in this batch — they ship next as batch 4b.)*
Author
Owner

Batch 4b — logical size and content mtime, v0.6.252 (#659 #664)

Needs at least v0.6.252. Important scoping: only files written AFTER this deploy carry the new record — pre-existing files keep showing backend (ciphertext) values until their next write. Test with fresh _qa uploads.

  1. #659 (plaintext sizes): upload a small file (e.g. 32 bytes) on files (enc). Expect: the UI size column shows ~32 B (not 3.2 KB), PROPFIND getcontentlength == GET/HEAD length, /api/v1/files size matches. Repeat one file on files-bao.
  2. #664 (Modified survives rename/move): upload, note Modified; rename it; move it into _qa/sub/. Expect Modified unchanged through both (UI + PROPFIND getlastmodified). A CONTENT edit (Collabora save) still updates Modified.
  3. Copy: duplicate a fresh file — the copy shows the same plaintext size, with its own new Modified.
  4. #670 side-finding: server log no longer mints versions for .cairn-meta.json (no .cairn-versions/.cairn-meta.json/... writes on tag/favourite/upload churn).
  5. Known limits (do NOT file as findings): old files keep ciphertext sizes until rewritten; a trash-restore returns to backend values until the file is next written.
## Batch 4b — logical size and content mtime, **v0.6.252** (#659 #664) Needs at least v0.6.252. **Important scoping: only files written AFTER this deploy carry the new record** — pre-existing files keep showing backend (ciphertext) values until their next write. Test with fresh `_qa` uploads. 1. **#659 (plaintext sizes):** upload a small file (e.g. 32 bytes) on files (enc). Expect: the UI size column shows ~32 B (not 3.2 KB), PROPFIND `getcontentlength` == GET/HEAD length, `/api/v1/files` size matches. Repeat one file on files-bao. 2. **#664 (Modified survives rename/move):** upload, note Modified; rename it; move it into `_qa/sub/`. Expect Modified unchanged through both (UI + PROPFIND `getlastmodified`). A CONTENT edit (Collabora save) still updates Modified. 3. **Copy:** duplicate a fresh file — the copy shows the same plaintext size, with its own new Modified. 4. **#670 side-finding:** server log no longer mints versions for `.cairn-meta.json` (no `.cairn-versions/.cairn-meta.json/...` writes on tag/favourite/upload churn). 5. Known limits (do NOT file as findings): old files keep ciphertext sizes until rewritten; a trash-restore returns to backend values until the file is next written.
Author
Owner

Batch 5a — navigation and views, v0.6.253 (#668 #667 #666 #669 #684)

Needs at least v0.6.253. Either host unless noted.

  1. #668 (view routes): click each of Favorites, Recents, Shares, Deleted files, Transfers, Federated — the URL becomes #view/<name>. Reload on each: you land back on that view. Paste #view/shares into a running tab (change the hash of an open tab): the view switches without reload. Personal and Spaces still use #/path.
  2. #667 (stale path): paste a nonsense route like #/favorites or #/home/gone-folder and reload. Expect: a toast "That location doesn't exist (anymore) — back to Personal.", landing in Personal, NO leftover rows from the previous folder and no raw "Load failed: not found…" line.
  3. #666 (storage meter): note the sidebar meter, upload a file (or delete one) — within a few seconds of the listing refresh the meter and its text update, no reload. The second line no longer starts with "Storage:" (the header already says it).
  4. #669 (Recents order): open a file, then Recents — it's first. The global Name A–Z sort preference must not reorder Recents (folder views still honor it).
  5. #684 (peering Global settings): Administration → Cairn Peering → Global settings — the tab OPENS, shows the master "Peering enabled" switch + group-suggest toggle + Save, and the hash reads #admin/peering/global (reload lands on the tab). Toggling the switch off/on should 503/un-503 the send APIs as before — verify state restores after your test.
  6. Regression: #loc: internal links and #admin/... routes still work (same router touched).
## Batch 5a — navigation and views, **v0.6.253** (#668 #667 #666 #669 #684) Needs at least v0.6.253. Either host unless noted. 1. **#668 (view routes):** click each of Favorites, Recents, Shares, Deleted files, Transfers, Federated — the URL becomes `#view/<name>`. Reload on each: you land back on that view. Paste `#view/shares` into a running tab (change the hash of an open tab): the view switches without reload. Personal and Spaces still use `#/path`. 2. **#667 (stale path):** paste a nonsense route like `#/favorites` or `#/home/gone-folder` and reload. Expect: a toast "That location doesn't exist (anymore) — back to Personal.", landing in Personal, NO leftover rows from the previous folder and no raw "Load failed: not found…" line. 3. **#666 (storage meter):** note the sidebar meter, upload a file (or delete one) — within a few seconds of the listing refresh the meter and its text update, no reload. The second line no longer starts with "Storage:" (the header already says it). 4. **#669 (Recents order):** open a file, then Recents — it's first. The global Name A–Z sort preference must not reorder Recents (folder views still honor it). 5. **#684 (peering Global settings):** Administration → Cairn Peering → Global settings — the tab OPENS, shows the master "Peering enabled" switch + group-suggest toggle + Save, and the hash reads `#admin/peering/global` (reload lands on the tab). Toggling the switch off/on should 503/un-503 the send APIs as before — verify state restores after your test. 6. Regression: `#loc:` internal links and `#admin/...` routes still work (same router touched).
Author
Owner

Batch 5b — activity feed, v0.6.254 (#673)

Needs at least v0.6.254. Only events from AFTER this deploy exist — the old feed was never written.

  1. Upload a fresh _qa file (any method: button, drop, or a large file via resumable upload) → details pane → Activity shows an "upload" event with your name.
  2. Edit it in Collabora, save, Back → an "update" event appears (editor saves now count).
  3. Rename it → rename/move event; duplicate it → the copy carries a "copy" event.
  4. A WebDAV PUT (rclone/Cyberduck) also lands in the feed — one spot check.
  5. Negative: .cairn-internal churn (tags/favourites writes) creates NO events for the manifest, and events never appear for another user's actions you can't see.
  6. Known limits (not findings): per-file queries only — a FOLDER's Activity stays empty; pre-deploy history doesn't exist.
## Batch 5b — activity feed, **v0.6.254** (#673) Needs at least v0.6.254. Only events from AFTER this deploy exist — the old feed was never written. 1. Upload a fresh `_qa` file (any method: button, drop, or a large file via resumable upload) → details pane → Activity shows an "upload" event with your name. 2. Edit it in Collabora, save, Back → an "update" event appears (editor saves now count). 3. Rename it → rename/move event; duplicate it → the copy carries a "copy" event. 4. A WebDAV PUT (rclone/Cyberduck) also lands in the feed — one spot check. 5. Negative: `.cairn`-internal churn (tags/favourites writes) creates NO events for the manifest, and events never appear for another user's actions you can't see. 6. Known limits (not findings): per-file queries only — a FOLDER's Activity stays empty; pre-deploy history doesn't exist.
Author
Owner

Batch 6 — picker polish and one empty state, v0.6.255 (#660 #661 #662 #663)

PR #704, merge e6b12ca3, live on both dogfoods.

#660 — duplicate empty state. The redundant say(t("emptyFolder")) status-bar line was removed from the listing renderer; an empty folder now shows only the single in-panel empty state. Verify: open an empty folder — no second "This folder is empty" in the status area.

#661 — picker button gives no disabled feedback. Added .pickfoot button:disabled { opacity: .5; cursor: not-allowed; }. Verify: open Move/Copy, select a destination where the action is not allowed (e.g. the source folder itself) — the primary visibly dims.

#662 — picker breadcrumb shows the storage segment. pickRootOf now returns /home for paths under it, and pickRootLabel maps both / and /home to the Personal label. Verify: in the picker, navigate into Personal — the breadcrumb root reads "Personal", never "home" or the storage mount name.

#663 — folder rows show no child counts. The picker read it.itemCount while /api/v1/files publishes the count as items (fillItemCounts, capped at 100). The renderer now reads items with an itemCount fallback:

const cnt = typeof it.itemCount === "number" ? it.itemCount
  : (typeof it.items === "number" ? it.items : null);

Verify: picker folder rows show "N items" (pickCount1/pickCountN, i18n ×4) for folders the API annotates.

Regression pins: web/test/qa6.test.js (static pins on index.html: single empty state, disabled CSS present, /home root mapping, it.items read with fallback, key4 i18n checks).

## Batch 6 — picker polish and one empty state, **v0.6.255** (#660 #661 #662 #663) PR #704, merge e6b12ca3, live on both dogfoods. **#660 — duplicate empty state.** The redundant `say(t("emptyFolder"))` status-bar line was removed from the listing renderer; an empty folder now shows only the single in-panel empty state. Verify: open an empty folder — no second "This folder is empty" in the status area. **#661 — picker button gives no disabled feedback.** Added `.pickfoot button:disabled { opacity: .5; cursor: not-allowed; }`. Verify: open Move/Copy, select a destination where the action is not allowed (e.g. the source folder itself) — the primary visibly dims. **#662 — picker breadcrumb shows the storage segment.** `pickRootOf` now returns `/home` for paths under it, and `pickRootLabel` maps both `/` and `/home` to the Personal label. Verify: in the picker, navigate into Personal — the breadcrumb root reads "Personal", never "home" or the storage mount name. **#663 — folder rows show no child counts.** The picker read `it.itemCount` while `/api/v1/files` publishes the count as `items` (fillItemCounts, capped at 100). The renderer now reads `items` with an `itemCount` fallback: ```js const cnt = typeof it.itemCount === "number" ? it.itemCount : (typeof it.items === "number" ? it.items : null); ``` Verify: picker folder rows show "N items" (pickCount1/pickCountN, i18n ×4) for folders the API annotates. **Regression pins:** `web/test/qa6.test.js` (static pins on index.html: single empty state, disabled CSS present, /home root mapping, `it.items` read with fallback, key4 i18n checks).
Author
Owner

Batch 7a — dark-theme contrast + dialog canon mechanics, v0.6.256 (#691, part of #693)

PR #705, merge d659e1c6, live on both dogfoods.

#691 — dark themes: white on lightened accent, 2.0–2.3:1 (CLOSED). The newer token system already carried a correct per-theme, per-accent-variant --accent-on dark ink; the old button rules just never read it. Eight sites switch from hardcoded #fff to var(--accent-on, #fff): button.primary/label.btn.primary, #avatarbtn (+ its dark override now var(--accent-on, var(--snow))), .pgn.on, .ksstep.done .kssdot, .ksbtn.primary, .uav.adm, .uabtn.primary. Off-token greys: .rochipvar(--lichen)/var(--slate-700), ul.plainlist .muted/.emptyvar(--slate-400), all with fallbacks. Deliberately unchanged: danger buttons keep #fff--danger (#a8543f) is not lightened in the dark themes, so white passes AA there; .kvbadge.good/.teal state tints left (deliberate tints per the issue). Verify: Night/Dusk/Ebony/Slate → "Upload file" primary shows dark ink on the light accent (≈8:1+), including with moss/ember/graphite accents.

#693 — dialog canon (PARTIAL, issue stays open). Mechanics done: askConfirm(message, verb) labels the primary with its verb — Delete (file delete ×2), Revoke (share ×2, app password, peer key), Remove (peer), Delete forever (purge) — instead of OK; askName gains ok and all four callers name Create/Rename; #namedlg-input gets an accent focus ring; licence dialog puts Close before "Refresh licence" and en spells Licence (key license, tabLicense, markup h2). New keys ×4: confirmVerb{Delete,Revoke,Purge,Remove,Create,Rename}. Left open on #693: fact-row rebuilds of #confirmdlg (design work, mockup-first), #pickdlg/#peeraddlg/#peerdetaildlg/#spcdetaildlg structure, and the legacy dialog trio — confirmed dead: openSettings() has zero callers, so #settingsdlg/#auditdlg/#holdsdlg are unreachable; not removed here because their wiring ($("set-close").onclick etc.) runs at script load and ripping them out is its own change.

Regression pins: web/test/qa7.test.js.

## Batch 7a — dark-theme contrast + dialog canon mechanics, **v0.6.256** (#691, part of #693) PR #705, merge d659e1c6, live on both dogfoods. **#691 — dark themes: white on lightened accent, 2.0–2.3:1 (CLOSED).** The newer token system already carried a correct per-theme, per-accent-variant `--accent-on` dark ink; the old button rules just never read it. Eight sites switch from hardcoded `#fff` to `var(--accent-on, #fff)`: `button.primary`/`label.btn.primary`, `#avatarbtn` (+ its dark override now `var(--accent-on, var(--snow))`), `.pgn.on`, `.ksstep.done .kssdot`, `.ksbtn.primary`, `.uav.adm`, `.uabtn.primary`. Off-token greys: `.rochip` → `var(--lichen)/var(--slate-700)`, `ul.plainlist .muted/.empty` → `var(--slate-400)`, all with fallbacks. **Deliberately unchanged:** danger buttons keep `#fff` — `--danger` (#a8543f) is not lightened in the dark themes, so white passes AA there; `.kvbadge.good/.teal` state tints left (deliberate tints per the issue). Verify: Night/Dusk/Ebony/Slate → "Upload file" primary shows dark ink on the light accent (≈8:1+), including with moss/ember/graphite accents. **#693 — dialog canon (PARTIAL, issue stays open).** Mechanics done: `askConfirm(message, verb)` labels the primary with its verb — Delete (file delete ×2), Revoke (share ×2, app password, peer key), Remove (peer), Delete forever (purge) — instead of OK; `askName` gains `ok` and all four callers name Create/Rename; `#namedlg-input` gets an accent focus ring; licence dialog puts Close before "Refresh licence" and en spells Licence (key `license`, `tabLicense`, markup h2). New keys ×4: confirmVerb{Delete,Revoke,Purge,Remove,Create,Rename}. **Left open on #693:** fact-row rebuilds of `#confirmdlg` (design work, mockup-first), `#pickdlg`/`#peeraddlg`/`#peerdetaildlg`/`#spcdetaildlg` structure, and the legacy dialog trio — **confirmed dead: `openSettings()` has zero callers**, so `#settingsdlg`/`#auditdlg`/`#holdsdlg` are unreachable; not removed here because their wiring (`$("set-close").onclick` etc.) runs at script load and ripping them out is its own change. **Regression pins:** `web/test/qa7.test.js`.
Author
Owner

Batch 7b — admin shell i18n + style-guide sweeps, v0.6.257 (part of #692, part of #694)

PR #706, merge 9ad07a4c, live on both dogfoods. This was the final batch — see the wrap-up below.

#692 — i18n gaps (PARTIAL, issue stays open). Root cause found: the admin sidebar buttons carried (overview) or now carry (admT_*, injected on the other 15) data-i18n, but 15 of 16 keys were absent from the dictionary, and applyI18n silently skips missing keys — hence the English sidebar. Fixed: all 16 nav keys ship ×4 (product names Cairn Peering / Open Cloud Mesh stay untranslated by design); ADM_TOPICS now stores key names and adm-title/adm-crumb/adm-desc go through t() with 16 new admD_* lede keys ×4; sidebar section headers Shared / This instance become navGrpShared/navGrpInstance. Transliterations accented: Übersicht, Aperçu, {n} élément(s), Vérifier les mises à jour, Update-Prüfung/für, configurée/è configurato, Géré par le fournisseur d'identité, Récupération, Détention, verschlüsselt (encOffB). Verify: switch to de/fr/it → admin sidebar, page titles and ledes translate; fr Overview reads "Aperçu".
Left open on #692: further ASCII-only strings in the dict (other "verschluesselt" carriers exist — the test pins encOffB's own line and notes the residue); the Sharing-policy hint strings (their sd* keys ARE translated ×4 and mapped at set-f-* — re-verify on v0.6.257 whether the admin pane renders them, the QA run was on .247); fr file-type subtitles are largely a false positive ("PDF document" is ext + fr "document" — byte-identical to en by coincidence; only the word order differs from "document PDF").

#694 — roll-up (PARTIAL, issue stays open). Done: .kvbadge follows the badge canon (pill 999px, 10.5px, 700, uppercase, letter-spacing); the sidebar storage meter re-renders on language switch (sideQuotaAt = 0; sideQuotaSoon() in the prefs handler). Left open: off-scale rem font sizes, lowercase inline actions, mono OCM buttons, UA-blue release-notes link, off-palette banners, red departed count, native selects, mono/sans swaps, raw gRPC error and bare 404 link pages, UTC labels, Collabora copy, 44px touch targets, singular/plural "legacy servers".

Regression pins: web/test/qa7b.test.js (includes a dynamic pin: every admnav data-i18n key must exist in the dictionary — a new topic without translations fails the suite).


Wrap-up — all seven batches shipped

Releases in this pass, all live on both dogfoods (files.c0rdyceps.ch and files-bao.c0rdyceps.ch): v0.6.248 → v0.6.257 (ten releases). Fully closed: #657 #658 #659 #660 #661 #662 #663 #664 #665 #666 #667 #668 #669 #670 #671 #672 #673 #676 #677 #678 #679 #680 #682 #683 #684 #685 #686 #687 #688 #689 #691. Partial with tracker open: #690 (structural question pending Nikola — comment on the issue offers the two options), #692, #693, #694. Deferred by prior agreement: #674/#675 (→ #389 phone pass), #681 (mockup-first), #695 (OpenCloud run with Nikola), #654.

@Opus: each batch section above carries per-issue "Verify:" steps. The regression pins live in web/test/qa1.test.jsqa7b.test.js and run in CI on every PR.

## Batch 7b — admin shell i18n + style-guide sweeps, **v0.6.257** (part of #692, part of #694) PR #706, merge 9ad07a4c, live on both dogfoods. This was the final batch — see the wrap-up below. **#692 — i18n gaps (PARTIAL, issue stays open).** Root cause found: the admin sidebar buttons carried (overview) or now carry (`admT_*`, injected on the other 15) `data-i18n`, but 15 of 16 keys were absent from the dictionary, and applyI18n silently skips missing keys — hence the English sidebar. Fixed: all 16 nav keys ship ×4 (product names Cairn Peering / Open Cloud Mesh stay untranslated by design); `ADM_TOPICS` now stores key names and `adm-title`/`adm-crumb`/`adm-desc` go through `t()` with 16 new `admD_*` lede keys ×4; sidebar section headers Shared / This instance become `navGrpShared`/`navGrpInstance`. Transliterations accented: Übersicht, Aperçu, {n} élément(s), Vérifier les mises à jour, Update-Prüfung/für, configurée/è configurato, Géré par le fournisseur d'identité, Récupération, Détention, verschlüsselt (encOffB). Verify: switch to de/fr/it → admin sidebar, page titles and ledes translate; fr Overview reads "Aperçu". **Left open on #692:** further ASCII-only strings in the dict (other "verschluesselt" carriers exist — the test pins encOffB's own line and notes the residue); the Sharing-policy hint strings (their `sd*` keys ARE translated ×4 and mapped at `set-f-*` — re-verify on v0.6.257 whether the admin pane renders them, the QA run was on .247); fr file-type subtitles are largely a false positive ("PDF document" is ext + fr "document" — byte-identical to en by coincidence; only the word order differs from "document PDF"). **#694 — roll-up (PARTIAL, issue stays open).** Done: `.kvbadge` follows the badge canon (pill 999px, 10.5px, 700, uppercase, letter-spacing); the sidebar storage meter re-renders on language switch (`sideQuotaAt = 0; sideQuotaSoon()` in the prefs handler). Left open: off-scale rem font sizes, lowercase inline actions, mono OCM buttons, UA-blue release-notes link, off-palette banners, red departed count, native selects, mono/sans swaps, raw gRPC error and bare 404 link pages, UTC labels, Collabora copy, 44px touch targets, singular/plural "legacy servers". **Regression pins:** `web/test/qa7b.test.js` (includes a dynamic pin: every admnav `data-i18n` key must exist in the dictionary — a new topic without translations fails the suite). --- ## Wrap-up — all seven batches shipped Releases in this pass, all live on both dogfoods (files.c0rdyceps.ch and files-bao.c0rdyceps.ch): v0.6.248 → v0.6.257 (ten releases). Fully closed: #657 #658 #659 #660 #661 #662 #663 #664 #665 #666 #667 #668 #669 #670 #671 #672 #673 #676 #677 #678 #679 #680 #682 #683 #684 #685 #686 #687 #688 #689 #691. Partial with tracker open: #690 (structural question pending Nikola — comment on the issue offers the two options), #692, #693, #694. Deferred by prior agreement: #674/#675 (→ #389 phone pass), #681 (mockup-first), #695 (OpenCloud run with Nikola), #654. @Opus: each batch section above carries per-issue "Verify:" steps. The regression pins live in `web/test/qa1.test.js` … `qa7b.test.js` and run in CI on every PR.
Author
Owner

#693 completion — dialog canon, v0.6.258 (CLOSED)

PR #707, merge 29ce3b3e, live on both dogfoods. This finishes what Batch 7a left open.

Fact-row destructive confirms. New shared #dangerdlg (h2 title, .conseq fact rows, Cancel before a named .danger verb), generalizing spacedeldlg's .crow/.cico/.ctext CSS via shared selectors, plus mono path styling. All eight destructive confirms migrated off the askConfirm prose blob: file delete and bulk delete (title/verb Delete; mono path or bold pluralised count; "Goes to Deleted files…" note), link revoke ×2 (Revoke link; mono path; "The link stops working immediately."), app-password revoke, delete-forever (warn row "This cannot be undone."), peer remove, peer-key revoke. askConfirm keeps exactly one caller: the trivial peer enable/disable toggle. New keys ×4: dgRevokeLink/dgRevokePw/dgRemovePeer/dgRevokeKey/dgNoteTrash/dgNoteLink/dgNotePurge. Verify: delete a file → titled dialog with trash icon, mono path, restore note, red Delete button; purge from Deleted files → amber warn row + "Delete forever".

Structure. peerdetaildlg gains a title (peerDetailTitle ×4); peeraddlg/peerdetaildlg/spcdetaildlg swap actions dlgfoot for canonical .dlgactions. #pickdlg is now a documented exception in docs/design/style-guide.md (composite footer carries the destination readout; New-folder is a tree action).

Dead trio removed. settingsdlg/auditdlg/holdsdlg are no longer dialogs — they are hidden parking-lot <div>s whose children (the set-* fields, audit filter/list, hold form/list) remain exactly where the admin shell adopts them from at open. Removed for good: openSettings (zero callers), the set-close/set-save/set-audit/set-holds/audit-close/holds-close buttons and their boot wiring, and saveSettings' dialog close. Untouched and pinned: ensureSettingsInShell, saveSettings, the adm-save-* buttons. Verify: admin → Basic settings/Audit/Retention→Holds all load and save as before; no console errors at boot; DOM contains no <dialog id="settingsdlg|auditdlg|holdsdlg">.

Regression pins: web/test/qa693.test.js (includes machinery-intact pins); qa7.test.js caller-count pins superseded there.

## #693 completion — dialog canon, **v0.6.258** (CLOSED) PR #707, merge 29ce3b3e, live on both dogfoods. This finishes what Batch 7a left open. **Fact-row destructive confirms.** New shared `#dangerdlg` (h2 title, `.conseq` fact rows, Cancel before a named `.danger` verb), generalizing spacedeldlg's `.crow`/`.cico`/`.ctext` CSS via shared selectors, plus mono path styling. All eight destructive confirms migrated off the askConfirm prose blob: file delete and bulk delete (title/verb Delete; mono path or bold pluralised count; "Goes to Deleted files…" note), link revoke ×2 (Revoke link; mono path; "The link stops working immediately."), app-password revoke, delete-forever (warn row "This cannot be undone."), peer remove, peer-key revoke. `askConfirm` keeps exactly one caller: the trivial peer enable/disable toggle. New keys ×4: dgRevokeLink/dgRevokePw/dgRemovePeer/dgRevokeKey/dgNoteTrash/dgNoteLink/dgNotePurge. Verify: delete a file → titled dialog with trash icon, mono path, restore note, red Delete button; purge from Deleted files → amber warn row + "Delete forever". **Structure.** peerdetaildlg gains a title (`peerDetailTitle` ×4); peeraddlg/peerdetaildlg/spcdetaildlg swap `actions dlgfoot` for canonical `.dlgactions`. `#pickdlg` is now a documented exception in `docs/design/style-guide.md` (composite footer carries the destination readout; New-folder is a tree action). **Dead trio removed.** `settingsdlg`/`auditdlg`/`holdsdlg` are no longer dialogs — they are hidden parking-lot `<div>`s whose children (the set-* fields, audit filter/list, hold form/list) remain exactly where the admin shell adopts them from at open. Removed for good: `openSettings` (zero callers), the set-close/set-save/set-audit/set-holds/audit-close/holds-close buttons and their boot wiring, and saveSettings' dialog close. Untouched and pinned: `ensureSettingsInShell`, `saveSettings`, the `adm-save-*` buttons. Verify: admin → Basic settings/Audit/Retention→Holds all load and save as before; no console errors at boot; DOM contains no `<dialog id="settingsdlg|auditdlg|holdsdlg">`. **Regression pins:** `web/test/qa693.test.js` (includes machinery-intact pins); qa7.test.js caller-count pins superseded there.
Author
Owner

#692 completion — i18n, v0.6.259 (CLOSED)

PR #708, merge 49bf6f85, live on both dogfoods. This finishes what Batch 7b left open.

121 transliterated de/fr/it strings repaired. A scanner over every dictionary line flagged ASCII-only values matching per-language transliteration patterns: Schluessel→Schlüssel, cle→clé, identita→identità, fuer→für, deja→déjà and kin, plus missing French (l'/n'/qu'/d') and Italian (un'/l'/dell') apostrophes — clustered in the encryption panel (enc*), licensing (lic*) and the peering lead/confirm strings. The splice was enforced both ways: every flagged string needed a correction, every correction had to be used exactly once. Four scanner rounds were needed — 50 strings had hidden behind em dashes (the original ASCII gate treated "—" as proof of localization; it now strips typographic punctuation first) or behind pattern gaps (identitaet, laeuft, instantane, missing-apostrophe "l " forms).

The scanner is now a permanent CI gate: web/test/qa692.test.js re-runs the same scan on every PR, so any future de/fr/it string written without its accents fails the suite with the offending key and text in the assertion message.

Settled from the original report:

  • Sharing-policy hints: already fixed since v0.6.247 — they render through t(SET_DESC[id]) with translated sd* keys (pinned in the test). Verify: admin → Sharing → Policy in de/fr — the explanations under the labels translate.
  • fr file-type subtitles: near-false-positive. "PDF document" is ext + fr "document" — byte-identical to English by coincidence; only the word order (document PDF) differs from ideal French, and a per-language composition rule was judged not worth it.

Verify: switch to de → Licensing shows "Schlüssel installieren", Encryption → Custody shows "Verschlüsselung im Ruhezustand"; fr → "Clé installée.", "N'expire pas"; it → "Home più grande", "Perché ibrido". Grep-level check: the served index.html contains no "Schluessel", no "Cle", no "Apercu".

## #692 completion — i18n, **v0.6.259** (CLOSED) PR #708, merge 49bf6f85, live on both dogfoods. This finishes what Batch 7b left open. **121 transliterated de/fr/it strings repaired.** A scanner over every dictionary line flagged ASCII-only values matching per-language transliteration patterns: Schluessel→Schlüssel, cle→clé, identita→identità, fuer→für, deja→déjà and kin, plus missing French (l'/n'/qu'/d') and Italian (un'/l'/dell') apostrophes — clustered in the encryption panel (enc*), licensing (lic*) and the peering lead/confirm strings. The splice was enforced both ways: every flagged string needed a correction, every correction had to be used exactly once. Four scanner rounds were needed — 50 strings had hidden behind **em dashes** (the original ASCII gate treated "—" as proof of localization; it now strips typographic punctuation first) or behind pattern gaps (identitaet, laeuft, instantane, missing-apostrophe "l " forms). **The scanner is now a permanent CI gate:** `web/test/qa692.test.js` re-runs the same scan on every PR, so any future de/fr/it string written without its accents fails the suite with the offending key and text in the assertion message. **Settled from the original report:** - Sharing-policy hints: already fixed since v0.6.247 — they render through `t(SET_DESC[id])` with translated `sd*` keys (pinned in the test). Verify: admin → Sharing → Policy in de/fr — the explanations under the labels translate. - fr file-type subtitles: near-false-positive. "PDF document" is ext + fr "document" — byte-identical to English by coincidence; only the word order (document PDF) differs from ideal French, and a per-language composition rule was judged not worth it. Verify: switch to de → Licensing shows "Schlüssel installieren", Encryption → Custody shows "Verschlüsselung im Ruhezustand"; fr → "Clé installée.", "N'expire pas"; it → "Home più grande", "Perché ibrido". Grep-level check: the served index.html contains no "Schluessel", no `"Cle"`, no `"Apercu"`.
Author
Owner

#694 completion — style-guide roll-up, v0.6.260 (CLOSED)

PR #709, merge 79569c21, live on both dogfoods. This finishes the roll-up.

Type scale. All ~100 body-copy rem font sizes snapped to the px scale (10.5/11/11.8/12/12.5/13/14/15) by a deterministic nearest-value sweep; sizes above 1rem (headings) untouched. Verify: computed font sizes on ledes, kv rows, .sharepath, "Exit to files" land on the scale.

Voice & copy. Inline actions are sentence case (Revoke, Remove, Restore, Delete forever ×4 languages). "Last seen" → "Last sign-in", which is what the field holds. The legacy-servers OCM status pluralises via tn (new ocmStatusTrustedV1 ×4), and every " - " inside the dictionary became " — " (sweep, ~40 strings). Both sweeps are now CI scanners in web/test/qa694.test.js.

Controls & colour. Selects get the canonical border/radius/accent focus ring globally; the release-notes link takes the accent colour instead of UA blue; OCM Check/Allow leave the mono font; the departed-owners tab badge goes muted 11px; @media (pointer: coarse) grows row actions to 42×44.

Times. fmtTimeRaw renders in the browser's timezone (was unlabeled UTC — a delete at 12:00 local showed 10:00). Verify: Deleted files timestamps match wall-clock (TZ Europe/Zurich).

Residue (consciously out, no tracker reopened): kvbadge state-tint drift and Overview-vs-Custody "On" colour (deliberate tints per the issue text); the Deleted-files retention banner (dynamic DOM — needs the v0.7 live pass); raw gRPC text in Transfers (only reproducible with real peer traffic — folds into the #106 A↔B dogfood); bare Go 404 for expired links (server-side, worth its own small issue if it grates); "Signed in now" count semantics (data question); Collabora welcome popup and READ-ONLY SPACE label (Collabora-side); public share page palette (own surface).

With this, all four QA trackers (#691 #692 #693 #694) are closed. Everything from QA run #656 is now either shipped (v0.6.248 → v0.6.260), deferred by explicit agreement (#674/#675 → #389, #681 mockup-first, #695 OpenCloud with Nikola, #654), or awaiting Nikola (#690 — mockup delivered separately).

## #694 completion — style-guide roll-up, **v0.6.260** (CLOSED) PR #709, merge 79569c21, live on both dogfoods. This finishes the roll-up. **Type scale.** All ~100 body-copy `rem` font sizes snapped to the px scale (10.5/11/11.8/12/12.5/13/14/15) by a deterministic nearest-value sweep; sizes above 1rem (headings) untouched. Verify: computed font sizes on ledes, kv rows, .sharepath, "Exit to files" land on the scale. **Voice & copy.** Inline actions are sentence case (Revoke, Remove, Restore, Delete forever ×4 languages). "Last seen" → "Last sign-in", which is what the field holds. The legacy-servers OCM status pluralises via `tn` (new `ocmStatusTrustedV1` ×4), and every " - " inside the dictionary became " — " (sweep, ~40 strings). Both sweeps are now CI scanners in `web/test/qa694.test.js`. **Controls & colour.** Selects get the canonical border/radius/accent focus ring globally; the release-notes link takes the accent colour instead of UA blue; OCM Check/Allow leave the mono font; the departed-owners tab badge goes muted 11px; `@media (pointer: coarse)` grows row actions to 42×44. **Times.** `fmtTimeRaw` renders in the browser's timezone (was unlabeled UTC — a delete at 12:00 local showed 10:00). Verify: Deleted files timestamps match wall-clock (TZ Europe/Zurich). **Residue (consciously out, no tracker reopened):** kvbadge state-tint drift and Overview-vs-Custody "On" colour (deliberate tints per the issue text); the Deleted-files retention banner (dynamic DOM — needs the v0.7 live pass); raw gRPC text in Transfers (only reproducible with real peer traffic — folds into the #106 A↔B dogfood); bare Go 404 for expired links (server-side, worth its own small issue if it grates); "Signed in now" count semantics (data question); Collabora welcome popup and READ-ONLY SPACE label (Collabora-side); public share page palette (own surface). With this, **all four QA trackers (#691 #692 #693 #694) are closed.** Everything from QA run #656 is now either shipped (v0.6.248 → v0.6.260), deferred by explicit agreement (#674/#675 → #389, #681 mockup-first, #695 OpenCloud with Nikola, #654), or awaiting Nikola (#690 — mockup delivered separately).
Author
Owner

#690 — opt-in group space (v0.6.261, PR #710)

What changed

  • App groups no longer synthesize /spaces/<group>: scope.Driver.WithGroupExclusion(GroupStore.Has) kills synthesis in both the /spaces listing and direct path resolution (IdP groups untouched; a store-backed space of the same name still grants).
  • New group dialog gained the opt-in: checkbox "Also create a shared space for this group" reveals a Space-name field prefilled "<group> space" (tracks the group name until hand-edited, resets on close). The interim guNewSpaceNote "for now" hint is gone (markup + key + old qa2 pin).
  • The provisioned space is a real app-owned space: owner = creating admin, group members materialized as write members, linked via spacestore.Space.Group. Group member add/remove mirrors into it (owner never evicted). Space-creation failure surfaces as a toast (guSpaceFailed), never fails the group create.
  • Deleting a linked group asks for the fate: radios "Delete the space with the group — its files go to Deleted files" (default; trash → deactivate → tombstone) vs "Keep the space — it stays with its owner" (unlink). API: DELETE /api/v1/admin/groups/{id}?space=delete|keep, 409 naming the space when the fate is missing.
  • Spaces inventory row now reads Owned by <owner> · group <name> · app-managed (spaceEntry.Group); Groups rows carry space (appGroupRow.Space).
  • 8 new i18n keys ×4 (guSpaceOpt/guSpaceName/guSpaceHintOn/guSpaceFailed/guDelHasSpace/guDelSpaceDrop/guDelSpaceKeep/spcGroupLbl).

Verify on either dogfood (files.c0rdyceps.ch / files-bao.c0rdyceps.ch, both on v0.6.261)

  1. Groups → New group qa690a, member sharer1, checkbox OFF → create. /spaces for sharer1 must NOT contain qa690a; Spaces admin must not list it. (This is the #690 root-cause fix — pre-existing synthesized spaces like test5 also disappear from members' /spaces now, since synthesis is gone. Their stray backend dirs remain until Nikola's manual cleanup.)
  2. New group qa690b, member sharer1, checkbox ON → Space-name prefills qa690b space; edit the group name and watch the prefill track until you type in the space field. Create → Spaces admin shows qa690b space · Owned by <you> · group qa690b · Members 1 · app-managed; sharer1 sees /spaces/qa690b space read-write.
  3. Add/remove a member on qa690b → space membership follows (owner never removed).
  4. Delete qa690b → dialog shows the fate radios naming "qa690b space"; pick Keep → space survives, group column empty. Recreate + delete with the default → space lands in Deleted files, name reserved.
  5. Create a group with checkbox ON whose space name collides with an existing space → group is created, toast reports the space error.

Tests: internal/storage/scope/scope_exclude_test.go, internal/spacestore/grouplink_test.go, internal/api/groups_space_test.go, web/test/qa690.test.js; stale interim pin in qa2.test.js retired. Full suite green on PR #710 CI (1m23s).

## #690 — opt-in group space (v0.6.261, PR #710) **What changed** - App groups no longer synthesize `/spaces/<group>`: `scope.Driver.WithGroupExclusion(GroupStore.Has)` kills synthesis in both the `/spaces` listing and direct path resolution (IdP groups untouched; a store-backed space of the same name still grants). - New group dialog gained the opt-in: checkbox "Also create a shared space for this group" reveals a Space-name field prefilled `"<group> space"` (tracks the group name until hand-edited, resets on close). The interim `guNewSpaceNote` "for now" hint is gone (markup + key + old qa2 pin). - The provisioned space is a real app-owned space: owner = creating admin, group members materialized as write members, linked via `spacestore.Space.Group`. Group member add/remove mirrors into it (owner never evicted). Space-creation failure surfaces as a toast (`guSpaceFailed`), never fails the group create. - Deleting a linked group asks for the fate: radios "Delete the space with the group — its files go to Deleted files" (default; trash → deactivate → tombstone) vs "Keep the space — it stays with its owner" (unlink). API: `DELETE /api/v1/admin/groups/{id}?space=delete|keep`, 409 naming the space when the fate is missing. - Spaces inventory row now reads `Owned by <owner> · group <name> · app-managed` (`spaceEntry.Group`); Groups rows carry `space` (`appGroupRow.Space`). - 8 new i18n keys ×4 (guSpaceOpt/guSpaceName/guSpaceHintOn/guSpaceFailed/guDelHasSpace/guDelSpaceDrop/guDelSpaceKeep/spcGroupLbl). **Verify on either dogfood (files.c0rdyceps.ch / files-bao.c0rdyceps.ch, both on v0.6.261)** 1. Groups → New group `qa690a`, member `sharer1`, checkbox OFF → create. `/spaces` for sharer1 must NOT contain `qa690a`; Spaces admin must not list it. (This is the #690 root-cause fix — pre-existing synthesized spaces like `test5` also disappear from members' `/spaces` now, since synthesis is gone. Their stray backend dirs remain until Nikola's manual cleanup.) 2. New group `qa690b`, member `sharer1`, checkbox ON → Space-name prefills `qa690b space`; edit the group name and watch the prefill track until you type in the space field. Create → Spaces admin shows `qa690b space · Owned by <you> · group qa690b · Members 1 · app-managed`; sharer1 sees `/spaces/qa690b space` read-write. 3. Add/remove a member on `qa690b` → space membership follows (owner never removed). 4. Delete `qa690b` → dialog shows the fate radios naming "qa690b space"; pick Keep → space survives, group column empty. Recreate + delete with the default → space lands in Deleted files, name reserved. 5. Create a group with checkbox ON whose space name collides with an existing space → group is created, toast reports the space error. **Tests:** `internal/storage/scope/scope_exclude_test.go`, `internal/spacestore/grouplink_test.go`, `internal/api/groups_space_test.go`, `web/test/qa690.test.js`; stale interim pin in `qa2.test.js` retired. Full suite green on PR #710 CI (1m23s).
Author
Owner

#681 — federated share from the panel (v0.6.262, PR #712)

Root cause (deep-dive result): the panel's federated Add path from #603 was complete (pick → POST /api/v1/ocm/shares, allow-list-checked, interop-green), but its ONLY candidate source was the invited-contacts list — a policy-"any" server with zero contacts could never surface a candidate, and typing deliberately resets the pick (the #571 guard). /api/v1/recipients staying local-only is by design (user-enumeration guard) and unchanged.

What changed (UI only): typing a full user@server address now offers a direct Federated candidate in the picklist when server is on the allow-list with policy "any" (canonical fqdn, case-insensitive host match, deduped against contacts). Picking it enables Add and sends over OCM exactly as before, boundary note included. New pure lift-block helper shareFedDirect + shareFedServers loaded on dialog open. "Invited contacts only" servers keep the contacts-only flow; a non-allow-listed host still offers nothing.

Verify on the dogfoods (both on v0.6.262; files → files-bao has policy "any"):

  1. files, as nikola-test: _qa/qa-note.md → Shares → type sharer1@files-bao.c0rdyceps.ch with real keystrokes → a picklist row sharer1 files-bao.c0rdyceps.ch · Federated appears; click it → Add enables and the boundary note shows; Add → "Federated share created."
  2. files-bao, as sharer1: Federated view shows the pending share → accept → browse/fetch works; decline path too.
  3. files: Shares view "Mine" carries the federated row with revoke; revoke ends it on files-bao.
  4. Type sharer1@not-allowed.example → no candidate, Add stays disabled (unchanged, intended).
  5. Case check: sharer1@FILES-BAO.c0rdyceps.CH still offers the candidate with the canonical fqdn.

This unblocks the rest of QA §5 (accept/decline inbound, federated revoke, Shares-view federated rows — untested in the #656 pass).

Tests: web/test/qa681.test.js (lifted-helper unit pins + wiring pins), full suite green on PR #712 CI (1m21s).

## #681 — federated share from the panel (v0.6.262, PR #712) **Root cause** (deep-dive result): the panel's federated Add path from #603 was complete (pick → `POST /api/v1/ocm/shares`, allow-list-checked, interop-green), but its ONLY candidate source was the invited-contacts list — a policy-"any" server with zero contacts could never surface a candidate, and typing deliberately resets the pick (the #571 guard). `/api/v1/recipients` staying local-only is by design (user-enumeration guard) and unchanged. **What changed** (UI only): typing a full `user@server` address now offers a direct **Federated** candidate in the picklist when `server` is on the allow-list with policy **"any"** (canonical fqdn, case-insensitive host match, deduped against contacts). Picking it enables Add and sends over OCM exactly as before, boundary note included. New pure lift-block helper `shareFedDirect` + `shareFedServers` loaded on dialog open. "Invited contacts only" servers keep the contacts-only flow; a non-allow-listed host still offers nothing. **Verify on the dogfoods (both on v0.6.262; files → files-bao has policy "any"):** 1. files, as nikola-test: `_qa/qa-note.md` → Shares → type `sharer1@files-bao.c0rdyceps.ch` with real keystrokes → a picklist row `sharer1 files-bao.c0rdyceps.ch · Federated` appears; click it → Add enables and the boundary note shows; Add → "Federated share created." 2. files-bao, as sharer1: Federated view shows the pending share → accept → browse/fetch works; decline path too. 3. files: Shares view "Mine" carries the federated row with revoke; revoke ends it on files-bao. 4. Type `sharer1@not-allowed.example` → no candidate, Add stays disabled (unchanged, intended). 5. Case check: `sharer1@FILES-BAO.c0rdyceps.CH` still offers the candidate with the canonical fqdn. This unblocks the rest of QA §5 (accept/decline inbound, federated revoke, Shares-view federated rows — untested in the #656 pass). **Tests:** `web/test/qa681.test.js` (lifted-helper unit pins + wiring pins), full suite green on PR #712 CI (1m21s).
Author
Owner

#713 — spaces row menu clip (v0.6.263, PR #714)

Found by Nikola during the #690 dogfood-space cleanup. Root cause: the spaces list .kvbox inherits overflow: hidden (rounded-corner clipping), and the row three-dot menu is an absolutely-positioned .pmenu inside .pkebab — so on rows near the table edge the popup clipped instead of overflowing. The Seats tab had the identical bug and already carried the fix; the spaces list now has the same rule (#adm-spaces .kvbox { overflow: visible; }).

Verify: Administration → Spaces on either dogfood (v0.6.263) → open the three-dot menu on the LAST row → the menu renders fully past the table's bottom edge. Spot-check Seats row menus still overflow too (regression guard is pinned in qa713.test.js).

## #713 — spaces row menu clip (v0.6.263, PR #714) Found by Nikola during the #690 dogfood-space cleanup. Root cause: the spaces list `.kvbox` inherits `overflow: hidden` (rounded-corner clipping), and the row three-dot menu is an absolutely-positioned `.pmenu` inside `.pkebab` — so on rows near the table edge the popup clipped instead of overflowing. The Seats tab had the identical bug and already carried the fix; the spaces list now has the same rule (`#adm-spaces .kvbox { overflow: visible; }`). **Verify:** Administration → Spaces on either dogfood (v0.6.263) → open the three-dot menu on the LAST row → the menu renders fully past the table's bottom edge. Spot-check Seats row menus still overflow too (regression guard is pinned in `qa713.test.js`).
Author
Owner

#715 — verified IdP attribution, orphan folders surface honestly (v0.6.264, PR #716)

Found by Nikola right after #713: test5/qa656/Fall * still read "Managed by the identity provider" although Keycloak has no such groups. Root cause: mergeSpaceDirs assumed IdP management for every unclaimed /spaces directory — the directory was never consulted.

What changed: the inventory now verifies the claim. GroupsAPI.DirNamesFor collects known directory names plus the live IdP listing; only a successful live listing is authoritative. A dir the directory knows → "Managed by the identity provider". A dir matching nothing → orphan: true, shown as "Folder in storage — no managing group found" (row + details dialog, i18n ×4). No client configured, or listing error → old attribution stands (an IdP outage never mass-flags orphans at a customer).

Verify (either dogfood, v0.6.264):

  1. Administration → Spaces: the leftover synthesized dirs (test5, qa656, Fall 1-3, case-folder-1, Test Ümlauf folderinö, cairn-admins*) now read "Folder in storage — no managing group found"; app-owned rows unchanged.
    *cairn-admins only flips if it isn't a real Keycloak group — if it IS one, it correctly keeps the IdP label.
  2. Details dialog on an orphan row shows the same membership line.
  3. Create a real group in Keycloak matching one dir name, reload → that row flips back to "Managed by the identity provider".
  4. Sanity: rows backed by real IdP groups (if any) keep their label; nothing changed for app-owned rows.

Tests: internal/api/spaces_orphan_test.go (orphan split, unverified fallback, DirNamesFor authority incl. listing failure), union tests updated, web/test/qa715.test.js UI pins. CI green on PR #716 (1m16s).

## #715 — verified IdP attribution, orphan folders surface honestly (v0.6.264, PR #716) Found by Nikola right after #713: `test5`/`qa656`/`Fall *` still read "Managed by the identity provider" although Keycloak has no such groups. Root cause: `mergeSpaceDirs` assumed IdP management for every unclaimed `/spaces` directory — the directory was never consulted. **What changed:** the inventory now verifies the claim. `GroupsAPI.DirNamesFor` collects known directory names plus the live IdP listing; only a *successful* live listing is authoritative. A dir the directory knows → "Managed by the identity provider". A dir matching nothing → `orphan: true`, shown as "Folder in storage — no managing group found" (row + details dialog, i18n ×4). No client configured, or listing error → old attribution stands (an IdP outage never mass-flags orphans at a customer). **Verify (either dogfood, v0.6.264):** 1. Administration → Spaces: the leftover synthesized dirs (`test5`, `qa656`, `Fall 1-3`, `case-folder-1`, `Test Ümlauf folderinö`, `cairn-admins`*) now read "Folder in storage — no managing group found"; app-owned rows unchanged. *`cairn-admins` only flips if it isn't a real Keycloak group — if it IS one, it correctly keeps the IdP label. 2. Details dialog on an orphan row shows the same membership line. 3. Create a real group in Keycloak matching one dir name, reload → that row flips back to "Managed by the identity provider". 4. Sanity: rows backed by real IdP groups (if any) keep their label; nothing changed for app-owned rows. **Tests:** `internal/api/spaces_orphan_test.go` (orphan split, unverified fallback, DirNamesFor authority incl. listing failure), union tests updated, `web/test/qa715.test.js` UI pins. CI green on PR #716 (1m16s).
Author
Owner

#717 — orphan folders: Take over and deactivate (v0.6.265, PR #718)

Follow-up to #715 from the same cleanup session: orphan rows offered only Details, because an orphan has no spacestore record and the lifecycle endpoints would 404.

What changed (UI only): the orphan row menu gains "Take over and deactivate" (danger item). It claims the folder through the normal POST /api/v1/spaces (record owned by the acting admin; the dir already exists) and immediately runs the standard deactivate. From there it is a regular app-owned space: Reactivate, or the type-the-name Delete → contents to Deleted files, name tombstoned. IdP-verified managed rows stay Details-only.

Verify (either dogfood, v0.6.265):

  1. Spaces admin → three-dot menu on an orphan row (test5 etc.) → "Take over and deactivate" → row becomes Owned by <you> · app-managed, Deactivated.
  2. Menu now offers Reactivate + Delete space; type-the-name Delete moves contents to Deleted files and reserves the name.
  3. A row that is IdP-verified managed still offers Details only.
  4. Take over a name that was recently deleted (tombstoned) → the "reserved by a recently deleted space" message surfaces.

Related: #719 filed for the keep-path ("Assign a new owner" dialog on orphan rows) — deliberately separate, low priority by construction.

Tests: web/test/qa717.test.js; CI green on PR #718 (1m16s).

## #717 — orphan folders: Take over and deactivate (v0.6.265, PR #718) Follow-up to #715 from the same cleanup session: orphan rows offered only Details, because an orphan has no spacestore record and the lifecycle endpoints would 404. **What changed (UI only):** the orphan row menu gains **"Take over and deactivate"** (danger item). It claims the folder through the normal `POST /api/v1/spaces` (record owned by the acting admin; the dir already exists) and immediately runs the standard deactivate. From there it is a regular app-owned space: Reactivate, or the type-the-name Delete → contents to Deleted files, name tombstoned. IdP-verified managed rows stay Details-only. **Verify (either dogfood, v0.6.265):** 1. Spaces admin → three-dot menu on an orphan row (`test5` etc.) → "Take over and deactivate" → row becomes `Owned by <you> · app-managed`, Deactivated. 2. Menu now offers Reactivate + Delete space; type-the-name Delete moves contents to Deleted files and reserves the name. 3. A row that is IdP-verified managed still offers Details only. 4. Take over a name that was recently deleted (tombstoned) → the "reserved by a recently deleted space" message surfaces. **Related:** #719 filed for the keep-path ("Assign a new owner" dialog on orphan rows) — deliberately separate, low priority by construction. **Tests:** `web/test/qa717.test.js`; CI green on PR #718 (1m16s).
Sign in to join this conversation.
No labels
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Cordy/Cairn#697
No description provided.