package apps import ( "path/filepath" "strings" "codeberg.org/VARASYS/ZDDC/zddc/internal/zddc" ) // AppAvailableAt reports whether app's virtual HTML can be served at // requestDir. Delegates to the .zddc cascade's available_tools union // (zddc.IsToolAvailableAt). The convention previously hardcoded here // now lives in defaults.zddc.yaml and is overridable per-directory // by operators. // // Operators can always drop a real .html file at any path to // override — that path is served by the static handler regardless of // this function's result. AppAvailableAt is consulted only when no // real file exists. // // Landing is a special case: the cascade declares it available // universally (since available_tools concat-merges from the root // baseline), but the dispatcher only auto-serves landing at the // deployment root. We enforce that here too so callers don't trip // over project-deep landing requests. func AppAvailableAt(root, requestDir, app string) bool { root = filepath.Clean(root) requestDir = filepath.Clean(requestDir) if app == "landing" { return requestDir == root } return zddc.IsToolAvailableAt(root, requestDir, app) } // DefaultAppAt returns the canonical default tool name for requestDir, // or "" if no specific tool fits. Used by the dispatcher to decide // which app to serve at a directory URL with no trailing slash — // trailing-slash URLs serve the browse app for any directory. // // Rules (case-insensitive on canonical folder names): // // - /archive//mdl/... → "tables" // - /archive/ → "archive" // - /archive//... → "archive" // - /staging/... → "transmittal" // - /working/... → "browse" (hosts the // markdown editor plugin) // - /reviewing/... → "browse" (operates on the // virtual aggregator listing) // - any other directory → "" (no default) // // The mdl rule wins over the broader archive rule because the table // editor is a more specific surface for browsing planned deliverables // than the archive index. Note: the dir at archive//mdl/ // itself IS the table — its table.yaml + form.yaml + row YAMLs all // live there together (self-contained directory). // // requestDir and root are absolute filesystem paths; requestDir must // be under root (otherwise "" is returned). // // Phase 3b: delegates to zddc.DefaultToolAt, which resolves the // answer from the .zddc cascade (operator on-disk + embedded // defaults). The convention previously hardcoded in the switch // statement below now lives in zddc/internal/zddc/defaults.zddc.yaml // and is overridable per-directory by operators. // // Project root itself (depth-1) still returns "" — the cascade // doesn't declare a default tool for it (landing is handled by the // dispatcher's own depth-1 check). func DefaultAppAt(root, requestDir string) string { root = filepath.Clean(root) requestDir = filepath.Clean(requestDir) if requestDir == root { return "" } rel, err := filepath.Rel(root, requestDir) if err != nil || strings.HasPrefix(rel, "..") { return "" } parts := strings.Split(rel, string(filepath.Separator)) if len(parts) < 2 { // Project root itself — no default tool from the cascade. // (Landing is handled separately by the dispatcher.) return "" } return zddc.DefaultToolAt(root, requestDir) }