File browser redesign (Claude Design): visual + multi-select + grid + owner/chips #311
Labels
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: Cordy/Cairn#311
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Port the Claude Design file-browser redesign (
design_handoff_file_browser/: README +files-mockup.html) intoweb/static/index.html, functionally and visually. High-fidelity handoff; exact tokens/spacing/states specified.Status
192.168.10.245/cordy/cairn:v0.6.64@sha256:932aa2af…, live on the dogfood (cairn-enc, 192.168.10.249, pod healthy 1/1, no restarts). Visual confirmation still pending an operator login (password entry is out of scope for the agent).GET /api/v1/files), which can't be integration-tested from CI alone; recommended to do with the operator available. Not started autonomously.Key finding (shapes the sequencing)
Against the current code this is more than a restyle. The file-listing API (
GET /api/v1/files) returns onlyname / size / modTime / path / isDirper item. The mockup depicts features/data we don't have yet:Also: current token system is
--snow/--surface/--slate-*/--lichen/--glacier; the mockup introduces--paper/--ink/--accent/--rule/--sunk. The redesign is scoped to the file browser, so the new tokens must coexist with the old ones (still used by preview/admin/dialog panels). Accents already exist as a pref (glacier default + moss/ember/graphite); the mockup's default is moss and adds a 4th (glacier as a named accent) — reconcile at port time.Plan (incremental PRs)
.top/ sidebar→.side+navscroll+quotastorage meter / toolbar / breadcrumb / empty state; rewriterenderRows(~1774) from<table>to CSS-grid.lrrows; add multi-select (aSet+ singleapplySelwriter, survives sort & list↔grid) with the selection bar wired to existing single-item ops; add grid view toggle. Columns for now: name / size / modified / actions (owner + chips hidden until PR2/3). No backend change. (shipped: #312, v0.6.64)GET /api/v1/files. Add per-itemowner,itemCount(dirs), and status flagsshared/link/hold/lock, sourced from the existing shares/holds/locks subsystems. Keep it cheap (batch lookups, not N+1). (deferred — do with operator available)applyPrefs3-value theme + Appearance-dialog third option; CSS block already in the port.Constraints (from the handoff)
data-i18nkey, event binding, andlocalStorage["cairn-prefs"]value.svgIcon/ICON_FOLDER/FILE_BASE/ICONS/EXT_GROUPS/iconFor) reused verbatim.--check+ Go build/test as the CI gate; PRs for review (no auto-ship to the dogfood).Design handoff files live in the operator's
design_handoff_file_browser/folder (not committed). Working branch:feat/files-redesign.Phase 2 shipped — v0.6.65 (PR #313, squash-merged)
Live on the dogfood:
cairn-encon192.168.10.245/cordy/cairn:v0.6.65@sha256:db68c142…, pod Ready 1/1, 0 restarts,/healthzgreen.PR1 gaps the operator flagged
SharedandThis instancesub-headers (.navgrp), each auto-hiding when all its items are hidden.blackvalue end-to-end: Appearance-dialog option,applyPrefs()promoted to tri-state, new-token palette + per-accent overrides (handoff values), plus an old-token fallback block so admin/preview/dialog surfaces stay dark. (The accent picker + moss/ember/graphite already existed and worked — Ebony was the one genuinely-missing option.)PR2 —
GET /api/v1/filesenrichment (internal/api/listmeta.go)shared— caller has an active public link on the path.hold— a legal hold covers the path/ancestor.Resolvebefore matching (no I/O). Best-effort; a disabled subsystem or unresolvable path just yields no flag.owner(storage.FileInfohas no uploader — files carry no creator metadata),lock(the only lock is the instance boot-guard instatestore, not per-file),itemCount(aListper subdir = N+1 on the main-screen hot path). Fabricating owner/lock in a product with real legal-hold semantics would be worse than omitting them. The owner column therefore stays hidden; making it real needs a storage-layer schema change, not a UI change.PR3 — status chips
Shared+On holdchips from the PR2 fields. Owner column remains hidden (no data).Gate: anchored splices (assert count==1),
go vet ./...clean,go test ./internal/api/...green (newapplyFlagstests),go build ./cmd/cairndOK,node --checkOK, and CI (ci.yml) green on the PR head.Left for the operator (bulk visual check, needs a fresh login — in-memory sessions): nav group labels, the Ebony theme option, and the chips on shared/held files. Also worth an eyeball: whether the file-nav buttons correctly hide in admin mode (a pre-existing PR1 structural question, not touched here).
Follow-up fix shipped — v0.6.66 (PR #314)
Members button missing in spaces. Root cause was a PR1 stopgap, not the redesign logic: PR1 added
document.body.setAttribute("data-space", "personal")at init (to keep the empty Owner column hidden "until PR2/PR3") and never updates it per-view. That made the CSS rulebody[data-space="personal"] #spacemembers{display:none}fire in every view, hiding the button even inside a space. The button's JS gate (feat.spaceMembers && at a /spaces/<name> root) was byte-identical before PR1, in PR1, and now — so before PR1 (nodata-spaceattribute) the rule never matched and the button showed.Fix: dropped the redundant
#spacemembersCSS rule; the JS gate alone hides it correctly in personal space and shows it in a space. Owner column stays hidden (thedata-space="personal"default is left in place intentionally, since there's still no per-item owner data). CI green on the head commit; live on the dogfood as v0.6.66.Redesign phase 3 is shipped and live on both dogfoods (v0.6.75), ported from the new
files-mockup.htmlhandoff in five slices:Themes are now five, per the operator's call: Light, Night (the existing blue dark — stored value stays
darkso no saved preference orphans), Dusk (new, the mockup's green-cast dark), Ebony, Slate (new). Both token systems (--paper/--ink/--accentand the legacy--snow/--glacier) carry all of them plus every accent variant, so legacy panels theme along.Two real bugs surfaced while porting, both fixed:
pvsel) was still styled astr.pvsel > td— dead since the table became.lrgrid rows, so "you are previewing this" has rendered nothing in the shipped app.@media-keyed, but the preview panel takes width away without the viewport changing: the breakpoints never fired and the shortfall landed entirely on the filename track. Now@container list.The pointer-capture divider supersedes the v0.6.67 iframe workaround (agreed with the operator); that patch is removed.
Not ported / deliberately open:
#membersdlg-idp(IdP-managed membership notice) ships hidden with i18n, waiting on a backend flag — that's #129.Fidelity spot-check across the five themes and the four accents is the operator's visual pass on the dogfood.
Admin redesign slice A shipped — v0.6.77, live on both dogfoods. PR #326.
Rail regroups to three (Instance / Access, sharing & federation / Billing — operator's call: Federation merges into access and sharing rather than standing alone, Billing keeps its own group). "← Files" leaves the rail and becomes "Exit to files" in a labelled header band with a crumb and a height-reserved description. Overview drops three equal stat cards for two meters (the only quantities with denominators — storage, seats) plus grouped status rows on the shared
chipcomponent.Remaining admin slices: B settings-row pattern + switches + save bar + segmented tabs (Basic/Storage/Sharing/Users/Encryption) · C data-heavy topics (Deleted files, Audit log, Spaces inventory) · D Retention & holds, Updates, Licensing, Peering.
Deferred items — running list
To be swept in one pass once the UI port is complete. Numbering continues across the whole redesign.
#membersdlg-idpships hidden, i18n done)tr.pvselandaside > buttonwere both silently orphaned by the redesign; likely more.notice, "an update is available") — needs the Updates check to run on Overview.ovcard/.ovcards/.ovlabel/.ovnum, possibly.kvbadgeItems 6 and 7 are the ones that need a product decision rather than just cleanup: either the endpoints grow to serve them, or those Overview rows stay out. I've omitted rather than faked them.
Admin slice B shipped — v0.6.78, live on both dogfoods. PR #327.
Settings rows (name + explanation left, control right), checkboxes render as switches, underline tabs become a segmented control, and Save becomes a sticky bar that appears only when something changed, counts the pending changes and offers Discard.
Implementation note worth keeping: this is presentation only.
SET_SLOTSstill relocates the same field elements, every field id / value binding / save handler is untouched, and all the CSS is scoped to#adminviewso the settings dialog and other.field/.dlgactionsusers are unaffected. The save bar is selected with:has(button[id^="adm-save-"])so the Updates topic's "Check for updates" bar is not swallowed. Baseline for the dirty state is captured right afterloadSettingsValues()and re-captured after a successful save by wrapping the existingonclick.Remaining: C data-heavy topics (Deleted files, Audit log, Spaces inventory) · D Retention & holds, Updates, Licensing, Peering.
Deferred items — running list (updated)
#membersdlg-idpships hidden, i18n done)tr.pvsel,aside > buttonclass of bug).notice) — needs the Updates check on Overview.ovcard/.ovcards/.ovlabel/.ovnum, possibly.kvbadgeSET_DESCrender without one rather than getting invented copy — the map covers what I could name with confidenceItems 6, 7 and 10 need a product decision; the rest are cleanup or content.
Admin slice C shipped — v0.6.79, live on both dogfoods. PR #328.
Deleted files split into Eligible now / Protected with each protected row carrying its reason; audit log becomes an aligned time/actor/action/path grid with an action filter and a stated 200-event cap; the spaces hint paragraph becomes an inventory that surfaces deactivated spaces still holding storage.
Two findings worth recording:
eligible,holdandkeepUntilper item. Eligibility was never missing data — it was only ever rendered as a disabled button. No backend change was needed for the split.ownerLblundefined before it shipped;t()returns the key verbatim when missing, so the spaces list would have read "ownerLbl nikola". Now defined in all four languages. This check is worth keeping as a CI step — added as item 12 below.Remaining: D — Retention & holds (the resolver), Updates (version comparison), Licensing (seat meter with the free-tier line), Peering (two-halves cards). That closes the admin port.
Deferred items — running list (updated)
#membersdlg-idpships hidden, i18n done)tr.pvsel,aside > buttonclass of bug).notice) — needs the Updates check on Overview.ovcard/.ovcards/.ovlabel/.ovnum, possibly.kvbadge; now alsofmtEvtis near-unusedSET_DESCentry render without a descriptionItems 6, 7, 10, 13 and 14 need a product or API decision; the rest are cleanup, content or CI.
Slice D shipped (v0.6.80, #329) — the admin port is complete. Updates, legal holds and licensing done; peering deliberately untouched.
Three new deferred items, plus one correction to the plan:
GET /api/v1/admin/retention/resolve?path=that returns the effective rule and its source; then the UI just displays the server's answer instead of computing a rival one.peerCardElalready joins the inbound key we issued with the outbound peer entry and handles either half being absent (v0.6.32–41). The mockup's "two halves" card is what is already shipping. Restyling would have re-litigated a design that already landed.Process note for the one-shot house style (not a product item, but it cost four failed runs across slices C and D): the hand-written
grep -c … ; test "$X" = "N"verification block adds nothing over the splices' ownassert count == 1plusnode --check, and every one of its four failures was my count being wrong while the edit itself was correct — the file was written, then the job aborted before pushing. Causes seen: element created viaclassNamein JS so the class string appears once not twice; a symbol legitimately appearing 2× not 3×. The last workflow drops the greps entirely and keepsnode --check+ the i18n key audit, which are the two checks that can actually fail for a real reason. Recommend that as the standing pattern.Item 12 (add the i18n key audit to
ci.yml) is now more clearly worth doing: it has caught one real bug (ownerLbl, slice C) and passed cleanly on every slice since, so it is cheap and it works. It only runs because I paste it into each one-shot; in CI it would guard every contributor's change.Deferred-items sweep complete — v0.6.81, live on both dogfoods. PRs #330, #331, #332, #333.
16 of 17 items closed. One deferred by decision, and it is the only one that needed deferring.
The finding that matters most
Four items recorded as "blocked on a missing endpoint" were already served. I had written them up from intent rather than from the code:
auditQueryhas parsedfrom/toas RFC3339 since it was written. Only the inputs were missing./admin/infoalready returnedstorageDriverandshareCount.RetentionPeriodsis amap[string]string. My stated blocker ("rules are free text, the grammar is unverified") was simply wrong; the textarea is a serialisation of structured data.Net: cluster C needed one endpoint and one boolean, not seven endpoints. Worth remembering next time I record something as blocked — "I have not read this" and "this does not exist" are different claims, and I filed the first as the second.
Two live bugs found by the new guard
internal/webcheckwas written first, and everything else in #330 is something it found rather than something I went looking for:classList.add("dragrow"); the CSS still saidtr.dragrow. No grab cursor, no drag opacity, no drop highlight, for weeks./api/v1/filesevery 60 seconds to decide the visibility of an elementdisplay:none !importanthad made unreachable. Found because the removal script refused to proceed on an unrecognised reference — the abort was the discovery.Deferred, deliberately
Item 2 (per-item notification read state) → #138. It needs per-user persistence, and every runtime state file is exactly what #138 moves into the storage backend. Building it now hands that milestone one more thing to migrate, and picks the schema before the new layout exists.
Audit event count on Overview stays out: no count endpoint exists, and a number inferred from a capped query would look authoritative without being so.
Standing notes
aside > button) — that needs a real DOM and therefore a login. Written into the package doc so nobody assumes more coverage than exists.TestUnusedClassesReportlogs and never fails, and this run proves why: it flagstealandgray, which are real and applied via"kvbadge " + badgeCls.t(SET_DESC[id])is a dynamic lookup the i18n guard cannot check; the splice validates those keys separately.set-quotafieldstill has no description — I cannot confirm from the frontend whether it caps the instance or sets a per-space default, and a confident wrong description on a quota control is worse than a blank.For your visual pass
Everything below needs a login, which I cannot do:
IdP-managed spaces enabled (power mode), soidpManagedis true and Add/Remove/role should be disabled with the notice showing.Closing as complete/superseded (operator decision, 2026-09-03).
The two unchecked plan boxes (PR2 listing enrichment, PR3 owner/chips) were delivered by v0.6.65 (
shared/holdflags + chips), and every field deliberately dropped there for lack of data has since become real and shipped through later work: the owner column (ownermeta sidecars), folder item counts, and lock chips (#315). The redesign itself completed through the admin slices A–D (v0.6.77–v0.6.80) and the 17-item deferred sweep (v0.6.81); the one item deferred by decision — per-item notification read state — lives on under #138/#402, where the watermark model now covers it.The UI has been iterated on repeatedly since (five-theme system, bell rework, Locks admin page, General settings card), all in daily dogfood use — nothing actionable remains on this issue.