diff --git a/.gitignore b/.gitignore index 8e5ac39..2ac58b7 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,8 @@ /dist/ *.test coverage.out + +# Prods configuration +prods.ini + +.pnpm-store \ No newline at end of file diff --git a/LICENSE_POLICY.md b/LICENSE_POLICY.md index 22bd4d1..2b4c87f 100644 --- a/LICENSE_POLICY.md +++ b/LICENSE_POLICY.md @@ -17,7 +17,7 @@ MIT, ISC, 0BSD, BSD-2-Clause, BSD-3-Clause, and Apache-2.0 dependencies are norm ## Audited dependency snapshot -Audit date: 2026-09-16. Exact inputs and expected counts are recorded in [`licenses/audit.json`](licenses/audit.json). +Audit date: 2026-09-18. Exact inputs and expected counts are recorded in [`licenses/audit.json`](licenses/audit.json). | Scope | Result | | --- | --- | diff --git a/README-zh_TW.md b/README-zh_TW.md index 504f7d4..b57540a 100644 --- a/README-zh_TW.md +++ b/README-zh_TW.md @@ -198,6 +198,31 @@ Browser / crawler - Restore 使用 durable journal;`prepared` 之前可安全放棄,之後只能用同一 operation 依各 root 證據向前完成。 - Admin 使用不透明 server-side session、server-side authorization、same-origin CSRF 與可信任 Host/proxy 設定。 +## 本地 UI 開發(Admin + Public HMR) + +正式環境仍內嵌建置後的 Admin/Public 資產。本地 React HMR 請以 Vite 當瀏覽器入口,並反代到 Go: + +1. Terminal A — 啟動 Prods(`base_url` 維持指向 Go listen,例如 `http://127.0.0.1:3310`): + +```sh +./prods +``` + +Vite proxy 會把瀏覽器的 `Origin`/`Referer`(`:5173`)改寫成 Go 的 `base_url` origin,避免 Admin POST(例如 `/admin/login`)被 `EnforceHost` 回 403。 + +2. Terminal B — 啟動 Vite proxy(此流程才需要 Node.js/pnpm): + +```sh +pnpm --dir web/ui install --frozen-lockfile +pnpm --dir web/ui dev +``` + +3. 瀏覽 `http://127.0.0.1:5173/` 與 `http://127.0.0.1:5173/admin`(`http://localhost:5173` 也可)。不要用 `:3310` 做 UI 修改;該埠仍提供內嵌 bundle,沒有 HMR。 + +若 Go listen 不是 `http://127.0.0.1:3310`,可用 `PRODS_DEV_PROXY_TARGET` 覆寫 proxy 目標(須與 `prods.ini` 的 `base_url` 一致)。 + +限制:Public HMR 只涵蓋 islands JS 與 `public.css`;Go 模板/SSR HTML 變更仍需整頁重整。Admin Fast Refresh 在改到 root mount 或 provider 外層時可能整頁 remount。發行驗證仍以 `pnpm --dir web/ui build`、`go build` 與直接執行 binary 為準。 + ## 從原始碼建置與驗證 所需工具: diff --git a/README.md b/README.md index c0cd0f9..e86fd67 100644 --- a/README.md +++ b/README.md @@ -198,6 +198,31 @@ Key boundaries: - Restore uses a durable journal. Before `prepared` it can be abandoned safely; after `prepared` it only rolls forward through the same operation with per-root evidence. - Admin uses opaque server-side sessions, server-side authorization, same-origin CSRF protection, and trusted Host/proxy settings. +## Local UI development (Admin + Public HMR) + +Production still embeds built Admin and Public assets. For local React HMR, run Vite as the browser front door and proxy to the Go process: + +1. Terminal A — start Prods (keep `base_url` on the Go listen address, e.g. `http://127.0.0.1:3310`): + +```sh +./prods +``` + +The Vite proxy rewrites the browser `Origin` / `Referer` (`:5173`) to the Go `base_url` origin so Admin POSTs (for example `/admin/login`) are not rejected by `EnforceHost` with 403. + +2. Terminal B — start the Vite proxy (requires Node.js / pnpm only for this workflow): + +```sh +pnpm --dir web/ui install --frozen-lockfile +pnpm --dir web/ui dev +``` + +3. Browse `http://127.0.0.1:5173/` and `http://127.0.0.1:5173/admin` (`http://localhost:5173` also works). Do not use `:3310` for UI edits; that port still serves the embedded bundle without HMR. + +Override the Go proxy target with `PRODS_DEV_PROXY_TARGET` when listen is not `http://127.0.0.1:3310` (it must match `base_url` in `prods.ini`). + +Limits: Public HMR covers islands JS and `public.css` only — Go template / SSR HTML changes still need a full reload. Admin Fast Refresh can remount when you edit the root mount or provider shell. Release validation remains `pnpm --dir web/ui build`, `go build`, and running the binary directly. + ## Build and verify from source Required tools: diff --git a/licenses/audit.json b/licenses/audit.json index 1a9d3b4..1848f11 100644 --- a/licenses/audit.json +++ b/licenses/audit.json @@ -1,10 +1,10 @@ { "schema_version": 1, - "audited_on": "2026-09-16", + "audited_on": "2026-09-18", "inputs": { "go.mod": "6da0f697298da66eb5925ec32aa0dc49e6b5999a93dd31b4ceb693882e9b620e", "go.sum": "1bc375f9ab523c32daa36c4facb0f968b8eb15c2c0eee05bae6d551a49a4d7ae", - "web/ui/package.json": "7e674c2007c94b857d125ea8ae2828f0ceed83f57aa8347a683fc64a4a19203e", + "web/ui/package.json": "3efb972712963df5e035b5ae0dc5544827e271792fbed6631344c2fead0654bc", "web/ui/pnpm-lock.yaml": "c8a6e141f6a610d07a8a57473f6b0233801b9e1aaf5954e97c9b123f86f07c65" }, "go": { diff --git a/web/ui/package.json b/web/ui/package.json index 64cb4e9..7bc89e2 100644 --- a/web/ui/package.json +++ b/web/ui/package.json @@ -4,6 +4,7 @@ "version": "0.1.0", "type": "module", "scripts": { + "dev": "vite --config vite.dev.config.ts", "build": "vite build --config vite.system.config.ts && node scripts/build-styles.mjs && vite build --config vite.public.config.ts && vite build --config vite.admin.config.ts", "build:styles": "node scripts/build-styles.mjs", "build:public": "node scripts/build-styles.mjs && vite build --config vite.public.config.ts", diff --git a/web/ui/scripts/dev-html-rewrite.test.ts b/web/ui/scripts/dev-html-rewrite.test.ts new file mode 100644 index 0000000..e39ed2d --- /dev/null +++ b/web/ui/scripts/dev-html-rewrite.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from "vitest"; +import { + DEV_ORIGIN, + GO_ORIGIN, + rewriteDevHtml, + rewriteLocationHeader, + rewriteUpstreamBrowserOrigin, +} from "./dev-html-rewrite"; + +describe("rewriteDevHtml", () => { + it("rewrites admin shell for Vite HMR", () => { + const html = `
+ + +`; + const out = rewriteDevHtml(html); + expect(out).not.toContain("/static/admin/style.css"); + expect(out).not.toContain("/static/admin/admin.js"); + expect(out).toContain('src="/src/admin/main.tsx"'); + expect(out).toContain("/@vite/client"); + expect(out).toContain("/src/dev/hmr-preamble.js"); + expect(out).not.toContain("injectIntoGlobalHook"); + }); + + it("rewrites public islands and stylesheet", () => { + const html = ` + +`; + const out = rewriteDevHtml(html); + expect(out).toContain('href="/src/public/style.css"'); + expect(out).toContain('src="/src/public/main.tsx"'); + expect(out).toContain("/@vite/client"); + expect(out).toContain("/src/dev/hmr-preamble.js"); + }); + + it("leaves unrelated HTML unchanged", () => { + const html = `ok
`; + expect(rewriteDevHtml(html)).toBe(html); + }); +}); + +describe("rewriteLocationHeader", () => { + it("maps Go origin to Vite origin", () => { + expect(rewriteLocationHeader(`${GO_ORIGIN}/admin`)).toBe(`${DEV_ORIGIN}/admin`); + expect(rewriteLocationHeader("http://localhost:3310/")).toBe(`${DEV_ORIGIN}/`); + }); +}); + +describe("rewriteUpstreamBrowserOrigin", () => { + it("maps Vite browser origins to the Go base URL origin", () => { + expect(rewriteUpstreamBrowserOrigin("http://localhost:5173")).toBe(GO_ORIGIN); + expect(rewriteUpstreamBrowserOrigin("http://127.0.0.1:5173")).toBe(GO_ORIGIN); + expect(rewriteUpstreamBrowserOrigin("http://localhost:5173/admin/login")).toBe( + `${GO_ORIGIN}/admin/login`, + ); + expect(rewriteUpstreamBrowserOrigin("https://evil.example")).toBe("https://evil.example"); + }); +}); diff --git a/web/ui/scripts/dev-html-rewrite.ts b/web/ui/scripts/dev-html-rewrite.ts new file mode 100644 index 0000000..8713315 --- /dev/null +++ b/web/ui/scripts/dev-html-rewrite.ts @@ -0,0 +1,84 @@ +/** HTML / Location / Origin rewrites for the Vite-fronted local HMR proxy. */ + +export const GO_ORIGIN = "http://127.0.0.1:3310"; +export const DEV_ORIGIN = "http://127.0.0.1:5173"; + +/** External only — Go CSP allows script-src 'self', not unsafe-inline. */ +const HMR_SCRIPTS = [ + ``, + ``, +].join(""); + +/** Rewrite Go-served HTML so Admin / Public load Vite source entries with HMR. */ +export function rewriteDevHtml(html: string): string { + let out = html; + const before = out; + + out = out.replace(//gi, ""); + out = out.replace( + /(