diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e64102009..3ede413bb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -647,7 +647,7 @@ jobs: # honored, not just replayed from cache). - name: UI build if: ${{ github.event_name == 'push' || needs.changes.outputs.ui == 'true' }} - run: npx turbo run build --filter=@loopover/ui + run: npx turbo run build --filter=@loopover/ui --filter=@loopover/ui-miner # Paired with "Restore Turborepo cache" above (same key, same gate condition) -- deliberately # positioned here, after every turbo-routed step in this job (Build engine package, Build MCP, Build # miner CLI, Build UI-kit package, UI lint/typecheck, Extension lint/typecheck, UI build), rather than @@ -662,21 +662,21 @@ jobs: with: path: .turbo/cache key: turbo-code-${{ hashFiles('package-lock.json') }}-${{ github.run_id }} - # Bundle-size regression tracking for the deployed marketing/product site (loopover-ui only -- - # loopover-miner-ui is a self-hosted operator dashboard with no build step in this job at all, see - # "UI build" above). Fork-excluded (no secrets.CODECOV_TOKEN on a fork PR, same boundary as the - # coverage-upload steps below) and non-blocking (continue-on-error): unlike coverage, nothing gates - # merges on bundle size, so an upload hiccup must never fail CI. Uses @codecov/bundle-analyzer's - # standalone CLI against the built dist/client directly rather than a bundler-plugin hook -- - # @codecov/vite-plugin's latest release doesn't support this repo's Vite 8 yet (peer range caps at - # 6.x; this repo's Vite 8 runs on rolldown, not classic rollup, so @codecov/rollup-plugin doesn't fit - # either) -- revisit wiring the plugin in once upstream catches up. + # Bundle-size regression tracking for the deployed marketing/product site (loopover-ui) and the + # miner-ui demo/self-host SPA (loopover-miner-ui, #5963). Fork-excluded (no secrets.CODECOV_TOKEN + # on a fork PR, same boundary as the coverage-upload steps below) and non-blocking + # (continue-on-error): unlike coverage, nothing gates merges on bundle size, so an upload hiccup + # must never fail CI. Uses @codecov/bundle-analyzer's standalone CLI against the built dist + # directly rather than a bundler-plugin hook -- @codecov/vite-plugin's latest release doesn't + # support this repo's Vite 8 yet (peer range caps at 6.x; this repo's Vite 8 runs on rolldown, + # not classic rollup, so @codecov/rollup-plugin doesn't fit either) -- revisit wiring the plugin + # in once upstream catches up. - name: Upload UI bundle stats to Codecov if: ${{ (github.event_name == 'push' || needs.changes.outputs.ui == 'true') && github.event.pull_request.head.repo.fork != true }} continue-on-error: true env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - run: npm run bundle-analysis --workspace @loopover/ui + run: npm run bundle-analysis --workspace @loopover/ui && npm run bundle-analysis --workspace @loopover/ui-miner # The full-suite coverage run, sharded (#ci-shard-coverage). This alone was ~9-10 of the ~11 minutes a # typical backend PR spent in validate-code, because vitest schedules whole test FILES atomically to diff --git a/.github/workflows/miner-ui-demo-deploy.yml b/.github/workflows/miner-ui-demo-deploy.yml new file mode 100644 index 000000000..547de53cf --- /dev/null +++ b/.github/workflows/miner-ui-demo-deploy.yml @@ -0,0 +1,60 @@ +name: Deploy miner-ui demo Worker + +# Manual deploy of the apps/loopover-miner-ui public demo (#5963). Mirrors ui-deploy.yml's +# workflow_dispatch-only trigger so a stale or broken demo never redeploys on every commit. +# Builds with Vite `--mode demo` (VITE_DEMO_MODE baked in vite.config.ts) so the static SPA needs no +# SQLite / Vite middleware backend on Cloudflare. + +on: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: miner-ui-demo-deploy-${{ github.ref }} + cancel-in-progress: true + +jobs: + deploy: + name: Build and deploy demo + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + persist-credentials: false + + - name: Check Cloudflare secrets + id: cfg + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + run: | + if [ -n "$CLOUDFLARE_API_TOKEN" ] && [ -n "$CLOUDFLARE_ACCOUNT_ID" ]; then + echo "ready=true" >> "$GITHUB_OUTPUT" + else + echo "ready=false" >> "$GITHUB_OUTPUT" + echo "::notice::CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID not set — building demo only, skipping deploy." + fi + + - name: Setup Node + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 + with: + node-version-file: .nvmrc + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Build miner-ui demo + run: npx turbo run build --filter=@loopover/engine --filter=@loopover/ui-kit && npm --workspace @loopover/ui-miner run build:demo + + - name: Deploy demo Worker + if: steps.cfg.outputs.ready == 'true' + working-directory: apps/loopover-miner-ui + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + run: npx wrangler deploy diff --git a/apps/loopover-miner-ui/README.md b/apps/loopover-miner-ui/README.md index c4da76961..8d136fb56 100644 --- a/apps/loopover-miner-ui/README.md +++ b/apps/loopover-miner-ui/README.md @@ -1,15 +1,29 @@ # LoopOver Miner UI Local, read-only dashboard for a laptop or fleet miner instance. It mirrors the main -`apps/loopover-ui/` tooling versions (React 19, TanStack Router, Vite, Tailwind v4) but intentionally -does **not** adopt that app's Cloudflare Worker deploy model or `@lovable.dev/*` scaffold dependency. +`apps/loopover-ui/` tooling versions (React 19, TanStack Router, Vite, Tailwind v4). The miner package invariant is client-side only with no required phone-home to boot -(`packages/loopover-miner/DEPLOYMENT.md`). This app is a plain Vite dev server / static build that a -local miner CLI can serve later — not a Wrangler deploy target. +(`packages/loopover-miner/DEPLOYMENT.md`). For day-to-day operator use this app is a plain Vite +dev server / static build that a local miner CLI can serve — see “Running as a persistent +service” below. -The Phase 6 data views have shipped: an overview summary (`routes/index.tsx`, #4853) alongside dedicated -run-history, portfolio, and ledgers views, each fed by the local read-only `/api/*` endpoints below. +## Public demo Worker (#5963) + +A **demo-mode** Cloudflare Worker build exists for sales/marketing prototypes and for Codecov +Bundle Analysis in CI. It uses the same SPA codebase with `VITE_DEMO_MODE=1` baked at build time +(`vite build --mode demo`), so client fetchers return synthetic fixtures and no SQLite / Vite +middleware backend is required on Cloudflare. + +| Command | Purpose | +| --- | --- | +| `npm run build:demo` (in this workspace) | Static SPA with demo mode baked in | +| `npm run deploy:demo` | Build demo + `wrangler deploy` (needs CF credentials) | +| `.github/workflows/miner-ui-demo-deploy.yml` | Manual `workflow_dispatch` CI deploy (mirrors `ui-deploy.yml`) | + +Custom domain routing is intentionally omitted until DNS is provisioned; `workers.dev` preview is +enough for the prototype. The Worker name is `loopover-miner-ui-demo` so it never collides with +`loopover-ui` / `loopover-api`. ## Configuration diff --git a/apps/loopover-miner-ui/package.json b/apps/loopover-miner-ui/package.json index eaa28a924..4e8ecbb6c 100644 --- a/apps/loopover-miner-ui/package.json +++ b/apps/loopover-miner-ui/package.json @@ -8,7 +8,11 @@ "scripts": { "dev": "vite dev", "build": "vite build", + "build:demo": "vite build --mode demo", "preview": "vite preview", + "preview:demo": "vite preview --mode demo", + "bundle-analysis": "bundle-analyzer ./dist --bundle-name=loopover-miner-ui --upload-token=$CODECOV_TOKEN", + "deploy:demo": "npm run build:demo && wrangler deploy", "typecheck": "tsc --noEmit", "lint": "eslint .", "test": "vitest run --coverage", @@ -26,6 +30,7 @@ "vite-tsconfig-paths": "^6.1.1" }, "devDependencies": { + "@codecov/bundle-analyzer": "^2.0.1", "@eslint/js": "^9.39.4", "@tanstack/router-plugin": "^1.168.19", "@testing-library/dom": "^10.4.1", diff --git a/apps/loopover-miner-ui/vite.config.ts b/apps/loopover-miner-ui/vite.config.ts index 9abb4b339..38d88b43b 100644 --- a/apps/loopover-miner-ui/vite.config.ts +++ b/apps/loopover-miner-ui/vite.config.ts @@ -17,7 +17,16 @@ import { portfolioQueueApiPlugin } from "./vite-portfolio-queue-api"; import { rankedCandidatesApiPlugin } from "./vite-ranked-candidates-api"; import { runStateApiPlugin } from "./vite-run-state-api"; -export default defineConfig({ +export default defineConfig(({ mode }) => ({ + // `--mode demo` (#5963) — bake VITE_DEMO_MODE so the static CF Worker build needs no .env file + // (`.env.*` is gitignored). Must be the string `"1"` to match `isDemoMode()` in demo-data.ts. + // Self-host `vite build` / `vite dev` leave demo mode off. + define: + mode === "demo" + ? { + "import.meta.env.VITE_DEMO_MODE": JSON.stringify("1"), + } + : undefined, plugins: [ TanStackRouterVite({ target: "react", autoCodeSplitting: true }), react(), @@ -48,4 +57,4 @@ export default defineConfig({ port: 4174, strictPort: true, }, -}); +})); diff --git a/apps/loopover-miner-ui/wrangler.jsonc b/apps/loopover-miner-ui/wrangler.jsonc new file mode 100644 index 000000000..d2e1a1e98 --- /dev/null +++ b/apps/loopover-miner-ui/wrangler.jsonc @@ -0,0 +1,14 @@ +{ + // Public prototype/demo Worker for apps/loopover-miner-ui (#5963). Static SPA assets only — + // client fetchers serve synthetic data when built with `vite build --mode demo`. Custom domain + // routes are intentionally omitted until DNS is provisioned; workers.dev preview is enough for + // the prototype. Distinct Worker name so it never collides with `loopover-ui` / `loopover-api`. + "name": "loopover-miner-ui-demo", + "compatibility_date": "2026-05-28", + "workers_dev": true, + "preview_urls": true, + "assets": { + "directory": "./dist", + "not_found_handling": "single-page-application", + }, +} diff --git a/package-lock.json b/package-lock.json index 6fa0ae9d4..e2e01e828 100644 --- a/package-lock.json +++ b/package-lock.json @@ -92,6 +92,7 @@ "vite-tsconfig-paths": "^6.1.1" }, "devDependencies": { + "@codecov/bundle-analyzer": "^2.0.1", "@eslint/js": "^9.39.4", "@tanstack/router-plugin": "^1.168.19", "@testing-library/dom": "^10.4.1", diff --git a/test/unit/ci-extension-packages.test.ts b/test/unit/ci-extension-packages.test.ts index eff1df001..5ba2734db 100644 --- a/test/unit/ci-extension-packages.test.ts +++ b/test/unit/ci-extension-packages.test.ts @@ -35,6 +35,7 @@ describe("browser extension workspace packages (#4866)", () => { "npx turbo run typecheck --filter=@loopover/extension --filter=@loopover/miner-extension", ); // @loopover/ui#build's dependsOn (turbo.json) covers the extension + miner-extension build pair. - expect(workflow).toContain("run: npx turbo run build --filter=@loopover/ui"); + // #5963 also builds @loopover/ui-miner for Codecov Bundle Analysis. + expect(workflow).toContain("run: npx turbo run build --filter=@loopover/ui --filter=@loopover/ui-miner"); }); }); diff --git a/test/unit/ci-ui-build-openapi.test.ts b/test/unit/ci-ui-build-openapi.test.ts index a3584b29f..2913be686 100644 --- a/test/unit/ci-ui-build-openapi.test.ts +++ b/test/unit/ci-ui-build-openapi.test.ts @@ -18,7 +18,8 @@ describe("UI build steps skip the redundant OpenAPI regen", () => { // @loopover/ui#build's dependsOn (turbo.json) covers the same extension + miner-extension build pair // the old hand-chained `&&` command ran explicitly -- see turbo.json's comment on that task. - expect(step).toContain("run: npx turbo run build --filter=@loopover/ui"); + // #5963 also builds @loopover/ui-miner so Codecov Bundle Analysis has a dist to upload for it. + expect(step).toContain("run: npx turbo run build --filter=@loopover/ui --filter=@loopover/ui-miner"); expect(step).not.toContain("npm run ui:build"); }); @@ -30,4 +31,13 @@ describe("UI build steps skip the redundant OpenAPI regen", () => { ); expect(workflow).not.toContain("&& npm run ui:build"); }); + + it("miner-ui-demo-deploy.yml builds the demo mode SPA on workflow_dispatch (#5963)", () => { + const workflow = read(".github/workflows/miner-ui-demo-deploy.yml"); + expect(workflow).toContain("workflow_dispatch"); + expect(workflow).toContain("npm --workspace @loopover/ui-miner run build:demo"); + expect(workflow).toContain("npx wrangler deploy"); + // Vite config resolution imports engine packages; build engine (+ ui-kit) before demo. + expect(workflow).toContain("npx turbo run build --filter=@loopover/engine --filter=@loopover/ui-kit"); + }); }); diff --git a/test/unit/codecov-policy.test.ts b/test/unit/codecov-policy.test.ts index 55b952bd2..d5a14c215 100644 --- a/test/unit/codecov-policy.test.ts +++ b/test/unit/codecov-policy.test.ts @@ -165,10 +165,8 @@ describe("Codecov policy", () => { expect(trustedWith.token).toBe("${{ secrets.CODECOV_TOKEN }}"); }); - it("uploads loopover-ui's bundle stats without letting an upload hiccup fail CI", () => { - // loopover-miner-ui deliberately has no build step in this job at all (self-hosted operator - // dashboard, not something we deploy -- see "UI build"'s own comment), so there is nothing to - // upload bundle stats for on that app here. + it("uploads loopover-ui and loopover-miner-ui bundle stats without letting an upload hiccup fail CI", () => { + // Both SPAs now have a CI build step (#5963 added miner-ui); bundle uploads stay non-blocking. const workflow = readYaml(".github/workflows/ci.yml"); const validateCode = nestedRecord(workflow, ["jobs", "validate-code"]); const steps = recordArray(validateCode.steps, "jobs.validate-code.steps"); @@ -178,6 +176,9 @@ describe("Codecov policy", () => { expect(buildIndex).toBeGreaterThan(-1); expect(bundleIndex).toBeGreaterThan(buildIndex); + const buildStep = steps[buildIndex]!; + expect(String(buildStep.run)).toContain("--filter=@loopover/ui-miner"); + const bundleStep = steps[bundleIndex]!; // Unlike the coverage uploads above, nothing gates a merge on bundle size -- a Codecov outage or a // missing token must never fail CI for an otherwise-honest PR. @@ -185,7 +186,9 @@ describe("Codecov policy", () => { expect(String(bundleStep.if)).toContain("github.event.pull_request.head.repo.fork != true"); const bundleEnv = record(bundleStep.env, "bundle step env"); expect(bundleEnv.CODECOV_TOKEN).toBe("${{ secrets.CODECOV_TOKEN }}"); - expect(bundleStep.run).toBe("npm run bundle-analysis --workspace @loopover/ui"); + expect(bundleStep.run).toBe( + "npm run bundle-analysis --workspace @loopover/ui && npm run bundle-analysis --workspace @loopover/ui-miner", + ); const uiPkg = JSON.parse(readFileSync("apps/loopover-ui/package.json", "utf8")) as { scripts: Record; @@ -195,6 +198,16 @@ describe("Codecov policy", () => { "bundle-analyzer ./dist/client --bundle-name=loopover-ui --upload-token=$CODECOV_TOKEN", ); expect(uiPkg.devDependencies["@codecov/bundle-analyzer"]).toBeDefined(); + + const minerUiPkg = JSON.parse(readFileSync("apps/loopover-miner-ui/package.json", "utf8")) as { + scripts: Record; + devDependencies: Record; + }; + expect(minerUiPkg.scripts["bundle-analysis"]).toBe( + "bundle-analyzer ./dist --bundle-name=loopover-miner-ui --upload-token=$CODECOV_TOKEN", + ); + expect(minerUiPkg.scripts["build:demo"]).toBe("vite build --mode demo"); + expect(minerUiPkg.devDependencies["@codecov/bundle-analyzer"]).toBeDefined(); }); it("captures review-enrichment node:test coverage for Codecov (#6250)", () => {