diff --git a/Makefile b/Makefile
index a7e1050..e7cb667 100644
--- a/Makefile
+++ b/Makefile
@@ -3,7 +3,7 @@
# two of them concurrently would corrupt each other's containers.
.NOTPARALLEL:
-.PHONY: test lint yaegi-check integration integration-keep integration-up integration-wait integration-down
+.PHONY: test lint yaegi-check admin-ui integration integration-keep integration-up integration-wait integration-down
test:
go test ./... -count=1
@@ -17,6 +17,18 @@ lint:
fi
go vet ./...
+# admin-ui rebuilds the Vue admin panel (webui/) and regenerates
+# admin_assets_gen.go — the repo-root Go file admin.go's serveAdminPage/
+# serveAdminAsset actually serve. Only ever needed after a webui/ source
+# change; CI never runs this (admin_assets_gen.go is committed, and
+# .gitattributes export-ignores webui/ from release source tarballs — see
+# its own comment), so CI never needs node. `npm ci` requires
+# webui/package-lock.json, which is committed for exactly this.
+admin-ui:
+ cd webui && npm ci && npm run build
+ node webui/generate.mjs
+ gofmt -w admin_assets_gen.go
+
yaegi-check:
cd tools/yaegi-check && GOWORK=off go run . $(CURDIR)
diff --git a/admin.go b/admin.go
index 6754cf2..c1d8d29 100644
--- a/admin.go
+++ b/admin.go
@@ -3,7 +3,6 @@ package traefikllmgateway
import (
"encoding/json"
"fmt"
- "io"
"net/http"
"net/url"
"strconv"
@@ -11,31 +10,44 @@ import (
"time"
)
-// The four routes the read-only admin dashboard registers (spec §4, v0.2;
-// adminUsageHistoryPath added by the v0.2 data-layer task), matched only
-// when adminEnabled(g.cfg) — see llmgateway.go's ServeHTTP dispatch.
+// The admin dashboard's routes (spec §4, v0.2; adminUsageHistoryPath added
+// by the v0.2 data-layer task; adminAssetsPathPrefix added by the Vue
+// admin-panel task), matched only when adminEnabled(g.cfg) — see
+// llmgateway.go's ServeHTTP dispatch. adminAssetsPathPrefix is a prefix,
+// not one fixed path: every hashed filename the Vite build emits
+// (admin_assets_gen.go) is served under it.
const (
adminPagePath = "/admin"
+ adminAssetsPathPrefix = "/admin/assets/"
adminOverviewPath = "/admin/api/overview"
adminUsagePath = "/admin/api/usage"
adminUsageHistoryPath = "/admin/api/usage/history"
)
-// adminCSP is the Content-Security-Policy header served with all three
-// admin routes (spec §4, v0.2; extended to the two JSON routes by a
-// folded review item, 2026-08-20 review): no external assets of any
-// kind, only this page's own inline script/style, and fetch calls
-// restricted to same-origin — the dashboard works air-gapped and cannot
-// be coerced into loading anything off-host. frame-ancestors/base-uri/
-// form-action are all 'none' (v0.2 final review wave, 2026-08-20): the
-// page carries a password-type key-entry input, so it must never be
-// embeddable in another site's frame (clickjacking), never have its
-// href hijacked to retarget a relative script/fetch URL, and never
-// have its auth-form submitted anywhere but nowhere at all — the form's
-// own submit handler already intercepts and cancels the real submit
-// (adminPageHTML's "submit" listener calls preventDefault()), so
-// form-action has nothing legitimate to allow.
-const adminCSP = "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'"
+// adminCSP is the Content-Security-Policy header served with GET /admin
+// and the three /admin/api/* JSON routes (spec §4, v0.2; extended to the
+// JSON routes by a folded review item, 2026-08-20 review). It was
+// widened with `script-src 'unsafe-inline'`/`style-src 'unsafe-inline'`
+// for the original vanilla-JS single-file page; the baked Vue build (Vue
+// admin-panel task) needs neither: `vite build` emits index.html with
+// only an external `
-