Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
60 changes: 60 additions & 0 deletions .github/workflows/miner-ui-demo-deploy.yml
Original file line number Diff line number Diff line change
@@ -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
26 changes: 20 additions & 6 deletions apps/loopover-miner-ui/README.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
5 changes: 5 additions & 0 deletions apps/loopover-miner-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
13 changes: 11 additions & 2 deletions apps/loopover-miner-ui/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -48,4 +57,4 @@ export default defineConfig({
port: 4174,
strictPort: true,
},
});
}));
14 changes: 14 additions & 0 deletions apps/loopover-miner-ui/wrangler.jsonc
Original file line number Diff line number Diff line change
@@ -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",
},
}
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion test/unit/ci-extension-packages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
12 changes: 11 additions & 1 deletion test/unit/ci-ui-build-openapi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});

Expand All @@ -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");
});
});
23 changes: 18 additions & 5 deletions test/unit/codecov-policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -178,14 +176,19 @@ 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.
expect(bundleStep["continue-on-error"]).toBe(true);
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<string, string>;
Expand All @@ -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<string, string>;
devDependencies: Record<string, string>;
};
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)", () => {
Expand Down
Loading