Four entangled change-sets from one session, committed together because
their file-level overlap (build.sh, docs, embedded/, watcher.go, …) makes
post-hoc separation noisy:
* fix(archive): nested-party + folder-type cascade
transmittalIsUnderVisibleParty short-circuited on the first matched
party segment, only checking the immediately-next segment for a
folder-type marker. Paths like BM/sub/Issued/<txn> bypassed the Issued
toggle entirely. Replaced with isUnderHiddenFolderType (full-path) +
any-segment party match. Eight new Playwright cases pin the contract
in tests/archive-cascade.spec.js.
* refactor(zddc-server): scope .archive index by project
archive.Index now buckets by top-level segment
(.ByProject[<project>].ByTracking[<tracking>]). Resolve and AllEntries
take a project parameter; handler extracts it from contextPath's first
segment. /.archive/ at root returns 404 — stable refs must be
project-rooted. Within-project (tracking, rev) collisions emit a WARN
with both paths. Cross-project tracking-number duplicates no longer
collide.
* perf(zddc-server): lazy-load expensive bits of the profile page
serveProfilePage now ships a minimal shell: Email, EmailHeader,
IsSuperAdmin (root .zddc only). Visible projects + admin subtrees +
editable scaffolds populate client-side via /.profile/access. Subtree-
admin scaffolds live in <template id="tmpl-subtree-admin">; pure
non-admins receive no live admin form. ScanZddcFiles now memoized,
invalidated on .zddc events by the watcher and writer helpers.
* feat: lockstep release + redesigned releases page
sh build.sh --release [version|alpha|beta] is the canonical lockstep
cut: every tool (5 HTML + zddc-server) bumps to the same coordinated
version. zddc-server binaries now committed under website/releases/
with the same cascade chain as HTML tools (no more Codeberg release-
asset publication). zddc/release.sh deprecated (kept as a guard);
shared/publish-codeberg-release.sh removed.
Releases page redesigned as an action-first install guide: hero +
version dropdown that rewires every download link, channel chips for
always-visible alpha/beta access (state-aware labels: "tracks stable"
vs "active dev"), Path A (zddc-server with platform auto-detect from
UA), Path B (5 standalone tool HTMLs), version-pinning empowerment
narrative (drop-a-copy vs .zddc apps: cascade), channels explainer.
Channel-link verifier asserts every <tool>_{stable,beta,alpha}.html
resolves at the end of every build. Bootstrap-friendly: zddc-server
artifact checks skip until the first lockstep cut anchors the chain.
Tests: 167 Playwright + all Go packages green.
Docs: CLAUDE.md, AGENTS.md, ARCHITECTURE.md, zddc/README.md updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
89 lines
3 KiB
Go
89 lines
3 KiB
Go
package zddc
|
|
|
|
import (
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// scanCache memoizes ScanZddcFiles results keyed by fsRoot.
|
|
//
|
|
// The walk is the dominant cost on every profile-page render in a real
|
|
// archive (thousands of directories). Caching is safe because:
|
|
// - The set of directories with .zddc files only changes via the writer
|
|
// helpers (CreateOrUpdateFile / DeleteFile) or external file events
|
|
// observed by the archive watcher; both call InvalidateScanCache.
|
|
// - Cache values are immutable []string slices; readers reuse them.
|
|
//
|
|
// Errors are NOT cached — callers always get a fresh attempt after a walk
|
|
// failure so a transient permissions blip doesn't poison the cache.
|
|
var scanCache sync.Map // map[string][]string
|
|
|
|
// ScanZddcFiles walks fsRoot and returns every directory that contains a
|
|
// .zddc file, sorted by path. Reserved-prefix directories ('.', '_') are
|
|
// pruned from the walk — they hide internal/scaffolding state from the
|
|
// project picker and should likewise not surface in the editor's tree
|
|
// view. fsRoot itself is not subject to the prefix rule (it is the root).
|
|
//
|
|
// The returned paths are absolute (rooted under fsRoot). On any walk
|
|
// error the partial result accumulated so far is returned alongside the
|
|
// error so callers can degrade gracefully. Successful results are cached
|
|
// in memory; subsequent calls return the cached slice until
|
|
// InvalidateScanCache is called (the .zddc writer helpers and the archive
|
|
// watcher both invalidate on file events).
|
|
func ScanZddcFiles(fsRoot string) ([]string, error) {
|
|
fsRoot = filepath.Clean(fsRoot)
|
|
|
|
if cached, ok := scanCache.Load(fsRoot); ok {
|
|
return cached.([]string), nil
|
|
}
|
|
|
|
var dirs []string
|
|
err := filepath.WalkDir(fsRoot, func(path string, d fs.DirEntry, walkErr error) error {
|
|
if walkErr != nil {
|
|
// A permission error on a single subtree shouldn't kill the walk.
|
|
if os.IsPermission(walkErr) {
|
|
return nil
|
|
}
|
|
return walkErr
|
|
}
|
|
if !d.IsDir() {
|
|
return nil
|
|
}
|
|
// Prune reserved-prefix subtrees, but never the root itself.
|
|
if path != fsRoot {
|
|
name := d.Name()
|
|
if strings.HasPrefix(name, ".") || strings.HasPrefix(name, "_") {
|
|
return filepath.SkipDir
|
|
}
|
|
}
|
|
// Does this directory contain a .zddc file?
|
|
if _, err := os.Stat(filepath.Join(path, ".zddc")); err == nil {
|
|
dirs = append(dirs, path)
|
|
}
|
|
return nil
|
|
})
|
|
sort.Strings(dirs)
|
|
|
|
if err == nil {
|
|
scanCache.Store(fsRoot, dirs)
|
|
}
|
|
return dirs, err
|
|
}
|
|
|
|
// InvalidateScanCache drops every cached ScanZddcFiles result. Called by
|
|
// the writer helpers when a .zddc file is created or deleted, and by the
|
|
// archive watcher on .zddc filesystem events. Modifications to existing
|
|
// .zddc files don't change the scan output — but distinguishing create
|
|
// from modify in fsnotify is fragile, so we treat any event as
|
|
// invalidating; a single extra walk-on-next-call is cheap compared to
|
|
// silent staleness.
|
|
func InvalidateScanCache() {
|
|
scanCache.Range(func(key, _ any) bool {
|
|
scanCache.Delete(key)
|
|
return true
|
|
})
|
|
}
|