mirror of
https://github.com/hanzoai/tasks.git
synced 2026-10-04 02:04:03 +00:00
Hanzo CI/CD / cicd (push) Failing after 6m22s
CI/CD / cicd (push) Failing after 6m23s
A registration that is not a typed operation is spelled Raw now, with the method named, so opting out of the typed model reads as what it is. The zip this pins also states the wire explicitly, which matters more than the version gap suggests: JSON semantics follow the toolchain, so the older pin answered with different bodies than the code was written against once the builder moved. Co-authored-by: Hanzo Dev <[email protected]>
109 lines
3.7 KiB
Go
109 lines
3.7 KiB
Go
// Package ui exposes the built admin-tasks (@hanzo/gui) bundle as an
|
|
// embedded filesystem.
|
|
//
|
|
// The bundle is produced externally in the @hanzo/gui workspace at
|
|
// ~/work/hanzo/gui/code/admin-tasks (Vite + @hanzo/gui) and synced into
|
|
// ui/dist by scripts/sync-admin-ui.sh. The synced dist/ is baked into
|
|
// the tasksd binary at compile time — no external static-assets
|
|
// directory, no sidecar, no separate deploy. See ui/README.md.
|
|
//
|
|
// Mount the returned handler at /_/tasks/ in the tasksd HTTP router
|
|
// so it serves the SPA shell for every non-API request:
|
|
//
|
|
// import tasksui "github.com/hanzoai/tasks/ui"
|
|
// router.PathPrefix("/_/tasks/").Handler(http.StripPrefix("/_/tasks", tasksui.Handler()))
|
|
//
|
|
// API routes under /v1/* take precedence via earlier route
|
|
// registration; anything else falls through to the SPA, which uses
|
|
// client-side routing so deep links survive reload.
|
|
package ui
|
|
|
|
import (
|
|
"embed"
|
|
"io/fs"
|
|
"net/http"
|
|
"path"
|
|
"strings"
|
|
)
|
|
|
|
//go:embed all:dist
|
|
var distFS embed.FS
|
|
|
|
// FS returns the embedded built-UI filesystem rooted at dist/.
|
|
// Empty when scripts/sync-admin-ui.sh has not been run (dev workflow:
|
|
// run the Vite dev server inside ~/work/hanzo/gui/code/admin-tasks).
|
|
func FS() fs.FS {
|
|
sub, err := fs.Sub(distFS, "dist")
|
|
if err != nil {
|
|
// Impossible at runtime because embed.FS entries are
|
|
// validated at compile time; if dist/ is missing the
|
|
// binary simply carries an empty FS.
|
|
return distFS
|
|
}
|
|
return sub
|
|
}
|
|
|
|
// Handler returns an http.Handler that serves the embedded SPA.
|
|
//
|
|
// Behaviour:
|
|
// - Exact-match static assets (JS/CSS/images) ship with
|
|
// immutable cache hints because Vite hashes filenames.
|
|
// - Anything else rewrites to /index.html so the React router
|
|
// handles the route client-side. This is the standard SPA
|
|
// fallback and is the only way react-router's BrowserRouter
|
|
// survives a page reload on a deep link.
|
|
// - If the build hasn't run and index.html is missing, every
|
|
// request returns 503 so operators notice in staging before
|
|
// shipping a blank image to production.
|
|
func Handler() http.Handler {
|
|
root := FS()
|
|
fileServer := http.FileServer(http.FS(root))
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Only GET/HEAD — the UI is static; any other method on a
|
|
// non-/api path is a client error, not a route we own.
|
|
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
reqPath := strings.TrimPrefix(path.Clean(r.URL.Path), "/")
|
|
if reqPath == "" {
|
|
reqPath = "index.html"
|
|
}
|
|
|
|
// Probe for the asset. If it doesn't exist, serve the SPA
|
|
// shell so client-side routing picks up the deep link.
|
|
if _, err := fs.Stat(root, reqPath); err != nil {
|
|
serveIndex(w, r, root)
|
|
return
|
|
}
|
|
|
|
// Hashed assets under /assets/ are immutable — Vite emits
|
|
// them with content-addressed names so it's safe to tell
|
|
// browsers to cache forever.
|
|
if strings.HasPrefix(reqPath, "assets/") {
|
|
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
|
|
} else {
|
|
w.Header().Set("Cache-Control", "no-cache")
|
|
}
|
|
fileServer.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
// serveIndex writes index.html with no-cache so a newly-deployed
|
|
// build replaces the stale shell on the next request.
|
|
func serveIndex(w http.ResponseWriter, r *http.Request, root fs.FS) {
|
|
data, err := fs.ReadFile(root, "index.html")
|
|
if err != nil {
|
|
// No UI bundled — report honestly. Operators see this in
|
|
// logs and in the browser; no blank screen in production.
|
|
http.Error(w, "tasks UI not built (run scripts/sync-admin-ui.sh)", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
w.Header().Set("Cache-Control", "no-cache")
|
|
_, _ = w.Write(data)
|
|
_ = r
|
|
}
|