Implement configuration, runtime, frontend, and task governance closure
This commit is contained in:
108
web/app/app.mjs
Normal file
108
web/app/app.mjs
Normal file
@@ -0,0 +1,108 @@
|
||||
import { WorkerClient } from "./worker-client.mjs";
|
||||
import { CommandController } from "./controls.mjs";
|
||||
import { EMPTY_STATE, reduceWorkerState, renderStatePanel } from "./state-panel.mjs";
|
||||
import { ProjectWorkflow } from "./project-workflow.mjs";
|
||||
import { renderRecoveryPanel } from "./recovery-panel.mjs";
|
||||
import { renderDevicePanel } from "./device-panel.mjs";
|
||||
import { renderHalPanel } from "./hal-panel.mjs";
|
||||
import { renderMachineView } from "./machine-view.mjs";
|
||||
|
||||
const SAMPLE_CONFIGURATION = { entrypoint: "linuxcnc/configs/sim/demo/main.ini", availablePaths: ["linuxcnc/configs/sim/demo/main.ini"], rows: [] };
|
||||
const SAMPLE_HAL = { manifest: [{ component: "demo" }], lines: ["loadrt demo names=d0", "addf d0 servo-thread"] };
|
||||
|
||||
function element(document, tag, className, text = "") {
|
||||
const node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (text) node.textContent = text;
|
||||
return node;
|
||||
}
|
||||
|
||||
function statusLine(document) {
|
||||
const node = element(document, "p", "status-line", "Disconnected");
|
||||
node.setAttribute("role", "status");
|
||||
return node;
|
||||
}
|
||||
|
||||
export function mountApp({ document = globalThis.document, client = null } = {}) {
|
||||
if (!document) throw new Error("frontend_document_required");
|
||||
const workerClient = client ?? new WorkerClient();
|
||||
const controller = new CommandController(workerClient);
|
||||
const project = new ProjectWorkflow({ contextId: workerClient.contextId, client: workerClient });
|
||||
let workerState = { ...EMPTY_STATE };
|
||||
const root = document.querySelector("#app") ?? document.body;
|
||||
root.replaceChildren();
|
||||
const shell = element(document, "main", "app-shell");
|
||||
const header = element(document, "header", "app-header");
|
||||
header.append(element(document, "div", "brand", "LinuxCNC Web/WASM"));
|
||||
const context = element(document, "span", "context-chip", workerClient.contextId);
|
||||
header.append(context);
|
||||
shell.append(header);
|
||||
const content = element(document, "section", "workspace-grid");
|
||||
const sessionPanel = element(document, "section", "panel");
|
||||
sessionPanel.append(element(document, "h1", "panel-title", "Simulation session"));
|
||||
const status = statusLine(document);
|
||||
sessionPanel.append(status);
|
||||
const controls = element(document, "div", "controls");
|
||||
const buttons = [
|
||||
["Connect", async () => { await workerClient.hello(); await workerClient.attach(); status.textContent = "Session ready"; }],
|
||||
["Load sample", async () => { await workerClient.loadWorkspace("/workspace", SAMPLE_CONFIGURATION, SAMPLE_HAL); status.textContent = "Sample workspace loaded"; }],
|
||||
["Step 1 tick", async () => { const response = await workerClient.step(1); status.textContent = `Tick ${response.simulation_tick}`; }],
|
||||
["Snapshot", async () => { const response = await workerClient.snapshot(); status.textContent = `Snapshot tick ${response.payload.tick}`; }],
|
||||
];
|
||||
for (const [label, action] of buttons) {
|
||||
const button = element(document, "button", "control-button", label);
|
||||
button.type = "button";
|
||||
button.addEventListener("click", () => Promise.resolve(action()).catch((error) => {
|
||||
status.textContent = `Error: ${error.response?.payload?.code ?? error.message}`;
|
||||
}));
|
||||
controls.append(button);
|
||||
}
|
||||
sessionPanel.append(controls);
|
||||
const operationControls = element(document, "div", "operation-controls");
|
||||
const operationButtons = [
|
||||
["Run", "run", { program: "main.ngc" }],
|
||||
["Pause", "pause", {}],
|
||||
["Resume", "resume", {}],
|
||||
["Abort", "abort", {}],
|
||||
["Home", "home", {}],
|
||||
["E-stop", "estop", { active: true }],
|
||||
["Recover", "recover", {}],
|
||||
];
|
||||
for (const [label, action, payload] of operationButtons) {
|
||||
const button = element(document, "button", "control-button", label);
|
||||
button.type = "button";
|
||||
button.addEventListener("click", () => controller.execute(action, payload).catch((error) => {
|
||||
status.textContent = `Error: ${error.response?.payload?.code ?? error.message}`;
|
||||
}));
|
||||
operationControls.append(button);
|
||||
}
|
||||
sessionPanel.append(operationControls);
|
||||
content.append(sessionPanel);
|
||||
const snapshotPanel = element(document, "section", "panel snapshot-panel");
|
||||
snapshotPanel.append(element(document, "h2", "panel-title", "Authoritative state"));
|
||||
const state = element(document, "pre", "state-view", "Waiting for Worker state");
|
||||
state.setAttribute("aria-live", "polite");
|
||||
snapshotPanel.append(state);
|
||||
content.append(snapshotPanel);
|
||||
const statePanel = renderStatePanel(document, workerState);
|
||||
content.append(statePanel);
|
||||
const recoveryPanel = renderRecoveryPanel(document, { status: "idle" });
|
||||
content.append(recoveryPanel);
|
||||
content.append(renderDevicePanel(document, { controller, snapshot: {} }));
|
||||
content.append(renderHalPanel(document, { components: 145, blocked: [] }, []));
|
||||
content.append(renderMachineView(document, { pose: { x: 0, y: 0, z: 0 }, stateHash: null }));
|
||||
shell.append(content);
|
||||
root.append(shell);
|
||||
workerClient.onMessage((message) => {
|
||||
workerState = reduceWorkerState(workerState, message);
|
||||
const replacement = renderStatePanel(document, workerState);
|
||||
statePanel.replaceChildren(...replacement.childNodes);
|
||||
if (message.kind === "CAPABILITIES" || message.kind === "SESSION_READY" || message.kind === "STATE_SNAPSHOT" || message.kind === "COMMAND_RESULT") {
|
||||
state.textContent = JSON.stringify(message.payload, null, 2);
|
||||
}
|
||||
if (message.kind === "ERROR") status.textContent = `Error: ${message.payload?.code ?? "worker_error"}`;
|
||||
});
|
||||
return { client: workerClient, controller, project, root: shell, status, state, recoveryPanel, get workerState() { return workerState; } };
|
||||
}
|
||||
|
||||
if (typeof document !== "undefined" && document.querySelector("#app")) mountApp();
|
||||
Reference in New Issue
Block a user