feat: add L6 CLI web MVP gate entry
This commit is contained in:
@@ -67,3 +67,21 @@ Run the lightweight validation suite:
|
||||
```sh
|
||||
npm run check
|
||||
```
|
||||
|
||||
## L6 Manual Acceptance Entry
|
||||
|
||||
The external MVP acceptance surface is DEV-only and report-driven:
|
||||
|
||||
```sh
|
||||
npm run l6:smoke
|
||||
npm run web:check
|
||||
npm run cli:dry-run
|
||||
node tools/hwlab-cli/bin/hwlab-cli.mjs test e2e --env dev --mvp
|
||||
```
|
||||
|
||||
The CLI command defaults to a JSON dry-run plan sourced from
|
||||
`reports/dev-gate/dev-mvp-gate-report.json` and
|
||||
`fixtures/mvp/m5-e2e/dry-run-plan.json`. A live DEV attempt requires
|
||||
`--live --confirm-dev --confirmed-non-production`; with open gate blockers it
|
||||
returns `BLOCKED` evidence rather than using fixtures or UniDesk runtime as a
|
||||
substitute.
|
||||
|
||||
+21
-4
@@ -2,7 +2,7 @@
|
||||
|
||||
M5 is the static orchestration gate before the first real DEV MVP e2e run. It
|
||||
does not deploy, does not call DEV, does not run browser e2e, and does not read
|
||||
secrets. The only executable gate in this milestone is:
|
||||
secrets. The source contract gate is:
|
||||
|
||||
```sh
|
||||
node scripts/m5-mvp-e2e-dry-run.mjs
|
||||
@@ -13,6 +13,19 @@ The script reads the frozen protocol contracts, the DEV acceptance checklist,
|
||||
validates the dependency graph and checks that each step has resolvable inputs
|
||||
and outputs.
|
||||
|
||||
The L6 external acceptance CLI wraps the same report and dry-run plan:
|
||||
|
||||
```sh
|
||||
node tools/hwlab-cli/bin/hwlab-cli.mjs test e2e --env dev --mvp
|
||||
```
|
||||
|
||||
This command defaults to a JSON dry-run plan. `--dry-run` is accepted for
|
||||
backward compatibility, but it is no longer required for safety. The output
|
||||
must identify the M0-M5 report source, artifact and DEV health contract counts,
|
||||
two gateway sessions, two box resources, patch-panel status, agent and worker
|
||||
session IDs, evidence records, open blockers, and the statement that no
|
||||
DEV/PROD changes were made.
|
||||
|
||||
## Dry-Run Flow
|
||||
|
||||
| Order | Step | Purpose | Required Inputs | Required Outputs |
|
||||
@@ -79,10 +92,14 @@ Do not switch to the real command until all of these are true:
|
||||
8. No blocker class is open and a human has explicitly approved a real DEV
|
||||
smoke.
|
||||
|
||||
Only after those gates are satisfied should the runner switch from dry-run to:
|
||||
Only after those gates are satisfied should the runner switch from dry-run to
|
||||
the explicit live DEV command:
|
||||
|
||||
```sh
|
||||
hwlab-cli test e2e --env dev --mvp
|
||||
hwlab-cli test e2e --env dev --mvp --live --confirm-dev --confirmed-non-production
|
||||
```
|
||||
|
||||
That command is intentionally not executed by M5.
|
||||
That command is intentionally not executed by M5. While
|
||||
`reports/dev-gate/dev-mvp-gate-report.json` contains open blockers, the CLI
|
||||
must return `code: "BLOCKED"` with blocker evidence instead of substituting
|
||||
fixture output as live DEV evidence.
|
||||
|
||||
@@ -89,6 +89,39 @@ reason.
|
||||
| M4 agent automation loop | Source-only or dry-run, then MVP e2e | Agent manager, worker, skills, session lifecycle, scoped workspace, trace events, cleanup, and evidence chain are observable in fixtures or dry-run before any DEV runtime action. |
|
||||
| M5 MVP e2e dry-run and acceptance | Dry-run, then MVP e2e | `npm run cli:dry-run` or equivalent names the MVP route, closed loops, evidence, and cleanup without mutation. Real acceptance is DEV-only, bounded, and requires prior DEV runtime smoke pass. |
|
||||
|
||||
## L6 CLI/Web Acceptance Entry
|
||||
|
||||
Use the L6 entry points for external manual acceptance triage. They read the
|
||||
checked-in `reports/dev-gate/dev-mvp-gate-report.json` and
|
||||
`fixtures/mvp/m5-e2e/dry-run-plan.json`; they do not call UniDesk runtime and
|
||||
do not claim live DEV evidence from fixtures.
|
||||
|
||||
Safe source and dry-run checks:
|
||||
|
||||
```sh
|
||||
npm run l6:smoke
|
||||
npm run web:check
|
||||
npm run cli:dry-run
|
||||
node tools/hwlab-cli/bin/hwlab-cli.mjs test e2e --env dev --mvp
|
||||
```
|
||||
|
||||
The default CLI MVP command is a JSON dry-run plan even when `--dry-run` is not
|
||||
provided. It must include the DEV endpoint, artifact/health counts, two
|
||||
gateway sessions, two box resources, the patch-panel status, agent and worker
|
||||
session IDs, evidence records, and a statement that no DEV/PROD changes were
|
||||
made.
|
||||
|
||||
The live DEV entry point is intentionally separate:
|
||||
|
||||
```sh
|
||||
node tools/hwlab-cli/bin/hwlab-cli.mjs test e2e --env dev --mvp --live --confirm-dev --confirmed-non-production
|
||||
```
|
||||
|
||||
When the M0-M5 gate report still has open blockers, the live command must stop
|
||||
with `code: "BLOCKED"` and include the blocker evidence. It must not use
|
||||
fixture data, UniDesk services, PROD, heavy e2e, restarts, or secret reads to
|
||||
produce a green result.
|
||||
|
||||
## Phase Procedures
|
||||
|
||||
### Source-Only Procedure
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
export const DEV_ENDPOINT = "http://74.48.78.17:6667";
|
||||
export const DEFAULT_GATE_REPORT_PATH = "reports/dev-gate/dev-mvp-gate-report.json";
|
||||
export const DEFAULT_M5_PLAN_PATH = "fixtures/mvp/m5-e2e/dry-run-plan.json";
|
||||
|
||||
function readJsonFile(repoRoot, relativePath) {
|
||||
const absolutePath = path.resolve(repoRoot, relativePath);
|
||||
const raw = fs.readFileSync(absolutePath, "utf8");
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
|
||||
function countByServiceId(items) {
|
||||
return new Map(items.map((item) => [item.serviceId, item]));
|
||||
}
|
||||
|
||||
function requireArray(value, label) {
|
||||
if (!Array.isArray(value)) {
|
||||
throw new Error(`${label} must be an array`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function assertRequiredMvpGate(report, plan) {
|
||||
if (report.devOnly !== true || report.prodDisabled !== true) {
|
||||
throw new Error("MVP gate report must be DEV-only and PROD-disabled");
|
||||
}
|
||||
if (report.gateStatus !== "blocked") {
|
||||
throw new Error("MVP gate report must not claim live DEV pass in this MVP fixture");
|
||||
}
|
||||
if (plan.mode !== "dry-run" || plan.environment !== "dev" || plan.endpoint !== DEV_ENDPOINT) {
|
||||
throw new Error("M5 plan must stay dry-run, dev, and pinned to the frozen DEV endpoint");
|
||||
}
|
||||
|
||||
const milestones = requireArray(report.milestones, "report.milestones");
|
||||
for (const milestone of ["M0", "M1", "M2", "M3", "M4", "M5"]) {
|
||||
if (!milestones.some((item) => item.id === milestone)) {
|
||||
throw new Error(`MVP gate report missing milestone ${milestone}`);
|
||||
}
|
||||
}
|
||||
|
||||
const artifacts = requireArray(plan.artifacts, "plan.artifacts");
|
||||
const health = requireArray(plan.health, "plan.health");
|
||||
const gatewaySessions = requireArray(plan.gatewaySessions, "plan.gatewaySessions");
|
||||
const boxResources = requireArray(plan.boxResources, "plan.boxResources");
|
||||
const evidenceRecords = requireArray(plan.evidenceRecords, "plan.evidenceRecords");
|
||||
const operations = requireArray(plan.operations, "plan.operations");
|
||||
|
||||
const byService = countByServiceId(artifacts);
|
||||
for (const serviceId of [
|
||||
"hwlab-cloud-web",
|
||||
"hwlab-cloud-api",
|
||||
"hwlab-gateway",
|
||||
"hwlab-gateway-simu",
|
||||
"hwlab-box-simu",
|
||||
"hwlab-patch-panel",
|
||||
"hwlab-agent-mgr",
|
||||
"hwlab-agent-worker",
|
||||
"hwlab-cli"
|
||||
]) {
|
||||
if (!byService.has(serviceId)) {
|
||||
throw new Error(`M5 plan missing artifact for ${serviceId}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (gatewaySessions.length < 2) {
|
||||
throw new Error("M5 plan must include two gateway sessions");
|
||||
}
|
||||
if (boxResources.length < 2) {
|
||||
throw new Error("M5 plan must include two box resources");
|
||||
}
|
||||
if (!plan.patchPanelStatus?.patchPanelStatusId) {
|
||||
throw new Error("M5 plan missing patch-panel status evidence");
|
||||
}
|
||||
if (!plan.agentSession?.agentSessionId || !plan.workerSession?.workerSessionId) {
|
||||
throw new Error("M5 plan missing agent and worker session evidence");
|
||||
}
|
||||
if (evidenceRecords.length < 2 || operations.length < 2) {
|
||||
throw new Error("M5 plan must include direct and agent evidence records");
|
||||
}
|
||||
}
|
||||
|
||||
function summarizeArtifact(artifact) {
|
||||
return {
|
||||
serviceId: artifact.serviceId,
|
||||
commitId: artifact.commitId,
|
||||
image: artifact.image,
|
||||
tag: artifact.tag,
|
||||
digest: artifact.digest,
|
||||
digestNotApplicableReason: artifact.digestNotApplicableReason ?? null,
|
||||
buildSource: artifact.buildSource,
|
||||
deployEnv: artifact.deployEnv,
|
||||
healthTimestamp: artifact.healthTimestamp
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeHealth(health) {
|
||||
return {
|
||||
healthId: health.healthId,
|
||||
serviceId: health.serviceId,
|
||||
component: health.component,
|
||||
status: health.status,
|
||||
deployEnv: health.deployEnv,
|
||||
endpoint: health.endpoint ?? null,
|
||||
observedBy: health.observedBy,
|
||||
dryRun: health.dryRun === true,
|
||||
healthTimestamp: health.healthTimestamp
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeMilestone(milestone) {
|
||||
return {
|
||||
id: milestone.id,
|
||||
status: milestone.status,
|
||||
commandCount: milestone.commands?.length ?? 0,
|
||||
evidenceCount: milestone.evidence?.length ?? 0,
|
||||
summary: milestone.summary
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeBlocker(blocker) {
|
||||
return {
|
||||
type: blocker.type,
|
||||
scope: blocker.scope,
|
||||
status: blocker.status,
|
||||
summary: blocker.summary
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeStep(step) {
|
||||
return {
|
||||
id: step.id,
|
||||
order: step.order,
|
||||
title: step.title,
|
||||
kind: step.kind,
|
||||
requires: step.requires,
|
||||
inputs: step.inputs,
|
||||
outputs: step.outputs,
|
||||
acceptanceStepIds: step.acceptanceStepIds
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeGatewaySession(session) {
|
||||
return {
|
||||
gatewaySessionId: session.gatewaySessionId,
|
||||
serviceId: session.serviceId,
|
||||
gatewayId: session.gatewayId,
|
||||
status: session.status,
|
||||
environment: session.environment,
|
||||
endpoint: session.endpoint ?? null
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeBoxResource(resource) {
|
||||
return {
|
||||
resourceId: resource.resourceId,
|
||||
gatewaySessionId: resource.gatewaySessionId,
|
||||
boxId: resource.boxId,
|
||||
resourceType: resource.resourceType,
|
||||
name: resource.name,
|
||||
state: resource.state,
|
||||
environment: resource.environment
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeEvidence(record) {
|
||||
return {
|
||||
evidenceId: record.evidenceId,
|
||||
operationId: record.operationId,
|
||||
agentSessionId: record.agentSessionId ?? null,
|
||||
workerSessionId: record.workerSessionId ?? null,
|
||||
kind: record.kind,
|
||||
uri: record.uri,
|
||||
sha256: record.sha256,
|
||||
serviceId: record.serviceId,
|
||||
environment: record.environment,
|
||||
dryRun: record.metadata?.dryRun === true
|
||||
};
|
||||
}
|
||||
|
||||
export function buildMvpGateSummary({ report, plan }) {
|
||||
assertRequiredMvpGate(report, plan);
|
||||
|
||||
const artifacts = plan.artifacts.map(summarizeArtifact);
|
||||
const health = plan.health.map(summarizeHealth);
|
||||
const milestones = report.milestones.map(summarizeMilestone);
|
||||
const blockers = report.blockers.map(summarizeBlocker);
|
||||
const steps = plan.steps.map(summarizeStep).sort((a, b) => a.order - b.order);
|
||||
const blocked = blockers.some((blocker) => blocker.status === "open") || report.gateStatus === "blocked";
|
||||
|
||||
return {
|
||||
generatedFrom: {
|
||||
gateReport: DEFAULT_GATE_REPORT_PATH,
|
||||
m5Plan: DEFAULT_M5_PLAN_PATH
|
||||
},
|
||||
issue: "pikasTech/HWLAB#56",
|
||||
supports: ["pikasTech/HWLAB#7", "pikasTech/HWLAB#9"],
|
||||
gateStatus: report.gateStatus,
|
||||
blocked,
|
||||
acceptanceLevel: report.acceptanceLevel,
|
||||
reportCommitId: report.commitId,
|
||||
dryRunOnly: true,
|
||||
devOnly: report.devOnly === true,
|
||||
prodDisabled: report.prodDisabled === true,
|
||||
environment: plan.environment,
|
||||
endpoint: plan.endpoint,
|
||||
namespace: "hwlab-dev",
|
||||
sourceSummary: report.sourceContract.summary,
|
||||
localSmoke: report.localSmoke,
|
||||
dryRun: report.dryRun,
|
||||
devPreconditions: report.devPreconditions,
|
||||
blockers,
|
||||
milestones,
|
||||
artifactCount: artifacts.length,
|
||||
healthCount: health.length,
|
||||
gatewaySessionCount: plan.gatewaySessions.length,
|
||||
boxResourceCount: plan.boxResources.length,
|
||||
patchPanelCount: 1,
|
||||
operationCount: plan.operations.length,
|
||||
evidenceCount: plan.evidenceRecords.length,
|
||||
traceCount: plan.traceEvents.length,
|
||||
auditCount: plan.auditEvents.length,
|
||||
cleanupCount: plan.cleanup.length,
|
||||
artifacts,
|
||||
health,
|
||||
topology: {
|
||||
projectId: plan.project.projectId,
|
||||
projectStatus: plan.project.status,
|
||||
gateways: plan.gatewaySessions.map(summarizeGatewaySession),
|
||||
boxResources: plan.boxResources.map(summarizeBoxResource),
|
||||
patchPanel: {
|
||||
patchPanelStatusId: plan.patchPanelStatus.patchPanelStatusId,
|
||||
serviceId: plan.patchPanelStatus.serviceId,
|
||||
state: plan.patchPanelStatus.state,
|
||||
environment: plan.patchPanelStatus.environment,
|
||||
activeConnectionCount: plan.patchPanelStatus.activeConnections.length,
|
||||
activeConnections: plan.patchPanelStatus.activeConnections
|
||||
}
|
||||
},
|
||||
agent: {
|
||||
agentSessionId: plan.agentSession.agentSessionId,
|
||||
agentServiceId: plan.agentSession.serviceId,
|
||||
agentStatus: plan.agentSession.status,
|
||||
workerSessionId: plan.workerSession.workerSessionId,
|
||||
workerServiceId: plan.workerSession.serviceId,
|
||||
workerStatus: plan.workerSession.status,
|
||||
projectId: plan.agentSession.projectId,
|
||||
environment: plan.agentSession.environment,
|
||||
cleanupId: plan.agentSession.metadata.cleanupId
|
||||
},
|
||||
operations: plan.operations.map((operation) => ({
|
||||
operationId: operation.operationId,
|
||||
requestedBy: operation.requestedBy,
|
||||
status: operation.status,
|
||||
resourceId: operation.resourceId,
|
||||
capabilityId: operation.capabilityId,
|
||||
agentSessionId: operation.agentSessionId ?? null,
|
||||
workerSessionId: operation.workerSessionId ?? null,
|
||||
environment: operation.environment
|
||||
})),
|
||||
evidenceRecords: plan.evidenceRecords.map(summarizeEvidence),
|
||||
steps,
|
||||
realDevGate: {
|
||||
sourcePlanCommand: plan.realDevGate.command,
|
||||
command: `${plan.realDevGate.command} --live --confirm-dev --confirmed-non-production`,
|
||||
requiresAllBlockersClear: plan.realDevGate.requiresAllBlockersClear,
|
||||
requiresDryRunPass: plan.realDevGate.requiresDryRunPass,
|
||||
requiresArtifactObservability: plan.realDevGate.requiresArtifactObservability,
|
||||
requiresD601RouteObservation: plan.realDevGate.requiresD601RouteObservation,
|
||||
requiresHumanApprovalForRealDev: plan.realDevGate.requiresHumanApprovalForRealDev
|
||||
},
|
||||
safety: {
|
||||
mode: plan.mode,
|
||||
allowNetwork: plan.safety.allowNetwork,
|
||||
allowDeploy: plan.safety.allowDeploy,
|
||||
allowBrowserE2E: plan.safety.allowBrowserE2E,
|
||||
allowSecrets: plan.safety.allowSecrets,
|
||||
prohibitedActions: plan.safety.prohibitedActions
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function loadMvpGateSummary(repoRoot, options = {}) {
|
||||
const reportPath = options.reportPath ?? DEFAULT_GATE_REPORT_PATH;
|
||||
const planPath = options.planPath ?? DEFAULT_M5_PLAN_PATH;
|
||||
const report = readJsonFile(repoRoot, reportPath);
|
||||
const plan = readJsonFile(repoRoot, planPath);
|
||||
const summary = buildMvpGateSummary({ report, plan });
|
||||
summary.generatedFrom = {
|
||||
gateReport: reportPath,
|
||||
m5Plan: planPath
|
||||
};
|
||||
return summary;
|
||||
}
|
||||
+4
-2
@@ -5,15 +5,17 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"validate": "node scripts/validate-contract.mjs",
|
||||
"check": "node --check internal/protocol/index.mjs && node --check internal/agent/index.mjs && node --check internal/agent/runtime.mjs && node --check internal/cloud/db-contract.mjs && node --check internal/cloud/json-rpc.mjs && node --check internal/cloud/server.mjs && node --check cmd/hwlab-cloud-api/main.mjs && node --check cmd/hwlab-agent-mgr/main.mjs && node --check cmd/hwlab-agent-worker/main.mjs && node --check scripts/dev-edge-health-smoke.mjs && node --check scripts/src/dev-edge-health-smoke-lib.mjs && node --check scripts/validate-contract.mjs && node --check scripts/validate-dev-m3-cardinality.mjs && node --check scripts/validate-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.mjs && node --check scripts/dev-artifact-publish.mjs && node --check scripts/preflight-dev-base-image.mjs && node --check scripts/src/dev-base-image-preflight.mjs && node --check scripts/dev-evidence-blocker-aggregator.mjs && node --check scripts/d601-k3s-readonly-observability.mjs && node --check scripts/src/d601-k3s-readonly-observability.mjs && node scripts/validate-contract.mjs && node scripts/validate-dev-m3-cardinality.mjs && node scripts/validate-artifact-catalog.mjs && node scripts/dev-evidence-blocker-aggregator.mjs --check && node --test internal/agent/index.test.mjs internal/cloud/json-rpc.test.mjs internal/cloud/server.test.mjs && sh -n scripts/bootstrap-skills.sh scripts/worker-entrypoint.sh",
|
||||
"check": "node --check internal/protocol/index.mjs && node --check internal/agent/index.mjs && node --check internal/agent/runtime.mjs && node --check internal/mvp-gate/summary.mjs && node --check internal/cloud/db-contract.mjs && node --check internal/cloud/json-rpc.mjs && node --check internal/cloud/server.mjs && node --check cmd/hwlab-cloud-api/main.mjs && node --check cmd/hwlab-agent-mgr/main.mjs && node --check cmd/hwlab-agent-worker/main.mjs && node --check scripts/dev-edge-health-smoke.mjs && node --check scripts/src/dev-edge-health-smoke-lib.mjs && node --check scripts/validate-contract.mjs && node --check scripts/validate-dev-m3-cardinality.mjs && node --check scripts/validate-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.mjs && node --check scripts/dev-artifact-publish.mjs && node --check scripts/preflight-dev-base-image.mjs && node --check scripts/src/dev-base-image-preflight.mjs && node --check scripts/dev-evidence-blocker-aggregator.mjs && node --check scripts/d601-k3s-readonly-observability.mjs && node --check scripts/src/d601-k3s-readonly-observability.mjs && node --check scripts/export-web-gate-summary.mjs && node --check scripts/l6-cli-web-smoke.mjs && node --check tools/hwlab-cli/bin/hwlab-cli.mjs && node --check tools/hwlab-cli/lib/cli.mjs && node --check web/hwlab-cloud-web/app.mjs && node --check web/hwlab-cloud-web/gate-summary.mjs && node --check web/hwlab-cloud-web/scripts/check.mjs && node --check web/hwlab-cloud-web/scripts/build.mjs && node scripts/validate-contract.mjs && node scripts/validate-dev-m3-cardinality.mjs && node scripts/validate-artifact-catalog.mjs && node scripts/dev-evidence-blocker-aggregator.mjs --check && node scripts/l6-cli-web-smoke.mjs && node web/hwlab-cloud-web/scripts/check.mjs && node --test internal/agent/index.test.mjs internal/cloud/json-rpc.test.mjs internal/cloud/server.test.mjs && sh -n scripts/bootstrap-skills.sh scripts/worker-entrypoint.sh",
|
||||
"dev-base-image:preflight": "node scripts/preflight-dev-base-image.mjs",
|
||||
"m1:smoke": "node scripts/m1-contract-smoke.mjs",
|
||||
"dev:evidence": "node scripts/dev-evidence-blocker-aggregator.mjs --pretty",
|
||||
"dev:evidence:check": "node scripts/dev-evidence-blocker-aggregator.mjs --check",
|
||||
"dev-edge:smoke": "node scripts/dev-edge-health-smoke.mjs",
|
||||
"cli:health": "node tools/hwlab-cli/bin/hwlab-cli.mjs health",
|
||||
"cli:dry-run": "node tools/hwlab-cli/bin/hwlab-cli.mjs test e2e --env dev --mvp --dry-run",
|
||||
"cli:dry-run": "node tools/hwlab-cli/bin/hwlab-cli.mjs test e2e --env dev --mvp",
|
||||
"cli:projects": "node tools/hwlab-cli/bin/hwlab-cli.mjs project list",
|
||||
"l6:smoke": "node scripts/l6-cli-web-smoke.mjs",
|
||||
"web:summary": "node scripts/export-web-gate-summary.mjs",
|
||||
"web:check": "node web/hwlab-cloud-web/scripts/check.mjs",
|
||||
"web:build": "node web/hwlab-cloud-web/scripts/build.mjs",
|
||||
"artifact-catalog:refresh-blocked": "node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --blocked",
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { loadMvpGateSummary } from "../internal/mvp-gate/summary.mjs";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const outputPath = path.join(repoRoot, "web/hwlab-cloud-web/gate-summary.mjs");
|
||||
const summary = loadMvpGateSummary(repoRoot);
|
||||
const body = `// Generated from reports/dev-gate/dev-mvp-gate-report.json and fixtures/mvp/m5-e2e/dry-run-plan.json.
|
||||
// Run node scripts/export-web-gate-summary.mjs after changing those fixtures.
|
||||
export const gateSummary = ${JSON.stringify(summary, null, 2)};
|
||||
`;
|
||||
|
||||
fs.writeFileSync(outputPath, body);
|
||||
process.stdout.write(`wrote ${path.relative(repoRoot, outputPath)}\n`);
|
||||
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env node
|
||||
import assert from "node:assert/strict";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { loadMvpGateSummary } from "../internal/mvp-gate/summary.mjs";
|
||||
import { runCli } from "../tools/hwlab-cli/lib/cli.mjs";
|
||||
import { gateSummary } from "../web/hwlab-cloud-web/gate-summary.mjs";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
|
||||
async function captureCli(args) {
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
const exitCode = await runCli(args, {
|
||||
cwd: repoRoot,
|
||||
stdout: {
|
||||
write(chunk) {
|
||||
stdout += chunk;
|
||||
}
|
||||
},
|
||||
stderr: {
|
||||
write(chunk) {
|
||||
stderr += chunk;
|
||||
}
|
||||
}
|
||||
});
|
||||
return { exitCode, stdout, stderr };
|
||||
}
|
||||
|
||||
const summary = loadMvpGateSummary(repoRoot);
|
||||
assert.equal(summary.endpoint, "http://74.48.78.17:6667");
|
||||
assert.deepEqual(
|
||||
summary.milestones.map((item) => item.id),
|
||||
["M0", "M1", "M2", "M3", "M4", "M5"]
|
||||
);
|
||||
|
||||
const dryRun = await captureCli(["test", "e2e", "--env", "dev", "--mvp"]);
|
||||
assert.equal(dryRun.exitCode, 0, dryRun.stderr);
|
||||
const dryRunJson = JSON.parse(dryRun.stdout);
|
||||
assert.equal(dryRunJson.mode, "dry-run");
|
||||
assert.equal(dryRunJson.environment, "dev");
|
||||
assert.equal(dryRunJson.endpoint, summary.endpoint);
|
||||
assert.equal(dryRunJson.contractStatus.artifacts.count, 13);
|
||||
assert.equal(dryRunJson.contractStatus.health.count, 14);
|
||||
assert.equal(dryRunJson.contractStatus.topology.gatewaySessions, 2);
|
||||
assert.equal(dryRunJson.contractStatus.topology.boxResources, 2);
|
||||
assert.equal(dryRunJson.contractStatus.topology.patchPanelStatusId, "pps_m5-0001");
|
||||
assert.equal(dryRunJson.contractStatus.agent.agentSessionId, "agt_m5-0001");
|
||||
assert.equal(dryRunJson.contractStatus.evidence.records.length, 2);
|
||||
assert.match(dryRunJson.statement, /no DEV\/PROD changes/);
|
||||
|
||||
const explicitDryRun = await captureCli(["test", "e2e", "--env", "dev", "--mvp", "--dry-run"]);
|
||||
assert.equal(explicitDryRun.exitCode, 0, explicitDryRun.stderr);
|
||||
assert.equal(JSON.parse(explicitDryRun.stdout).mode, "dry-run");
|
||||
|
||||
const liveBlocked = await captureCli([
|
||||
"test",
|
||||
"e2e",
|
||||
"--env",
|
||||
"dev",
|
||||
"--mvp",
|
||||
"--live",
|
||||
"--confirm-dev",
|
||||
"--confirmed-non-production"
|
||||
]);
|
||||
assert.equal(liveBlocked.exitCode, 2);
|
||||
const liveBlockedJson = JSON.parse(liveBlocked.stderr);
|
||||
assert.equal(liveBlockedJson.code, "BLOCKED");
|
||||
assert.equal(liveBlockedJson.blockers[0].type, "network_blocker");
|
||||
|
||||
assert.equal(gateSummary.endpoint, summary.endpoint);
|
||||
assert.equal(gateSummary.gateStatus, "blocked");
|
||||
assert.equal(gateSummary.artifactCount, 13);
|
||||
assert.equal(gateSummary.healthCount, 14);
|
||||
assert.equal(gateSummary.gatewaySessionCount, 2);
|
||||
assert.equal(gateSummary.boxResourceCount, 2);
|
||||
assert.equal(gateSummary.topology.patchPanel.serviceId, "hwlab-patch-panel");
|
||||
assert.equal(gateSummary.agent.agentServiceId, "hwlab-agent-mgr");
|
||||
assert.equal(gateSummary.agent.workerServiceId, "hwlab-agent-worker");
|
||||
assert.equal(gateSummary.evidenceRecords.length, 2);
|
||||
|
||||
console.log("L6 CLI/Web smoke passed: CLI and Web read the M0-M5 gate report");
|
||||
@@ -310,11 +310,19 @@ async function smokeCliDryRunFixture() {
|
||||
});
|
||||
|
||||
assert.equal(exitCode, 0, stderr);
|
||||
assert.match(stdout, /hwlab-cli test e2e --env dev --mvp --dry-run/);
|
||||
assert.match(stdout, /cloud-web\/cloud-api/);
|
||||
assert.match(stdout, /hardware trusted closed loop/);
|
||||
assert.match(stdout, /evidence_mvp-runtime-plan/);
|
||||
assert.match(stdout, /dry-run only: no DEV\/PROD changes were made/);
|
||||
const output = JSON.parse(stdout);
|
||||
assert.equal(output.command, "hwlab-cli test e2e --env dev --mvp --dry-run");
|
||||
assert.equal(output.mode, "dry-run");
|
||||
assert.equal(output.environment, "dev");
|
||||
assert.equal(output.endpoint, DEV_ENDPOINT);
|
||||
assert.equal(output.safety.allowNetwork, false);
|
||||
assert.equal(output.contractStatus.artifacts.services.includes("hwlab-cloud-web"), true);
|
||||
assert.equal(output.contractStatus.artifacts.services.includes("hwlab-cloud-api"), true);
|
||||
assert.equal(output.contractStatus.topology.gatewaySessions, 2);
|
||||
assert.equal(output.contractStatus.topology.boxResources, 2);
|
||||
assert.equal(output.contractStatus.agent.agentSessionId, "agt_m5-0001");
|
||||
assert.equal(output.contractStatus.evidence.records.length, 2);
|
||||
assert.match(output.statement, /no DEV\/PROD changes/);
|
||||
|
||||
logOk("CLI dry-run fixture parse");
|
||||
}
|
||||
|
||||
+199
-122
@@ -1,60 +1,4 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { DEV_ENDPOINT } from "../../../internal/protocol/index.mjs";
|
||||
|
||||
const DEFAULT_PROJECTS = [
|
||||
{
|
||||
projectId: "proj_mvp-l6",
|
||||
name: "HWLAB L6 MVP",
|
||||
status: "active",
|
||||
environment: "dev",
|
||||
updatedAt: "2026-05-21T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
projectId: "proj_cloud-web",
|
||||
name: "Cloud Web Skeleton",
|
||||
status: "active",
|
||||
environment: "dev",
|
||||
updatedAt: "2026-05-20T12:00:00.000Z"
|
||||
}
|
||||
];
|
||||
|
||||
function loadRuntimeFixture(cwd) {
|
||||
const fixturePath = path.resolve(cwd, "fixtures/mvp/runtime.json");
|
||||
if (!fs.existsSync(fixturePath)) {
|
||||
return null;
|
||||
}
|
||||
const raw = fs.readFileSync(fixturePath, "utf8");
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
|
||||
function resolveRuntime(cwd) {
|
||||
const fixture = loadRuntimeFixture(cwd);
|
||||
if (fixture) {
|
||||
return fixture;
|
||||
}
|
||||
return {
|
||||
endpoints: { dev: DEV_ENDPOINT },
|
||||
projects: DEFAULT_PROJECTS,
|
||||
mvpSteps: [
|
||||
"browser/CLI/gateway",
|
||||
"master hwlab-edge-proxy",
|
||||
"frp",
|
||||
"D601 hwlab-dev/hwlab-router",
|
||||
"cloud-web/cloud-api",
|
||||
"hardware trusted closed loop",
|
||||
"agent automation closed loop",
|
||||
"evidence record",
|
||||
"worker cleanup"
|
||||
],
|
||||
loops: [
|
||||
{ name: "hardware trusted closed loop" },
|
||||
{ name: "agent automation closed loop" }
|
||||
],
|
||||
evidence: [],
|
||||
cleanup: []
|
||||
};
|
||||
}
|
||||
import { DEV_ENDPOINT, loadMvpGateSummary } from "../../../internal/mvp-gate/summary.mjs";
|
||||
|
||||
function formatJson(value) {
|
||||
return JSON.stringify(value, null, 2);
|
||||
@@ -64,60 +8,8 @@ function writeLine(stream, line = "") {
|
||||
stream.write(`${line}\n`);
|
||||
}
|
||||
|
||||
function writeProjectList(runtime, stdout) {
|
||||
writeLine(stdout, "hwlab-cli project list");
|
||||
writeLine(stdout, `endpoint: ${runtime.endpoints?.dev ?? DEV_ENDPOINT}`);
|
||||
for (const project of runtime.projects ?? []) {
|
||||
writeLine(
|
||||
stdout,
|
||||
`${project.projectId} | ${project.name} | ${project.status} | ${project.environment} | ${project.updatedAt}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function writeHealth(runtime, stdout) {
|
||||
writeLine(stdout, "hwlab-cli health");
|
||||
writeLine(stdout, "status: ok");
|
||||
writeLine(stdout, `environment: dev`);
|
||||
writeLine(stdout, `endpoint: ${runtime.endpoints?.dev ?? DEV_ENDPOINT}`);
|
||||
writeLine(stdout, `projects: ${(runtime.projects ?? []).length}`);
|
||||
}
|
||||
|
||||
function writeDryRun(runtime, stdout) {
|
||||
writeLine(stdout, "hwlab-cli test e2e --env dev --mvp --dry-run");
|
||||
writeLine(stdout, `endpoint: ${runtime.endpoints?.dev ?? DEV_ENDPOINT}`);
|
||||
writeLine(stdout, "mvp route:");
|
||||
writeLine(
|
||||
stdout,
|
||||
` ${runtime.serviceRoute?.join(" -> ") ?? "browser/CLI/gateway -> master hwlab-edge-proxy -> frp -> D601 hwlab-dev/hwlab-router -> cloud-web/cloud-api"}`
|
||||
);
|
||||
for (const step of runtime.mvpSteps ?? []) {
|
||||
writeLine(stdout, `- ${step}`);
|
||||
}
|
||||
writeLine(stdout, "closed loops:");
|
||||
for (const loop of runtime.loops ?? []) {
|
||||
writeLine(stdout, `- ${loop.name}${loop.description ? `: ${loop.description}` : ""}`);
|
||||
}
|
||||
writeLine(stdout, "evidence record:");
|
||||
for (const evidence of runtime.evidence ?? []) {
|
||||
writeLine(
|
||||
stdout,
|
||||
`- ${evidence.evidenceId} | ${evidence.kind} | ${evidence.serviceId} | ${evidence.uri}`
|
||||
);
|
||||
}
|
||||
writeLine(stdout, "worker cleanup:");
|
||||
for (const step of runtime.cleanup ?? []) {
|
||||
writeLine(stdout, `- ${step}`);
|
||||
}
|
||||
writeLine(stdout, "dry-run only: no DEV/PROD changes were made");
|
||||
}
|
||||
|
||||
function writeHelp(stdout) {
|
||||
writeLine(stdout, "hwlab-cli");
|
||||
writeLine(stdout, "commands:");
|
||||
writeLine(stdout, " health");
|
||||
writeLine(stdout, " project list");
|
||||
writeLine(stdout, " test e2e --env dev --mvp --dry-run");
|
||||
function writeJson(stream, value) {
|
||||
writeLine(stream, formatJson(value));
|
||||
}
|
||||
|
||||
function parseArgs(args) {
|
||||
@@ -127,11 +19,12 @@ function parseArgs(args) {
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (arg.startsWith("--")) {
|
||||
flags.add(arg);
|
||||
const next = args[index + 1];
|
||||
if (next && !next.startsWith("--")) {
|
||||
options.set(arg, next);
|
||||
index += 1;
|
||||
} else {
|
||||
flags.add(arg);
|
||||
}
|
||||
} else {
|
||||
rest.push(arg);
|
||||
@@ -140,46 +33,230 @@ function parseArgs(args) {
|
||||
return { flags, options, rest };
|
||||
}
|
||||
|
||||
function makeError(code, message, details = {}) {
|
||||
return {
|
||||
ok: false,
|
||||
code,
|
||||
message,
|
||||
...details
|
||||
};
|
||||
}
|
||||
|
||||
function mvpGateOverview(summary) {
|
||||
return {
|
||||
ok: true,
|
||||
command: "hwlab-cli health",
|
||||
environment: summary.environment,
|
||||
endpoint: summary.endpoint,
|
||||
gateStatus: summary.gateStatus,
|
||||
devOnly: summary.devOnly,
|
||||
prodDisabled: summary.prodDisabled,
|
||||
reportCommitId: summary.reportCommitId,
|
||||
artifactCount: summary.artifactCount,
|
||||
healthCount: summary.healthCount,
|
||||
milestoneStatus: Object.fromEntries(summary.milestones.map((item) => [item.id, item.status])),
|
||||
blocked: summary.blocked,
|
||||
blockers: summary.blockers
|
||||
};
|
||||
}
|
||||
|
||||
function mvpProjectList(summary) {
|
||||
return {
|
||||
ok: true,
|
||||
command: "hwlab-cli project list",
|
||||
environment: summary.environment,
|
||||
endpoint: summary.endpoint,
|
||||
projects: [
|
||||
{
|
||||
projectId: summary.topology.projectId,
|
||||
name: "HWLAB M5 MVP E2E Dry Run",
|
||||
status: summary.topology.projectStatus,
|
||||
environment: summary.environment,
|
||||
source: summary.generatedFrom.m5Plan
|
||||
}
|
||||
],
|
||||
topology: {
|
||||
gatewaySessions: summary.topology.gateways.map((gateway) => gateway.gatewaySessionId),
|
||||
boxResources: summary.topology.boxResources.map((resource) => resource.resourceId),
|
||||
patchPanelStatusId: summary.topology.patchPanel.patchPanelStatusId
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function mvpDryRunPlan(summary, command) {
|
||||
return {
|
||||
ok: true,
|
||||
command,
|
||||
mode: "dry-run",
|
||||
dryRunOnly: true,
|
||||
environment: summary.environment,
|
||||
endpoint: summary.endpoint,
|
||||
gateStatus: summary.gateStatus,
|
||||
blocked: summary.blocked,
|
||||
generatedFrom: summary.generatedFrom,
|
||||
safety: summary.safety,
|
||||
milestones: summary.milestones,
|
||||
contractStatus: {
|
||||
artifacts: {
|
||||
status: "fixture",
|
||||
count: summary.artifactCount,
|
||||
commitIds: [...new Set(summary.artifacts.map((artifact) => artifact.commitId))],
|
||||
services: summary.artifacts.map((artifact) => artifact.serviceId)
|
||||
},
|
||||
health: {
|
||||
status: "fixture",
|
||||
count: summary.healthCount,
|
||||
components: summary.health.map((health) => ({
|
||||
component: health.component,
|
||||
serviceId: health.serviceId,
|
||||
status: health.status,
|
||||
dryRun: health.dryRun
|
||||
}))
|
||||
},
|
||||
topology: {
|
||||
projectId: summary.topology.projectId,
|
||||
gatewaySessions: summary.gatewaySessionCount,
|
||||
boxResources: summary.boxResourceCount,
|
||||
patchPanelStatusId: summary.topology.patchPanel.patchPanelStatusId,
|
||||
activeConnectionCount: summary.topology.patchPanel.activeConnectionCount
|
||||
},
|
||||
agent: summary.agent,
|
||||
evidence: {
|
||||
operations: summary.operationCount,
|
||||
traceEvents: summary.traceCount,
|
||||
auditEvents: summary.auditCount,
|
||||
records: summary.evidenceRecords,
|
||||
cleanupRecords: summary.cleanupCount
|
||||
}
|
||||
},
|
||||
steps: summary.steps.map((step) => ({
|
||||
id: step.id,
|
||||
order: step.order,
|
||||
title: step.title,
|
||||
kind: step.kind,
|
||||
requires: step.requires
|
||||
})),
|
||||
blockers: summary.blockers,
|
||||
realDevGate: summary.realDevGate,
|
||||
statement: "dry-run only: no DEV/PROD changes were made"
|
||||
};
|
||||
}
|
||||
|
||||
function mvpLiveBlocked(summary, command) {
|
||||
return {
|
||||
ok: false,
|
||||
code: "BLOCKED",
|
||||
command,
|
||||
mode: "live-dev",
|
||||
environment: summary.environment,
|
||||
endpoint: summary.endpoint,
|
||||
gateStatus: summary.gateStatus,
|
||||
blocked: true,
|
||||
blockers: summary.blockers,
|
||||
requiredBeforeRetry: [
|
||||
"Clear all open blockers in reports/dev-gate/dev-mvp-gate-report.json.",
|
||||
"Record live HWLAB DEV health for /health and /live on the frozen DEV endpoint.",
|
||||
"Keep the run DEV-only; do not use UniDesk runtime or PROD substitutes."
|
||||
],
|
||||
generatedFrom: summary.generatedFrom
|
||||
};
|
||||
}
|
||||
|
||||
function writeHelp(stdout) {
|
||||
writeJson(stdout, {
|
||||
ok: true,
|
||||
command: "hwlab-cli",
|
||||
commands: [
|
||||
"health",
|
||||
"project list",
|
||||
"test e2e --env dev --mvp",
|
||||
"test e2e --env dev --mvp --dry-run",
|
||||
"test e2e --env dev --mvp --live --confirm-dev --confirmed-non-production"
|
||||
],
|
||||
defaults: {
|
||||
testE2eMvp: "dry-run plan",
|
||||
output: "json",
|
||||
devEndpoint: DEV_ENDPOINT
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function runCli(argv, io) {
|
||||
const stdout = io.stdout ?? process.stdout;
|
||||
const stderr = io.stderr ?? process.stderr;
|
||||
const runtime = resolveRuntime(io.cwd ?? process.cwd());
|
||||
const repoRoot = io.cwd ?? process.cwd();
|
||||
let summary;
|
||||
|
||||
try {
|
||||
summary = loadMvpGateSummary(repoRoot);
|
||||
} catch (error) {
|
||||
writeJson(stderr, makeError("GATE_SUMMARY_UNREADABLE", error.message));
|
||||
return 1;
|
||||
}
|
||||
|
||||
const [command = "help", subcommand, ...rest] = argv;
|
||||
const { flags, options } = parseArgs(rest);
|
||||
|
||||
if (command === "health") {
|
||||
writeHealth(runtime, stdout);
|
||||
writeJson(stdout, mvpGateOverview(summary));
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (command === "project" && subcommand === "list") {
|
||||
writeProjectList(runtime, stdout);
|
||||
writeJson(stdout, mvpProjectList(summary));
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (command === "test" && subcommand === "e2e") {
|
||||
const env = options.get("--env") ?? null;
|
||||
const isDryRun = flags.has("--dry-run");
|
||||
const isMvp = flags.has("--mvp");
|
||||
const isLive = flags.has("--live");
|
||||
const isDryRun = flags.has("--dry-run") || !isLive;
|
||||
const commandLine = `hwlab-cli test e2e --env ${env ?? "<missing>"}${isMvp ? " --mvp" : ""}${
|
||||
isLive ? " --live" : isDryRun && flags.has("--dry-run") ? " --dry-run" : ""
|
||||
}`;
|
||||
|
||||
if (env !== "dev") {
|
||||
writeLine(stderr, "only --env dev is supported in the skeleton");
|
||||
writeJson(stderr, makeError("UNSUPPORTED_ENV", "Only --env dev is supported for the MVP gate.", { env }));
|
||||
return 1;
|
||||
}
|
||||
if (!isMvp || !isDryRun) {
|
||||
writeLine(stderr, "use --mvp --dry-run for the skeleton");
|
||||
if (!isMvp) {
|
||||
writeJson(stderr, makeError("MISSING_MVP_FLAG", "Use --mvp for the HWLAB MVP E2E gate."));
|
||||
return 1;
|
||||
}
|
||||
writeDryRun(runtime, stdout);
|
||||
|
||||
if (isLive) {
|
||||
if (!flags.has("--confirm-dev") || !flags.has("--confirmed-non-production")) {
|
||||
writeJson(
|
||||
stderr,
|
||||
makeError("LIVE_CONFIRMATION_REQUIRED", "Live DEV requires --confirm-dev --confirmed-non-production.", {
|
||||
requiredFlags: ["--confirm-dev", "--confirmed-non-production"]
|
||||
})
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
if (summary.blocked) {
|
||||
writeJson(stderr, mvpLiveBlocked(summary, commandLine));
|
||||
return 2;
|
||||
}
|
||||
writeJson(
|
||||
stderr,
|
||||
makeError("LIVE_DEV_NOT_IMPLEMENTED", "The MVP CLI does not execute live DEV e2e from this runner.")
|
||||
);
|
||||
return 2;
|
||||
}
|
||||
|
||||
writeJson(stdout, mvpDryRunPlan(summary, commandLine));
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (command === "project" && !subcommand) {
|
||||
writeLine(stderr, "usage: hwlab-cli project list");
|
||||
writeJson(stderr, makeError("USAGE", "usage: hwlab-cli project list"));
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (command === "test" && !subcommand) {
|
||||
writeLine(stderr, "usage: hwlab-cli test e2e --env dev --mvp --dry-run");
|
||||
writeJson(stderr, makeError("USAGE", "usage: hwlab-cli test e2e --env dev --mvp"));
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"hwlab-cli": "./bin/hwlab-cli.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"check": "node --check bin/hwlab-cli.mjs && node --check lib/cli.mjs",
|
||||
"check": "node --check bin/hwlab-cli.mjs && node --check lib/cli.mjs && node ../../scripts/l6-cli-web-smoke.mjs",
|
||||
"health": "node bin/hwlab-cli.mjs health"
|
||||
}
|
||||
}
|
||||
|
||||
+139
-31
@@ -1,40 +1,148 @@
|
||||
import { runtime } from "./runtime.mjs";
|
||||
import { gateSummary } from "./gate-summary.mjs";
|
||||
|
||||
const runtimeGrid = document.querySelector("#runtime-grid");
|
||||
const steps = document.querySelector("#mvp-steps");
|
||||
const projectsBody = document.querySelector("#projects-body");
|
||||
const status = document.querySelector("#status");
|
||||
const metaGrid = document.querySelector("#meta-grid");
|
||||
const milestoneGrid = document.querySelector("#milestone-grid");
|
||||
const healthBody = document.querySelector("#health-body");
|
||||
const artifactBody = document.querySelector("#artifact-body");
|
||||
const topologyGrid = document.querySelector("#topology-grid");
|
||||
const agentGrid = document.querySelector("#agent-grid");
|
||||
const evidenceBody = document.querySelector("#evidence-body");
|
||||
const blockerList = document.querySelector("#blocker-list");
|
||||
const commandList = document.querySelector("#command-list");
|
||||
|
||||
status.textContent = "dev skeleton";
|
||||
|
||||
const runtimePairs = [
|
||||
["Endpoint", runtime.endpoints.dev],
|
||||
["Route", runtime.serviceRoute.join(" -> ")],
|
||||
["Closed loops", runtime.closedLoops.join(" / ")],
|
||||
["Evidence", runtime.evidence[0].uri]
|
||||
];
|
||||
|
||||
for (const [label, value] of runtimePairs) {
|
||||
const dt = document.createElement("dt");
|
||||
dt.textContent = label;
|
||||
const dd = document.createElement("dd");
|
||||
dd.textContent = value;
|
||||
runtimeGrid.append(dt, dd);
|
||||
function appendText(parent, tagName, text, className) {
|
||||
const element = document.createElement(tagName);
|
||||
element.textContent = text;
|
||||
if (className) {
|
||||
element.className = className;
|
||||
}
|
||||
parent.append(element);
|
||||
return element;
|
||||
}
|
||||
|
||||
for (const step of runtime.mvpSteps) {
|
||||
function appendMetricGrid(parent, pairs) {
|
||||
for (const [label, value] of pairs) {
|
||||
const item = document.createElement("div");
|
||||
item.className = "metric";
|
||||
appendText(item, "span", label, "metric-label");
|
||||
appendText(item, "strong", String(value ?? "n/a"), "metric-value");
|
||||
parent.append(item);
|
||||
}
|
||||
}
|
||||
|
||||
function badge(text, tone = text) {
|
||||
const span = document.createElement("span");
|
||||
span.className = `badge tone-${String(tone).replace(/[^a-z0-9_-]/gi, "-").toLowerCase()}`;
|
||||
span.textContent = text;
|
||||
return span;
|
||||
}
|
||||
|
||||
function cell(text, className) {
|
||||
const td = document.createElement("td");
|
||||
td.textContent = text ?? "n/a";
|
||||
if (className) {
|
||||
td.className = className;
|
||||
}
|
||||
return td;
|
||||
}
|
||||
|
||||
status.replaceChildren(badge(gateSummary.gateStatus, gateSummary.gateStatus));
|
||||
|
||||
appendMetricGrid(metaGrid, [
|
||||
["Environment", gateSummary.environment],
|
||||
["Endpoint", gateSummary.endpoint],
|
||||
["Namespace", gateSummary.namespace],
|
||||
["Report commit", gateSummary.reportCommitId],
|
||||
["Gate report", gateSummary.generatedFrom.gateReport],
|
||||
["M5 plan", gateSummary.generatedFrom.m5Plan]
|
||||
]);
|
||||
|
||||
for (const milestone of gateSummary.milestones) {
|
||||
const item = document.createElement("article");
|
||||
item.className = "milestone";
|
||||
const head = document.createElement("div");
|
||||
head.className = "milestone-head";
|
||||
appendText(head, "strong", milestone.id);
|
||||
head.append(badge(milestone.status, milestone.status));
|
||||
item.append(head);
|
||||
appendText(item, "p", milestone.summary);
|
||||
appendText(item, "small", `${milestone.commandCount} commands / ${milestone.evidenceCount} evidence lines`);
|
||||
milestoneGrid.append(item);
|
||||
}
|
||||
|
||||
for (const row of gateSummary.health) {
|
||||
const tr = document.createElement("tr");
|
||||
tr.append(cell(row.component));
|
||||
tr.append(cell(row.serviceId, "mono"));
|
||||
const statusCell = document.createElement("td");
|
||||
statusCell.append(badge(row.status, row.status));
|
||||
tr.append(statusCell);
|
||||
tr.append(cell(row.observedBy));
|
||||
tr.append(cell(row.endpoint ?? gateSummary.endpoint, "mono wrap"));
|
||||
healthBody.append(tr);
|
||||
}
|
||||
|
||||
for (const artifact of gateSummary.artifacts) {
|
||||
const tr = document.createElement("tr");
|
||||
tr.append(cell(artifact.serviceId, "mono"));
|
||||
tr.append(cell(artifact.commitId, "mono"));
|
||||
tr.append(cell(`${artifact.image}:${artifact.tag}`, "mono wrap"));
|
||||
tr.append(cell(artifact.digest, "mono wrap"));
|
||||
tr.append(cell(artifact.healthTimestamp, "mono"));
|
||||
artifactBody.append(tr);
|
||||
}
|
||||
|
||||
appendMetricGrid(topologyGrid, [
|
||||
["Project", gateSummary.topology.projectId],
|
||||
["Gateways", gateSummary.gatewaySessionCount],
|
||||
["Box resources", gateSummary.boxResourceCount],
|
||||
["Patch panel", gateSummary.topology.patchPanel.patchPanelStatusId],
|
||||
["Active links", gateSummary.topology.patchPanel.activeConnectionCount],
|
||||
[
|
||||
"Connection",
|
||||
gateSummary.topology.patchPanel.activeConnections
|
||||
.map((link) => `${link.fromResourceId}:${link.fromPort} -> ${link.toResourceId}:${link.toPort}`)
|
||||
.join(", ")
|
||||
]
|
||||
]);
|
||||
|
||||
appendMetricGrid(agentGrid, [
|
||||
["Agent session", gateSummary.agent.agentSessionId],
|
||||
["Agent service", gateSummary.agent.agentServiceId],
|
||||
["Worker session", gateSummary.agent.workerSessionId],
|
||||
["Worker service", gateSummary.agent.workerServiceId],
|
||||
["Trace events", gateSummary.traceCount],
|
||||
["Audit events", gateSummary.auditCount],
|
||||
["Cleanup records", gateSummary.cleanupCount],
|
||||
["Cleanup id", gateSummary.agent.cleanupId]
|
||||
]);
|
||||
|
||||
for (const record of gateSummary.evidenceRecords) {
|
||||
const tr = document.createElement("tr");
|
||||
tr.append(cell(record.evidenceId, "mono"));
|
||||
tr.append(cell(record.operationId, "mono"));
|
||||
tr.append(cell(record.serviceId, "mono"));
|
||||
tr.append(cell(record.uri, "mono wrap"));
|
||||
tr.append(cell(record.sha256, "mono wrap"));
|
||||
evidenceBody.append(tr);
|
||||
}
|
||||
|
||||
for (const blocker of gateSummary.blockers) {
|
||||
const li = document.createElement("li");
|
||||
li.textContent = step;
|
||||
steps.append(li);
|
||||
const head = document.createElement("div");
|
||||
head.className = "blocker-head";
|
||||
head.append(badge(blocker.type, "blocked"));
|
||||
appendText(head, "strong", blocker.scope);
|
||||
li.append(head);
|
||||
appendText(li, "p", blocker.summary);
|
||||
blockerList.append(li);
|
||||
}
|
||||
|
||||
for (const project of runtime.projects) {
|
||||
const row = document.createElement("tr");
|
||||
row.innerHTML = `
|
||||
<td>${project.projectId}<br /><strong>${project.name}</strong></td>
|
||||
<td>${project.status}</td>
|
||||
<td>${project.environment}</td>
|
||||
<td>${project.updatedAt}</td>
|
||||
`;
|
||||
projectsBody.append(row);
|
||||
for (const command of [
|
||||
"node tools/hwlab-cli/bin/hwlab-cli.mjs test e2e --env dev --mvp",
|
||||
"node tools/hwlab-cli/bin/hwlab-cli.mjs test e2e --env dev --mvp --live --confirm-dev --confirmed-non-production",
|
||||
"node scripts/m5-mvp-e2e-dry-run.mjs"
|
||||
]) {
|
||||
appendText(commandList, "li", command, "mono");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,809 @@
|
||||
// Generated from reports/dev-gate/dev-mvp-gate-report.json and fixtures/mvp/m5-e2e/dry-run-plan.json.
|
||||
// Run node scripts/export-web-gate-summary.mjs after changing those fixtures.
|
||||
export const gateSummary = {
|
||||
"generatedFrom": {
|
||||
"gateReport": "reports/dev-gate/dev-mvp-gate-report.json",
|
||||
"m5Plan": "fixtures/mvp/m5-e2e/dry-run-plan.json"
|
||||
},
|
||||
"issue": "pikasTech/HWLAB#56",
|
||||
"supports": [
|
||||
"pikasTech/HWLAB#7",
|
||||
"pikasTech/HWLAB#9"
|
||||
],
|
||||
"gateStatus": "blocked",
|
||||
"blocked": true,
|
||||
"acceptanceLevel": "dev_mvp_gate",
|
||||
"reportCommitId": "6eb4c199b0c63cca7fd68fc3306f7a9cbe4323ea",
|
||||
"dryRunOnly": true,
|
||||
"devOnly": true,
|
||||
"prodDisabled": true,
|
||||
"environment": "dev",
|
||||
"endpoint": "http://74.48.78.17:6667",
|
||||
"namespace": "hwlab-dev",
|
||||
"sourceSummary": "The DEV gate is anchored to the frozen matrix, gate contract, and M0-M5 milestone docs on origin/main.",
|
||||
"localSmoke": {
|
||||
"status": "pass",
|
||||
"commands": [
|
||||
"node scripts/validate-contract.mjs",
|
||||
"node scripts/validate-m0-contract.mjs",
|
||||
"node scripts/validate-artifact-catalog.mjs",
|
||||
"node scripts/validate-runtime-boundary.mjs",
|
||||
"node scripts/validate-schema-drift-map.mjs",
|
||||
"node scripts/validate-evidence-chain.mjs",
|
||||
"node scripts/m1-contract-smoke.mjs",
|
||||
"node scripts/m2-dev-deploy-smoke.mjs --dry-run",
|
||||
"node scripts/m3-hardware-loop-smoke.mjs",
|
||||
"node scripts/m4-agent-loop-smoke.mjs"
|
||||
],
|
||||
"evidence": [
|
||||
"validated 15 JSON files and 13 service ids",
|
||||
"validated M0 contract examples: 7 JSON files, 13 service ids",
|
||||
"validated 13 DEV artifact catalog services at 24eb3bf",
|
||||
"validated HWLAB runtime boundary guard for 13 services; forbidden substitutes: unidesk-backend, provider-gateway, microservice-proxy",
|
||||
"validated schema drift map with 59 aligned fields and 12 gaps",
|
||||
"validated 2 evidence chain fixtures and 2 evidence records",
|
||||
"[m1-smoke] passed",
|
||||
"[m2-smoke] validated 11 service contracts",
|
||||
"[m2-smoke] mode=dry-run endpoint=http://74.48.78.17:6667",
|
||||
"[m2-smoke] no real DEV/PROD request was made",
|
||||
"M3 hardware loop smoke passed",
|
||||
"[m4-smoke] ok m4-agent-automation-loop-local-contract-smoke"
|
||||
],
|
||||
"summary": "M0-M4 contract and smoke checks pass locally without contacting the live DEV ingress."
|
||||
},
|
||||
"dryRun": {
|
||||
"status": "pass",
|
||||
"commands": [
|
||||
"node tools/hwlab-cli/bin/hwlab-cli.mjs health",
|
||||
"node tools/hwlab-cli/bin/hwlab-cli.mjs test e2e --env dev --mvp --dry-run",
|
||||
"node scripts/m5-mvp-e2e-dry-run.mjs"
|
||||
],
|
||||
"evidence": [
|
||||
"hwlab-cli health -> status: ok, environment: dev, endpoint: http://74.48.78.17:6667, projects: 2",
|
||||
"hwlab-cli dry-run -> 9 steps, 13 artifacts, 14 health contracts, 2 hardware operations, 2 evidence records, 0 network calls",
|
||||
"M5 MVP E2E dry-run passed: 9 steps, 13 artifacts, 14 health contracts, 2 hardware operations, 2 evidence records, 0 network calls"
|
||||
],
|
||||
"summary": "The M5 orchestration dry-run is green, but it remains fixture-only and does not establish a live DEV gate."
|
||||
},
|
||||
"devPreconditions": {
|
||||
"status": "blocked",
|
||||
"requirements": [
|
||||
"Obtain a live HTTP 200/JSON health observation from http://74.48.78.17:6667/health and /live",
|
||||
"Record live route observations for hwlab-edge-proxy, hwlab-tunnel-client, hwlab-router, hwlab-cloud-api/web, hwlab-gateway, hwlab-gateway-simu, hwlab-box-simu, hwlab-patch-panel, hwlab-agent-mgr, hwlab-agent-worker, and hwlab-agent-skills",
|
||||
"Keep the run DEV-only and avoid PROD, heavy e2e, secrets, restarts, or UniDesk substitution"
|
||||
],
|
||||
"commands": [
|
||||
"curl -fsS --max-time 5 http://74.48.78.17:6667/health",
|
||||
"curl -fsS --max-time 5 http://74.48.78.17:6667/live"
|
||||
],
|
||||
"evidence": [
|
||||
"curl exit 7 for /health: could not connect to 74.48.78.17 port 6667",
|
||||
"curl exit 7 for /live: could not connect to 74.48.78.17 port 6667"
|
||||
],
|
||||
"summary": "The frozen DEV endpoint is not reachable from this runner, so no live DEV gate can be claimed yet."
|
||||
},
|
||||
"blockers": [
|
||||
{
|
||||
"type": "network_blocker",
|
||||
"scope": "devPreconditions",
|
||||
"status": "open",
|
||||
"summary": "http://74.48.78.17:6667/health and /live both fail with curl exit 7; live DEV ingress evidence is missing."
|
||||
}
|
||||
],
|
||||
"milestones": [
|
||||
{
|
||||
"id": "M0",
|
||||
"status": "pass",
|
||||
"commandCount": 6,
|
||||
"evidenceCount": 6,
|
||||
"summary": "Frozen contract, artifact, boundary, drift, and evidence fixtures are parseable and aligned to the DEV boundary."
|
||||
},
|
||||
{
|
||||
"id": "M1",
|
||||
"status": "pass",
|
||||
"commandCount": 1,
|
||||
"evidenceCount": 4,
|
||||
"summary": "The local M1 contract smoke passes."
|
||||
},
|
||||
{
|
||||
"id": "M2",
|
||||
"status": "pass",
|
||||
"commandCount": 1,
|
||||
"evidenceCount": 3,
|
||||
"summary": "The deploy smoke remains fixture-only and validates the frozen route phases and artifact metadata."
|
||||
},
|
||||
{
|
||||
"id": "M3",
|
||||
"status": "pass",
|
||||
"commandCount": 1,
|
||||
"evidenceCount": 3,
|
||||
"summary": "The local hardware trusted loop smoke passes."
|
||||
},
|
||||
{
|
||||
"id": "M4",
|
||||
"status": "pass",
|
||||
"commandCount": 1,
|
||||
"evidenceCount": 1,
|
||||
"summary": "The local agent automation loop smoke passes."
|
||||
},
|
||||
{
|
||||
"id": "M5",
|
||||
"status": "pass",
|
||||
"commandCount": 3,
|
||||
"evidenceCount": 3,
|
||||
"summary": "The M5 orchestration is green in dry-run mode, but it stays fixture-only and does not claim live DEV readiness."
|
||||
}
|
||||
],
|
||||
"artifactCount": 13,
|
||||
"healthCount": 14,
|
||||
"gatewaySessionCount": 2,
|
||||
"boxResourceCount": 2,
|
||||
"patchPanelCount": 1,
|
||||
"operationCount": 2,
|
||||
"evidenceCount": 2,
|
||||
"traceCount": 8,
|
||||
"auditCount": 7,
|
||||
"cleanupCount": 2,
|
||||
"artifacts": [
|
||||
{
|
||||
"serviceId": "hwlab-edge-proxy",
|
||||
"commitId": "caa9ed0",
|
||||
"image": "hwlab-edge-proxy",
|
||||
"tag": "dry-run-caa9ed0",
|
||||
"digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
|
||||
"digestNotApplicableReason": null,
|
||||
"buildSource": "pikasTech/HWLAB@origin/main#dry-run-fixture",
|
||||
"deployEnv": "dev",
|
||||
"healthTimestamp": "2026-05-21T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-tunnel-client",
|
||||
"commitId": "caa9ed0",
|
||||
"image": "hwlab-tunnel-client",
|
||||
"tag": "dry-run-caa9ed0",
|
||||
"digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222",
|
||||
"digestNotApplicableReason": null,
|
||||
"buildSource": "pikasTech/HWLAB@origin/main#dry-run-fixture",
|
||||
"deployEnv": "dev",
|
||||
"healthTimestamp": "2026-05-21T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-router",
|
||||
"commitId": "caa9ed0",
|
||||
"image": "hwlab-router",
|
||||
"tag": "dry-run-caa9ed0",
|
||||
"digest": "sha256:3333333333333333333333333333333333333333333333333333333333333333",
|
||||
"digestNotApplicableReason": null,
|
||||
"buildSource": "pikasTech/HWLAB@origin/main#dry-run-fixture",
|
||||
"deployEnv": "dev",
|
||||
"healthTimestamp": "2026-05-21T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-cloud-api",
|
||||
"commitId": "caa9ed0",
|
||||
"image": "hwlab-cloud-api",
|
||||
"tag": "dry-run-caa9ed0",
|
||||
"digest": "sha256:4444444444444444444444444444444444444444444444444444444444444444",
|
||||
"digestNotApplicableReason": null,
|
||||
"buildSource": "pikasTech/HWLAB@origin/main#dry-run-fixture",
|
||||
"deployEnv": "dev",
|
||||
"healthTimestamp": "2026-05-21T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-cloud-web",
|
||||
"commitId": "caa9ed0",
|
||||
"image": "hwlab-cloud-web-static-assets",
|
||||
"tag": "dry-run-caa9ed0",
|
||||
"digest": "not_applicable",
|
||||
"digestNotApplicableReason": "static asset digest is produced by the future DEV build pipeline, not this local dry-run fixture",
|
||||
"buildSource": "pikasTech/HWLAB@origin/main#dry-run-fixture",
|
||||
"deployEnv": "dev",
|
||||
"healthTimestamp": "2026-05-21T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-gateway",
|
||||
"commitId": "caa9ed0",
|
||||
"image": "hwlab-gateway",
|
||||
"tag": "dry-run-caa9ed0",
|
||||
"digest": "sha256:5555555555555555555555555555555555555555555555555555555555555555",
|
||||
"digestNotApplicableReason": null,
|
||||
"buildSource": "pikasTech/HWLAB@origin/main#dry-run-fixture",
|
||||
"deployEnv": "dev",
|
||||
"healthTimestamp": "2026-05-21T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-gateway-simu",
|
||||
"commitId": "caa9ed0",
|
||||
"image": "hwlab-gateway-simu",
|
||||
"tag": "dry-run-caa9ed0",
|
||||
"digest": "sha256:6666666666666666666666666666666666666666666666666666666666666666",
|
||||
"digestNotApplicableReason": null,
|
||||
"buildSource": "pikasTech/HWLAB@origin/main#dry-run-fixture",
|
||||
"deployEnv": "dev",
|
||||
"healthTimestamp": "2026-05-21T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-box-simu",
|
||||
"commitId": "caa9ed0",
|
||||
"image": "hwlab-box-simu",
|
||||
"tag": "dry-run-caa9ed0",
|
||||
"digest": "sha256:7777777777777777777777777777777777777777777777777777777777777777",
|
||||
"digestNotApplicableReason": null,
|
||||
"buildSource": "pikasTech/HWLAB@origin/main#dry-run-fixture",
|
||||
"deployEnv": "dev",
|
||||
"healthTimestamp": "2026-05-21T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-patch-panel",
|
||||
"commitId": "caa9ed0",
|
||||
"image": "hwlab-patch-panel",
|
||||
"tag": "dry-run-caa9ed0",
|
||||
"digest": "sha256:8888888888888888888888888888888888888888888888888888888888888888",
|
||||
"digestNotApplicableReason": null,
|
||||
"buildSource": "pikasTech/HWLAB@origin/main#dry-run-fixture",
|
||||
"deployEnv": "dev",
|
||||
"healthTimestamp": "2026-05-21T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-agent-mgr",
|
||||
"commitId": "caa9ed0",
|
||||
"image": "hwlab-agent-mgr",
|
||||
"tag": "dry-run-caa9ed0",
|
||||
"digest": "sha256:9999999999999999999999999999999999999999999999999999999999999999",
|
||||
"digestNotApplicableReason": null,
|
||||
"buildSource": "pikasTech/HWLAB@origin/main#dry-run-fixture",
|
||||
"deployEnv": "dev",
|
||||
"healthTimestamp": "2026-05-21T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-agent-worker",
|
||||
"commitId": "caa9ed0",
|
||||
"image": "hwlab-agent-worker",
|
||||
"tag": "dry-run-caa9ed0",
|
||||
"digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"digestNotApplicableReason": null,
|
||||
"buildSource": "pikasTech/HWLAB@origin/main#dry-run-fixture",
|
||||
"deployEnv": "dev",
|
||||
"healthTimestamp": "2026-05-21T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-agent-skills",
|
||||
"commitId": "caa9ed0",
|
||||
"image": "hwlab-agent-runtime-skills",
|
||||
"tag": "dry-run-caa9ed0",
|
||||
"digest": "not_applicable",
|
||||
"digestNotApplicableReason": "skill bundle digest is produced by the future DEV build pipeline, not this local dry-run fixture",
|
||||
"buildSource": "pikasTech/HWLAB@origin/main#dry-run-fixture",
|
||||
"deployEnv": "dev",
|
||||
"healthTimestamp": "2026-05-21T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-cli",
|
||||
"commitId": "caa9ed0",
|
||||
"image": "hwlab-cli-npm-package",
|
||||
"tag": "dry-run-caa9ed0",
|
||||
"digest": "not_applicable",
|
||||
"digestNotApplicableReason": "local CLI package is not containerized in this dry-run fixture",
|
||||
"buildSource": "pikasTech/HWLAB@origin/main#dry-run-fixture",
|
||||
"deployEnv": "dev",
|
||||
"healthTimestamp": "2026-05-21T00:00:00.000Z"
|
||||
}
|
||||
],
|
||||
"health": [
|
||||
{
|
||||
"healthId": "hlt_m5-dev-ingress",
|
||||
"serviceId": "hwlab-edge-proxy",
|
||||
"component": "DEV ingress",
|
||||
"status": "healthy",
|
||||
"deployEnv": "dev",
|
||||
"endpoint": "http://74.48.78.17:6667/health",
|
||||
"observedBy": "fixture",
|
||||
"dryRun": true,
|
||||
"healthTimestamp": "2026-05-21T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"healthId": "hlt_m5-edge-route",
|
||||
"serviceId": "hwlab-edge-proxy",
|
||||
"component": "master edge proxy",
|
||||
"status": "healthy",
|
||||
"deployEnv": "dev",
|
||||
"endpoint": "http://74.48.78.17:6667",
|
||||
"observedBy": "fixture",
|
||||
"dryRun": true,
|
||||
"healthTimestamp": "2026-05-21T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"healthId": "hlt_m5-frp",
|
||||
"serviceId": "hwlab-tunnel-client",
|
||||
"component": "frp",
|
||||
"status": "healthy",
|
||||
"deployEnv": "dev",
|
||||
"endpoint": null,
|
||||
"observedBy": "fixture",
|
||||
"dryRun": true,
|
||||
"healthTimestamp": "2026-05-21T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"healthId": "hlt_m5-router",
|
||||
"serviceId": "hwlab-router",
|
||||
"component": "D601 router",
|
||||
"status": "healthy",
|
||||
"deployEnv": "dev",
|
||||
"endpoint": null,
|
||||
"observedBy": "fixture",
|
||||
"dryRun": true,
|
||||
"healthTimestamp": "2026-05-21T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"healthId": "hlt_m5-cloud-api",
|
||||
"serviceId": "hwlab-cloud-api",
|
||||
"component": "Cloud API",
|
||||
"status": "healthy",
|
||||
"deployEnv": "dev",
|
||||
"endpoint": null,
|
||||
"observedBy": "fixture",
|
||||
"dryRun": true,
|
||||
"healthTimestamp": "2026-05-21T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"healthId": "hlt_m5-cloud-web",
|
||||
"serviceId": "hwlab-cloud-web",
|
||||
"component": "Cloud Web",
|
||||
"status": "healthy",
|
||||
"deployEnv": "dev",
|
||||
"endpoint": "http://74.48.78.17:6667",
|
||||
"observedBy": "fixture",
|
||||
"dryRun": true,
|
||||
"healthTimestamp": "2026-05-21T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"healthId": "hlt_m5-gateway",
|
||||
"serviceId": "hwlab-gateway",
|
||||
"component": "Gateway",
|
||||
"status": "healthy",
|
||||
"deployEnv": "dev",
|
||||
"endpoint": null,
|
||||
"observedBy": "fixture",
|
||||
"dryRun": true,
|
||||
"healthTimestamp": "2026-05-21T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"healthId": "hlt_m5-gateway-simu",
|
||||
"serviceId": "hwlab-gateway-simu",
|
||||
"component": "Gateway simulator",
|
||||
"status": "healthy",
|
||||
"deployEnv": "dev",
|
||||
"endpoint": null,
|
||||
"observedBy": "fixture",
|
||||
"dryRun": true,
|
||||
"healthTimestamp": "2026-05-21T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"healthId": "hlt_m5-box-simu",
|
||||
"serviceId": "hwlab-box-simu",
|
||||
"component": "Box simulator",
|
||||
"status": "healthy",
|
||||
"deployEnv": "dev",
|
||||
"endpoint": null,
|
||||
"observedBy": "fixture",
|
||||
"dryRun": true,
|
||||
"healthTimestamp": "2026-05-21T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"healthId": "hlt_m5-patch-panel",
|
||||
"serviceId": "hwlab-patch-panel",
|
||||
"component": "Patch panel",
|
||||
"status": "healthy",
|
||||
"deployEnv": "dev",
|
||||
"endpoint": null,
|
||||
"observedBy": "fixture",
|
||||
"dryRun": true,
|
||||
"healthTimestamp": "2026-05-21T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"healthId": "hlt_m5-agent-mgr",
|
||||
"serviceId": "hwlab-agent-mgr",
|
||||
"component": "Agent manager",
|
||||
"status": "healthy",
|
||||
"deployEnv": "dev",
|
||||
"endpoint": null,
|
||||
"observedBy": "fixture",
|
||||
"dryRun": true,
|
||||
"healthTimestamp": "2026-05-21T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"healthId": "hlt_m5-agent-worker",
|
||||
"serviceId": "hwlab-agent-worker",
|
||||
"component": "Agent worker",
|
||||
"status": "healthy",
|
||||
"deployEnv": "dev",
|
||||
"endpoint": null,
|
||||
"observedBy": "fixture",
|
||||
"dryRun": true,
|
||||
"healthTimestamp": "2026-05-21T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"healthId": "hlt_m5-agent-skills",
|
||||
"serviceId": "hwlab-agent-skills",
|
||||
"component": "Agent skills",
|
||||
"status": "healthy",
|
||||
"deployEnv": "dev",
|
||||
"endpoint": null,
|
||||
"observedBy": "fixture",
|
||||
"dryRun": true,
|
||||
"healthTimestamp": "2026-05-21T00:00:00.000Z"
|
||||
},
|
||||
{
|
||||
"healthId": "hlt_m5-cli",
|
||||
"serviceId": "hwlab-cli",
|
||||
"component": "CLI",
|
||||
"status": "healthy",
|
||||
"deployEnv": "dev",
|
||||
"endpoint": "http://74.48.78.17:6667",
|
||||
"observedBy": "fixture",
|
||||
"dryRun": true,
|
||||
"healthTimestamp": "2026-05-21T00:00:00.000Z"
|
||||
}
|
||||
],
|
||||
"topology": {
|
||||
"projectId": "proj_m5-mvp-e2e",
|
||||
"projectStatus": "active",
|
||||
"gateways": [
|
||||
{
|
||||
"gatewaySessionId": "gws_m5-0001",
|
||||
"serviceId": "hwlab-gateway",
|
||||
"gatewayId": "gtw_m5-dev-boundary",
|
||||
"status": "connected",
|
||||
"environment": "dev",
|
||||
"endpoint": "http://74.48.78.17:6667/gateway"
|
||||
},
|
||||
{
|
||||
"gatewaySessionId": "gws_m5-simu-0001",
|
||||
"serviceId": "hwlab-gateway-simu",
|
||||
"gatewayId": "gtw_m5-simu",
|
||||
"status": "connected",
|
||||
"environment": "dev",
|
||||
"endpoint": null
|
||||
}
|
||||
],
|
||||
"boxResources": [
|
||||
{
|
||||
"resourceId": "res_m5-control-relay",
|
||||
"gatewaySessionId": "gws_m5-0001",
|
||||
"boxId": "box_m5-control",
|
||||
"resourceType": "relay",
|
||||
"name": "M5 control relay",
|
||||
"state": "available",
|
||||
"environment": "dev"
|
||||
},
|
||||
{
|
||||
"resourceId": "res_m5-target-board",
|
||||
"gatewaySessionId": "gws_m5-0001",
|
||||
"boxId": "box_m5-target",
|
||||
"resourceType": "board",
|
||||
"name": "M5 target board",
|
||||
"state": "available",
|
||||
"environment": "dev"
|
||||
}
|
||||
],
|
||||
"patchPanel": {
|
||||
"patchPanelStatusId": "pps_m5-0001",
|
||||
"serviceId": "hwlab-patch-panel",
|
||||
"state": "active",
|
||||
"environment": "dev",
|
||||
"activeConnectionCount": 1,
|
||||
"activeConnections": [
|
||||
{
|
||||
"fromResourceId": "res_m5-control-relay",
|
||||
"fromPort": "out1",
|
||||
"toResourceId": "res_m5-target-board",
|
||||
"toPort": "reset"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"agent": {
|
||||
"agentSessionId": "agt_m5-0001",
|
||||
"agentServiceId": "hwlab-agent-mgr",
|
||||
"agentStatus": "completed",
|
||||
"workerSessionId": "wkr_m5-0001",
|
||||
"workerServiceId": "hwlab-agent-worker",
|
||||
"workerStatus": "completed",
|
||||
"projectId": "proj_m5-mvp-e2e",
|
||||
"environment": "dev",
|
||||
"cleanupId": "cln_m5-worker"
|
||||
},
|
||||
"operations": [
|
||||
{
|
||||
"operationId": "op_m5-direct-0001",
|
||||
"requestedBy": "user_m5-dry-run",
|
||||
"status": "succeeded",
|
||||
"resourceId": "res_m5-control-relay",
|
||||
"capabilityId": "cap_m5-relay-set",
|
||||
"agentSessionId": null,
|
||||
"workerSessionId": null,
|
||||
"environment": "dev"
|
||||
},
|
||||
{
|
||||
"operationId": "op_m5-agent-0001",
|
||||
"requestedBy": "agent_m5-dry-run",
|
||||
"status": "succeeded",
|
||||
"resourceId": "res_m5-control-relay",
|
||||
"capabilityId": "cap_m5-relay-set",
|
||||
"agentSessionId": "agt_m5-0001",
|
||||
"workerSessionId": "wkr_m5-0001",
|
||||
"environment": "dev"
|
||||
}
|
||||
],
|
||||
"evidenceRecords": [
|
||||
{
|
||||
"evidenceId": "evi_m5-direct-0001",
|
||||
"operationId": "op_m5-direct-0001",
|
||||
"agentSessionId": null,
|
||||
"workerSessionId": null,
|
||||
"kind": "trace",
|
||||
"uri": "fixtures/mvp/m5-e2e/evidence/direct-operation.txt",
|
||||
"sha256": "4cd5cc2c942501b55f7a32f4184e9a1c920b31f13110f44ec68ddc0ad7dc1e34",
|
||||
"serviceId": "hwlab-cloud-api",
|
||||
"environment": "dev",
|
||||
"dryRun": true
|
||||
},
|
||||
{
|
||||
"evidenceId": "evi_m5-agent-0001",
|
||||
"operationId": "op_m5-agent-0001",
|
||||
"agentSessionId": "agt_m5-0001",
|
||||
"workerSessionId": "wkr_m5-0001",
|
||||
"kind": "trace",
|
||||
"uri": "fixtures/mvp/m5-e2e/evidence/agent-operation.txt",
|
||||
"sha256": "96e7ffa99efd78b3d5d1b1dfd08be2f077f618dacf786b47f764794714c15c1c",
|
||||
"serviceId": "hwlab-agent-worker",
|
||||
"environment": "dev",
|
||||
"dryRun": true
|
||||
}
|
||||
],
|
||||
"steps": [
|
||||
{
|
||||
"id": "step-dev-health",
|
||||
"order": 1,
|
||||
"title": "DEV health",
|
||||
"kind": "health",
|
||||
"requires": [],
|
||||
"inputs": [
|
||||
"endpoint",
|
||||
"health:hlt_m5-dev-ingress",
|
||||
"health:hlt_m5-edge-route",
|
||||
"health:hlt_m5-frp",
|
||||
"health:hlt_m5-router",
|
||||
"health:hlt_m5-cloud-api",
|
||||
"health:hlt_m5-cloud-web"
|
||||
],
|
||||
"outputs": [
|
||||
"route_m5-dev",
|
||||
"tun_m5-d601",
|
||||
"hwlab-dev",
|
||||
"hwlab-cloud-api",
|
||||
"hwlab-cloud-web"
|
||||
],
|
||||
"acceptanceStepIds": [
|
||||
"static-contract-parse",
|
||||
"endpoint-freeze",
|
||||
"dev-ingress-health",
|
||||
"edge-route",
|
||||
"frp-tunnel",
|
||||
"d601-router",
|
||||
"cloud-surface"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "step-gateway-box-status",
|
||||
"order": 2,
|
||||
"title": "gateway/box status",
|
||||
"kind": "status",
|
||||
"requires": [
|
||||
"step-dev-health"
|
||||
],
|
||||
"inputs": [
|
||||
"gatewaySession:gws_m5-0001",
|
||||
"gatewaySession:gws_m5-simu-0001",
|
||||
"boxResource:res_m5-control-relay",
|
||||
"boxResource:res_m5-target-board",
|
||||
"capability:cap_m5-relay-set",
|
||||
"capability:cap_m5-board-reset"
|
||||
],
|
||||
"outputs": [
|
||||
"gws_m5-0001",
|
||||
"res_m5-control-relay",
|
||||
"res_m5-target-board",
|
||||
"cap_m5-relay-set",
|
||||
"cap_m5-board-reset"
|
||||
],
|
||||
"acceptanceStepIds": [
|
||||
"gateway-sim-patch-panel"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "step-wiring",
|
||||
"order": 3,
|
||||
"title": "wiring",
|
||||
"kind": "wiring",
|
||||
"requires": [
|
||||
"step-gateway-box-status"
|
||||
],
|
||||
"inputs": [
|
||||
"wiringConfig:wir_m5-0001",
|
||||
"patchPanelStatus:pps_m5-0001"
|
||||
],
|
||||
"outputs": [
|
||||
"wir_m5-0001",
|
||||
"pps_m5-0001"
|
||||
],
|
||||
"acceptanceStepIds": [
|
||||
"gateway-sim-patch-panel"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "step-direct-hardware-call",
|
||||
"order": 4,
|
||||
"title": "direct hardware call",
|
||||
"kind": "hardware_operation",
|
||||
"requires": [
|
||||
"step-wiring"
|
||||
],
|
||||
"inputs": [
|
||||
"rpc:direct-hardware-request",
|
||||
"operation:op_m5-direct-0001",
|
||||
"resource:res_m5-control-relay",
|
||||
"capability:cap_m5-relay-set",
|
||||
"patchPanelStatus:pps_m5-0001"
|
||||
],
|
||||
"outputs": [
|
||||
"op_m5-direct-0001",
|
||||
"trc_m5-direct-0001"
|
||||
],
|
||||
"acceptanceStepIds": [
|
||||
"gateway-sim-patch-panel"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "step-audit",
|
||||
"order": 5,
|
||||
"title": "audit",
|
||||
"kind": "audit",
|
||||
"requires": [
|
||||
"step-direct-hardware-call"
|
||||
],
|
||||
"inputs": [
|
||||
"audit:aud_m5-project-selected",
|
||||
"audit:aud_m5-gateway-started",
|
||||
"audit:aud_m5-wiring-applied",
|
||||
"audit:aud_m5-direct-operation"
|
||||
],
|
||||
"outputs": [
|
||||
"aud_m5-direct-operation"
|
||||
],
|
||||
"acceptanceStepIds": [
|
||||
"artifact-observability"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "step-agent-session",
|
||||
"order": 6,
|
||||
"title": "agent session",
|
||||
"kind": "agent",
|
||||
"requires": [
|
||||
"step-audit"
|
||||
],
|
||||
"inputs": [
|
||||
"agentSession:agt_m5-0001",
|
||||
"workerSession:wkr_m5-0001",
|
||||
"health:hlt_m5-agent-mgr",
|
||||
"health:hlt_m5-agent-worker",
|
||||
"health:hlt_m5-agent-skills"
|
||||
],
|
||||
"outputs": [
|
||||
"agt_m5-0001",
|
||||
"wkr_m5-0001"
|
||||
],
|
||||
"acceptanceStepIds": [
|
||||
"agent-runtime"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "step-agent-hardware-call",
|
||||
"order": 7,
|
||||
"title": "agent hardware call",
|
||||
"kind": "hardware_operation",
|
||||
"requires": [
|
||||
"step-agent-session",
|
||||
"step-wiring"
|
||||
],
|
||||
"inputs": [
|
||||
"rpc:agent-hardware-request",
|
||||
"operation:op_m5-agent-0001",
|
||||
"agentSession:agt_m5-0001",
|
||||
"workerSession:wkr_m5-0001",
|
||||
"resource:res_m5-control-relay",
|
||||
"capability:cap_m5-relay-set",
|
||||
"patchPanelStatus:pps_m5-0001"
|
||||
],
|
||||
"outputs": [
|
||||
"op_m5-agent-0001",
|
||||
"trc_m5-agent-0001"
|
||||
],
|
||||
"acceptanceStepIds": [
|
||||
"agent-runtime"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "step-evidence",
|
||||
"order": 8,
|
||||
"title": "evidence",
|
||||
"kind": "evidence",
|
||||
"requires": [
|
||||
"step-direct-hardware-call",
|
||||
"step-agent-hardware-call"
|
||||
],
|
||||
"inputs": [
|
||||
"trace:trc_m5-direct-0001",
|
||||
"trace:trc_m5-agent-0001",
|
||||
"evidence:evi_m5-direct-0001",
|
||||
"evidence:evi_m5-agent-0001"
|
||||
],
|
||||
"outputs": [
|
||||
"evi_m5-direct-0001",
|
||||
"evi_m5-agent-0001"
|
||||
],
|
||||
"acceptanceStepIds": [
|
||||
"agent-runtime",
|
||||
"artifact-observability"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "step-cleanup",
|
||||
"order": 9,
|
||||
"title": "cleanup",
|
||||
"kind": "cleanup",
|
||||
"requires": [
|
||||
"step-evidence"
|
||||
],
|
||||
"inputs": [
|
||||
"cleanup:cln_m5-worker",
|
||||
"cleanup:cln_m5-gateway-state",
|
||||
"audit:aud_m5-cleanup"
|
||||
],
|
||||
"outputs": [
|
||||
"cln_m5-worker",
|
||||
"cln_m5-gateway-state"
|
||||
],
|
||||
"acceptanceStepIds": [
|
||||
"agent-runtime"
|
||||
]
|
||||
}
|
||||
],
|
||||
"realDevGate": {
|
||||
"sourcePlanCommand": "hwlab-cli test e2e --env dev --mvp",
|
||||
"command": "hwlab-cli test e2e --env dev --mvp --live --confirm-dev --confirmed-non-production",
|
||||
"requiresAllBlockersClear": true,
|
||||
"requiresDryRunPass": true,
|
||||
"requiresArtifactObservability": true,
|
||||
"requiresD601RouteObservation": true,
|
||||
"requiresHumanApprovalForRealDev": true
|
||||
},
|
||||
"safety": {
|
||||
"mode": "dry-run",
|
||||
"allowNetwork": false,
|
||||
"allowDeploy": false,
|
||||
"allowBrowserE2E": false,
|
||||
"allowSecrets": false,
|
||||
"prohibitedActions": [
|
||||
"real-dev-deploy",
|
||||
"prod-deploy",
|
||||
"prod-smoke",
|
||||
"browser-e2e",
|
||||
"heavyweight-e2e",
|
||||
"secret-or-token-read",
|
||||
"force-push",
|
||||
"unidesk-runtime-substitution"
|
||||
]
|
||||
}
|
||||
};
|
||||
+101
-22
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>HWLAB Cloud Web</title>
|
||||
<title>HWLAB DEV MVP Gate</title>
|
||||
<link rel="stylesheet" href="./styles.css" />
|
||||
<script type="module" src="./app.mjs"></script>
|
||||
</head>
|
||||
@@ -11,47 +11,126 @@
|
||||
<main class="shell">
|
||||
<header class="topbar">
|
||||
<div>
|
||||
<p class="eyebrow">HWLAB</p>
|
||||
<h1>Cloud Web</h1>
|
||||
<p class="eyebrow">HWLAB L6 Frontend / CLI MVP</p>
|
||||
<h1>DEV MVP Gate</h1>
|
||||
</div>
|
||||
<div class="status" id="status">dev skeleton</div>
|
||||
<div class="status" id="status"></div>
|
||||
</header>
|
||||
|
||||
<section class="panel">
|
||||
<section class="panel compact">
|
||||
<div class="panel-head">
|
||||
<h2>Runtime</h2>
|
||||
<p>Mock dashboard for the L6 CLI and cloud-web surface.</p>
|
||||
<h2>Acceptance Source</h2>
|
||||
<p>Contract-driven view of the checked-in M0-M5 gate report and M5 dry-run plan.</p>
|
||||
</div>
|
||||
<dl class="grid" id="runtime-grid"></dl>
|
||||
<div class="metrics six" id="meta-grid"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel compact">
|
||||
<div class="panel-head">
|
||||
<h2>M0-M5 Gates</h2>
|
||||
<p>Local contract and dry-run status. Live DEV remains separate from fixture evidence.</p>
|
||||
</div>
|
||||
<div class="milestones" id="milestone-grid"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-head">
|
||||
<h2>MVP route</h2>
|
||||
<p>Fixed dry-run path shared with the CLI skeleton.</p>
|
||||
</div>
|
||||
<ol class="steps" id="mvp-steps"></ol>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-head">
|
||||
<h2>Projects</h2>
|
||||
<p>Static project list seeded from the mock fixture.</p>
|
||||
<h2>DEV Health Contract</h2>
|
||||
<p>Read from the M5 gate fixture; live health is not claimed while blockers are open.</p>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Project</th>
|
||||
<th>Component</th>
|
||||
<th>Service</th>
|
||||
<th>Status</th>
|
||||
<th>Environment</th>
|
||||
<th>Updated</th>
|
||||
<th>Observed</th>
|
||||
<th>Endpoint</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="projects-body"></tbody>
|
||||
<tbody id="health-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-head">
|
||||
<h2>Artifact / Deploy Commit</h2>
|
||||
<p>Artifact identity fields required by the MVP DoD.</p>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Service</th>
|
||||
<th>Commit</th>
|
||||
<th>Image</th>
|
||||
<th>Digest</th>
|
||||
<th>Health timestamp</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="artifact-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="split">
|
||||
<section class="panel compact">
|
||||
<div class="panel-head">
|
||||
<h2>2 Box / 2 Gateway / Patch Panel</h2>
|
||||
<p>Topology contract for the hardware trusted loop.</p>
|
||||
</div>
|
||||
<div class="metrics two" id="topology-grid"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel compact">
|
||||
<div class="panel-head">
|
||||
<h2>Agent Session Evidence</h2>
|
||||
<p>Agent and worker session IDs tied to trace, audit, evidence, and cleanup.</p>
|
||||
</div>
|
||||
<div class="metrics two" id="agent-grid"></div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-head">
|
||||
<h2>Evidence Records</h2>
|
||||
<p>Direct and agent hardware operations remain fixture-bound until live DEV blockers clear.</p>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Evidence</th>
|
||||
<th>Operation</th>
|
||||
<th>Service</th>
|
||||
<th>URI</th>
|
||||
<th>SHA-256</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="evidence-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="split">
|
||||
<section class="panel compact danger">
|
||||
<div class="panel-head">
|
||||
<h2>Blocked Evidence</h2>
|
||||
<p>Open blocker evidence that prevents live DEV acceptance.</p>
|
||||
</div>
|
||||
<ul class="blockers" id="blocker-list"></ul>
|
||||
</section>
|
||||
|
||||
<section class="panel compact">
|
||||
<div class="panel-head">
|
||||
<h2>Operator Commands</h2>
|
||||
<p>Safe CLI entry points for manual acceptance.</p>
|
||||
</div>
|
||||
<ol class="commands" id="command-list"></ol>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,7 +5,7 @@ const rootDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".
|
||||
const distDir = path.resolve(rootDir, "dist");
|
||||
fs.mkdirSync(distDir, { recursive: true });
|
||||
|
||||
for (const file of ["index.html", "styles.css", "app.mjs", "runtime.mjs"]) {
|
||||
for (const file of ["index.html", "styles.css", "app.mjs", "gate-summary.mjs"]) {
|
||||
fs.copyFileSync(path.resolve(rootDir, file), path.resolve(distDir, file));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { loadMvpGateSummary } from "../../../internal/mvp-gate/summary.mjs";
|
||||
import { gateSummary } from "../gate-summary.mjs";
|
||||
|
||||
const rootDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..");
|
||||
const requiredFiles = ["index.html", "styles.css", "app.mjs", "runtime.mjs"];
|
||||
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const repoRoot = path.resolve(rootDir, "../..");
|
||||
const requiredFiles = ["index.html", "styles.css", "app.mjs", "gate-summary.mjs"];
|
||||
|
||||
for (const file of requiredFiles) {
|
||||
const filePath = path.resolve(rootDir, file);
|
||||
@@ -12,8 +17,27 @@ for (const file of requiredFiles) {
|
||||
}
|
||||
|
||||
const html = fs.readFileSync(path.resolve(rootDir, "index.html"), "utf8");
|
||||
if (!html.includes("HWLAB Cloud Web")) {
|
||||
throw new Error("index.html missing title");
|
||||
}
|
||||
assert.match(html, /HWLAB DEV MVP Gate/);
|
||||
assert.match(html, /health-body/);
|
||||
assert.match(html, /artifact-body/);
|
||||
assert.match(html, /agent-grid/);
|
||||
|
||||
console.log("hwlab-cloud-web check ok");
|
||||
const sourceSummary = loadMvpGateSummary(repoRoot);
|
||||
assert.equal(gateSummary.endpoint, sourceSummary.endpoint);
|
||||
assert.equal(gateSummary.gateStatus, sourceSummary.gateStatus);
|
||||
assert.equal(gateSummary.milestones.length, 6);
|
||||
assert.deepEqual(
|
||||
gateSummary.milestones.map((item) => item.id),
|
||||
["M0", "M1", "M2", "M3", "M4", "M5"]
|
||||
);
|
||||
assert.equal(gateSummary.artifactCount, 13);
|
||||
assert.equal(gateSummary.healthCount, 14);
|
||||
assert.equal(gateSummary.gatewaySessionCount, 2);
|
||||
assert.equal(gateSummary.boxResourceCount, 2);
|
||||
assert.equal(gateSummary.topology.patchPanel.serviceId, "hwlab-patch-panel");
|
||||
assert.equal(gateSummary.agent.agentServiceId, "hwlab-agent-mgr");
|
||||
assert.equal(gateSummary.agent.workerServiceId, "hwlab-agent-worker");
|
||||
assert.equal(gateSummary.evidenceRecords.length, 2);
|
||||
assert.equal(gateSummary.safety.allowNetwork, false);
|
||||
|
||||
console.log("hwlab-cloud-web check ok: gate summary reads M0-M5 report");
|
||||
|
||||
+222
-51
@@ -1,13 +1,18 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--bg: #0d1117;
|
||||
--surface: #161b22;
|
||||
--surface-2: #1f2630;
|
||||
--text: #e6edf3;
|
||||
--muted: #9aa4b2;
|
||||
--accent: #58a6ff;
|
||||
--border: #30363d;
|
||||
--good: #3fb950;
|
||||
--bg: #101214;
|
||||
--surface: #171a1f;
|
||||
--surface-2: #20242b;
|
||||
--surface-3: #252a32;
|
||||
--text: #edf0e8;
|
||||
--muted: #a9b0aa;
|
||||
--line: #373d43;
|
||||
--accent: #f0c75e;
|
||||
--ok: #7fc97a;
|
||||
--warn: #f0a35e;
|
||||
--bad: #ff6b5f;
|
||||
--mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
||||
--body: "Aptos", "Segoe UI", system-ui, sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -20,40 +25,41 @@ body {
|
||||
min-height: 100%;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font: 14px/1.5 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
font: 13px/1.45 var(--body);
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 24px;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.shell {
|
||||
max-width: 1160px;
|
||||
max-width: 1380px;
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.topbar,
|
||||
.panel {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.topbar {
|
||||
min-height: 74px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
padding: 16px 18px;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 4px;
|
||||
margin: 0 0 2px;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1,
|
||||
@@ -61,94 +67,259 @@ h2,
|
||||
p,
|
||||
dl,
|
||||
ol,
|
||||
ul,
|
||||
table {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
font-size: 24px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 15px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface-2);
|
||||
color: var(--good);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.panel {
|
||||
padding: 16px 18px;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.panel.compact {
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.panel-head {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
margin-bottom: 16px;
|
||||
gap: 3px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.panel-head p {
|
||||
color: var(--muted);
|
||||
max-width: 880px;
|
||||
}
|
||||
|
||||
.grid {
|
||||
.split {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px 18px;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.grid dt {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.grid dd {
|
||||
margin: 2px 0 0;
|
||||
font-weight: 600;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.steps {
|
||||
.metrics {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.steps li {
|
||||
padding-left: 8px;
|
||||
.metrics.six {
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.metrics.two {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.metric,
|
||||
.milestone {
|
||||
min-width: 0;
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--line);
|
||||
padding: 9px 10px;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
display: block;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
font-size: 13px;
|
||||
line-height: 1.25;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.milestones {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.milestone {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
min-height: 118px;
|
||||
}
|
||||
|
||||
.milestone-head,
|
||||
.blocker-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.milestone p,
|
||||
.blockers p {
|
||||
color: var(--text);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.milestone small {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
min-height: 24px;
|
||||
padding: 3px 8px;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--surface-3);
|
||||
color: var(--text);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.tone-pass,
|
||||
.tone-healthy {
|
||||
color: var(--ok);
|
||||
}
|
||||
|
||||
.tone-blocked,
|
||||
.tone-network_blocker {
|
||||
color: var(--bad);
|
||||
}
|
||||
|
||||
.tone-not_run {
|
||||
color: var(--warn);
|
||||
}
|
||||
|
||||
.table-wrap {
|
||||
overflow-x: auto;
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
min-width: 900px;
|
||||
border-collapse: collapse;
|
||||
background: var(--surface-2);
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
text-align: left;
|
||||
padding: 10px 8px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
vertical-align: top;
|
||||
padding: 8px 9px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
th {
|
||||
color: var(--muted);
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
td {
|
||||
max-width: 360px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
tbody tr:last-child td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.danger {
|
||||
border-color: color-mix(in srgb, var(--bad) 45%, var(--line));
|
||||
}
|
||||
|
||||
.blockers,
|
||||
.commands {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.blockers li,
|
||||
.commands li {
|
||||
min-width: 0;
|
||||
padding: 9px 10px;
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--line);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.commands {
|
||||
counter-reset: command;
|
||||
}
|
||||
|
||||
.commands li {
|
||||
counter-increment: command;
|
||||
}
|
||||
|
||||
.commands li::before {
|
||||
content: counter(command) ". ";
|
||||
color: var(--accent);
|
||||
font-family: var(--body);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@media (max-width: 1120px) {
|
||||
.metrics.six,
|
||||
.milestones {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
body {
|
||||
padding: 14px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.topbar,
|
||||
.split {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
display: grid;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.grid {
|
||||
.status {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.metrics.six,
|
||||
.metrics.two,
|
||||
.milestones {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user