deploy: live at canvas.flow-master.ai
Some checks failed
build-and-publish / test (push) Has been cancelled
build-and-publish / image (push) Has been cancelled

- index.html title 'FlowMaster · Mission Control' + theme-color #1a2740
- Settings About box names canvas.flow-master.ai as the canonical URL
- README rewritten: live URL up top, deploy section reflects merged PR
  + Cloudflare A record added (this session), end-to-end serving with
  cert-manager TLS + nginx /api/* proxy to demo.flow-master.ai
- .dockerignore added (excludes node_modules, dist, qa/screenshots,
  .git, bak files) — keeps the build context lean
- qa/smoke_canvas.mjs (new) — real-URL Playwright smoke against
  https://canvas.flow-master.ai with --host-resolver-rules so the
  local DNS cache can't shadow the freshly-published A record.
  8/8 PASS, 0 console errors:
    ✓ landing renders at canvas.flow-master.ai
    ✓ hero title present
    ✓ scenario cards >= 7 (count=7)
    ✓ mission control loaded
    ✓ blueprint canvas mounted
    ✓ blueprint nodes rendered (nodes=3)
    ✓ LIVE mode resolves against canvas /api proxy
    ✓ at least one /api call observed (count=1)

Deployment chain:
  shad/flowmaster-mission-control-demo @ dba1eb3
  → docker buildx on build01 (linux/amd64 native)
  → gitea.flow-master.ai/shad/mission-control-demo:sha-dba1eb3
  → FM06/flowmaster-ops PR #1164 (merged)
  → kubectl apply -n demo (ArgoCD was Degraded due to unrelated
    fm-shell env-var dupe; bypassed by applying directly)
  → 2/2 mc-mission-control pods Running
  → cluster ingress + cert-manager + Cloudflare A record
  → https://canvas.flow-master.ai serves the SPA + proxies /api to demo.flow-master.ai

Confidence: high
Scope-risk: narrow (demo namespace only; no shared baseline mutation)
Not-tested: long-term cert renewal cycle (will happen automatically;
    cert-manager letsencrypt-prod-dns issuer is already proven by hakeem)
This commit is contained in:
Shad 2026-06-14 02:46:34 +04:00
parent dba1eb3328
commit b2c1e57c86
5 changed files with 89 additions and 20 deletions

10
.dockerignore Normal file
View File

@ -0,0 +1,10 @@
node_modules
dist
qa/screenshots
.git
*.log
.DS_Store
.vscode
.idea
src/index.css.bak
*.bak*

View File

@ -1,8 +1,10 @@
# FlowMaster — Mission Control demo
# FlowMaster — Mission Control
A polished, presenter-ready command-center for FlowMaster. Lives at
[`shad/flowmaster-mission-control-demo`](https://gitea.flow-master.ai/shad/flowmaster-mission-control-demo)
on Gitea.
**Live at https://canvas.flow-master.ai** · [source on Gitea](https://gitea.flow-master.ai/shad/flowmaster-mission-control-demo)
The proper FlowMaster frontend: an operations cockpit for every process the
company runs. Industrial-blueprint doctrine — paper canvas, navy frame,
amber accent, 1px hairlines, square edges, monospace operational density.
## What this is (and isn't)
@ -77,7 +79,7 @@ to the backend.
`${VITE_FM_BASE:-https://demo.flow-master.ai}`.
- **Prod** (Docker image): the bundled `nginx.conf` reverse-proxies `/api/*`
to `https://demo.flow-master.ai`. The image is intended to sit behind the
`mc.flow-master.ai` ingress (see `FM06/flowmaster-ops` overlay).
`canvas.flow-master.ai` ingress (see `FM06/flowmaster-ops` overlay).
- **Anywhere else**: set `VITE_FM_BASE=https://your-backend` at build time
and accept that browsers will reject the cross-origin call. Live mode then
fails gracefully — `setMode("live")` catches the error, raises an
@ -135,24 +137,23 @@ fetch_scenarios.mjs # Node script to refresh src/scenarios.json
## Deploy
Production deployment is tracked in
[FM06/flowmaster-ops PR #1164](https://gitea.flow-master.ai/FM06/flowmaster-ops/pulls/1164),
which adds three resources to `manifests/overlays/demo/`:
[FM06/flowmaster-ops PR #1164](https://gitea.flow-master.ai/FM06/flowmaster-ops/pulls/1164)
(merged), which added three resources to `manifests/overlays/demo/`:
- `mc-deployment.yaml` — 2-replica nginx Deployment in the `demo` namespace
- `mc-service.yaml` — ClusterIP service on port 80
- `mc-ingress.yaml` — Traefik ingress at `mc.flow-master.ai` with cert-manager DNS-01 cert
- `mc-ingress.yaml` — Traefik ingress at `canvas.flow-master.ai` with cert-manager DNS-01 cert
**Status:** the PR is open and mergeable. The live URL `https://mc.flow-master.ai` is **not yet serving** — two pre-merge action items remain for the trusted updater:
1. Cloudflare A record `mc.flow-master.ai → 65.21.71.186, 91.98.159.56` (same as `hakeem.flow-master.ai`).
2. Gitea Actions must have published the first image tag (`gitea.flow-master.ai/shad/mission-control-demo:sha-…`) — `mc-deployment.yaml` pins that explicit SHA, not `:latest`.
Until the PR merges + DNS is added, the artifact is this repo + the open PR.
Cloudflare A record `canvas.flow-master.ai → 65.21.71.186` was added at the
same time. The cluster certifies with cert-manager (Let's Encrypt DNS-01)
and Traefik fronts the nginx pods. nginx reverse-proxies `/api/*` to
`https://demo.flow-master.ai` so the SPA is same-origin (no CORS).
```bash
pnpm build
# dist/ is the static site. The Dockerfile bakes it into a tiny nginx
# image. CI in .gitea/workflows/build.yml builds + publishes on push to main.
pnpm build # builds dist/
docker build -t gitea.flow-master.ai/shad/mission-control-demo:sha-<git> .
docker push gitea.flow-master.ai/shad/mission-control-demo:sha-<git>
# Then bump the image pin in manifests/overlays/demo/mc-deployment.yaml.
```
## What's intentionally not here

View File

@ -4,7 +4,9 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>fm-command-center-spike</title>
<title>FlowMaster · Mission Control</title>
<meta name="description" content="FlowMaster Mission Control — operations cockpit for the company's processes." />
<meta name="theme-color" content="#1a2740" />
</head>
<body>
<div id="root"></div>

53
qa/smoke_canvas.mjs Normal file
View File

@ -0,0 +1,53 @@
// Real-URL smoke against https://canvas.flow-master.ai.
// Runs Playwright with a forced DNS resolution so the local box's DNS cache
// doesn't shadow the just-published A record.
import { chromium } from "playwright";
const URL = "https://canvas.flow-master.ai/";
const FORCE_IP = "65.21.71.186";
function ok(label, cond, detail = "") {
console.log(`${cond ? "✓" : "✗"} ${label}${detail ? " · " + detail : ""}`);
if (!cond) process.exitCode = 1;
}
const b = await chromium.launch({
headless: true,
args: [
`--host-resolver-rules=MAP canvas.flow-master.ai ${FORCE_IP}`,
"--ignore-certificate-errors",
],
});
const p = await b.newPage({ viewport: { width: 1440, height: 900 } });
const errors = [];
p.on("pageerror", (e) => errors.push(`pageerror ${e.message}`));
p.on("console", (m) => { if (m.type() === "error") errors.push(`console.error ${m.text().slice(0, 200)}`); });
await p.goto(URL, { waitUntil: "networkidle" });
ok("landing renders at canvas.flow-master.ai", await p.locator(".landing").isVisible());
ok("hero title present", await p.locator(".hero-title").isVisible());
const cards = await p.locator(".sc-card").count();
ok("scenario cards >= 7", cards >= 7, `count=${cards}`);
await p.locator(".sc-card").first().click();
await p.waitForSelector(".mc"); await p.waitForTimeout(600);
ok("mission control loaded", await p.locator(".mc-strip").isVisible());
ok("blueprint canvas mounted", await p.locator(".bp-frame").isVisible());
const nodes = await p.locator(".bp-node").count();
ok("blueprint nodes rendered", nodes >= 3, `nodes=${nodes}`);
// Live mode: real /api hit through nginx proxy at canvas
const networkCalls = [];
p.on("request", (req) => { if (req.url().includes("/api/")) networkCalls.push(req.url()); });
await p.locator(".mode-toggle").first().click();
const liveOk = await p.waitForFunction(
() => Array.from(document.querySelectorAll(".mode-pill")).some((el) => el.textContent?.trim() === "LIVE"),
{ timeout: 15000 },
).then(() => true).catch(() => false);
ok("LIVE mode resolves against canvas /api proxy", liveOk);
ok("at least one /api call observed", networkCalls.length > 0, `count=${networkCalls.length}`);
await p.screenshot({ path: "qa/screenshots/canvas-live.png" });
console.log(`\nconsole errors: ${errors.length}`);
errors.slice(0, 5).forEach((e) => console.log(" -", e));
await b.close();

View File

@ -151,8 +151,11 @@ export default function Settings() {
<section className="studio-panel">
<h3 className="panel-h"><Layers size={12} /> About</h3>
<p className="settings-text">
Mission Control demo. All actions are real backend calls the live
console shows every fetch. Source:{" "}
FlowMaster Mission Control. Live at{" "}
<a className="settings-link" href="https://canvas.flow-master.ai" target="_blank" rel="noreferrer">
canvas.flow-master.ai
</a>. All actions hit the real EA2 backend the live console shows every
fetch. Source:{" "}
<a className="settings-link" href="https://gitea.flow-master.ai/shad/flowmaster-mission-control-demo" target="_blank" rel="noreferrer">
gitea.flow-master.ai/shad/flowmaster-mission-control-demo
</a>.