package handler import ( "html" "net/http" "strings" "codeberg.org/VARASYS/ZDDC/zddc/internal/apps" ) // AppsVirtualPrefix is a virtual, public directory that serves the standalone // tool HTMLs so anyone can grab one and run it against their OWN local // filesystem — download it, open it from disk, and it runs offline via the // browser's File System Access picker. It carries no project data, so it's // served with no auth/ACL. `_`-prefixed names are system-reserved (mkdir // rejects them), so /_apps/ never collides with content, and it's virtual — // nothing exists on disk. const AppsVirtualPrefix = "/_apps/" type standaloneApp struct { File string // URL + filename, e.g. "classifier.html" Name string Desc string } // The order here is the order shown on the index; "local-first" tools lead. var standaloneApps = []standaloneApp{ {"classifier.html", "Classifier", "Rename a local folder of files to ZDDC naming conventions."}, {"browse.html", "Browse", "Browse + edit a local directory tree — markdown, YAML, CSV, and more."}, {"transmittal.html", "Transmittal", "Assemble a transmittal package from local files."}, {"tables.html", "Tables", "View and edit a directory of YAML rows as a sortable table."}, {"form.html", "Form", "Edit a single YAML record with a schema-driven form."}, {"archive.html", "Archive", "Review an archive of ZDDC-named files."}, } // appBytesForFile returns the HTML for a /_apps/ request, or nil for an // unknown file. It prefers the site .zddc.zip bundle member (operator // override / the freshest dev build) and falls back to the binary's embedded // copy. tables + form share the one embedded tables/form bundle. func appBytesForFile(appsSrv *apps.Server, file string) []byte { if appsSrv != nil && appsSrv.Bundle != nil { if b, ok := appsSrv.Bundle.Member(file); ok && len(b) > 0 { return b } } switch file { case "classifier.html": return apps.EmbeddedBytes("classifier") case "browse.html": return apps.EmbeddedBytes("browse") case "transmittal.html": return apps.EmbeddedBytes("transmittal") case "archive.html": return apps.EmbeddedBytes("archive") case "tables.html", "form.html": return EmbeddedTablesHTML() } return nil } // ServeApps handles GET/HEAD under /_apps/. "/_apps/" → an index of the // standalone tools (Download / Open links); "/_apps/.html" → that tool's // embedded HTML. Append ?download to force a save dialog. No auth — tool UI // only, no data. func ServeApps(appsSrv *apps.Server, w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet && r.Method != http.MethodHead { w.Header().Set("Allow", "GET, HEAD") http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) return } rest := strings.TrimPrefix(r.URL.Path, AppsVirtualPrefix) if rest == "" || rest == "index" { serveAppsIndex(w, r) return } if strings.Contains(rest, "/") { http.NotFound(w, r) return } body := appBytesForFile(appsSrv, rest) if body == nil { http.NotFound(w, r) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") // Conditional-GET-friendly: revalidate each load; bytes change only on a // binary redeploy. (Index is no-store; it's tiny + generated.) w.Header().Set("Cache-Control", "max-age=0, must-revalidate") if r.URL.Query().Get("download") != "" { w.Header().Set("Content-Disposition", `attachment; filename="`+rest+`"`) } if r.Method == http.MethodHead { return } _, _ = w.Write(body) } func serveAppsIndex(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Cache-Control", "no-store") if r.Method == http.MethodHead { return } var b strings.Builder b.WriteString(``) b.WriteString(``) b.WriteString(`ZDDC standalone apps`) b.WriteString(`

ZDDC standalone apps

`) b.WriteString(`

Each tool is a single, self-contained HTML file. Download one and open it from your disk to run it offline against a folder on your own machine (use a Chromium-family browser — it needs the File System Access API). Open runs it here, against the server.

`) for _, a := range standaloneApps { b.WriteString(`

` + html.EscapeString(a.Name) + `

`) b.WriteString(`

` + html.EscapeString(a.Desc) + `

`) b.WriteString(`Download`) b.WriteString(`Open`) b.WriteString(`
`) } b.WriteString(``) _, _ = w.Write([]byte(b.String())) }