From 0ae76c2414ef917d8692ab1d29e2b2e137c2bb83 Mon Sep 17 00:00:00 2001 From: Snider Date: Sat, 14 Mar 2026 12:25:06 +0000 Subject: [PATCH] feat(ui): add Lit custom elements for process management Five Lit custom elements following the go-scm UI pattern: - tabbed container (Daemons/Processes/Pipelines) - daemon registry with health checks and stop - managed processes with status badges - live stdout/stderr WS stream viewer - pipeline execution results display Also adds provider.Renderable interface to ProcessProvider with Element() returning core-process-panel tag, extends WS channels with process-level events, and embeds the built UI bundle via go:embed. Co-Authored-By: Virgil --- .gitignore | 1 + docs/plans/2026-03-14-process-ui-elements.md | 163 ++ go.sum | 3 +- pkg/api/embed.go | 11 + pkg/api/provider.go | 16 +- pkg/api/ui/dist/core-process.js | 1958 ++++++++++++++++++ ui/index.html | 79 + ui/package-lock.json | 1213 +++++++++++ ui/package.json | 18 + ui/src/index.ts | 11 + ui/src/process-daemons.ts | 334 +++ ui/src/process-list.ts | 301 +++ ui/src/process-output.ts | 252 +++ ui/src/process-panel.ts | 275 +++ ui/src/process-runner.ts | 325 +++ ui/src/shared/api.ts | 105 + ui/src/shared/events.ts | 32 + ui/tsconfig.json | 17 + ui/vite.config.ts | 20 + 19 files changed, 5131 insertions(+), 3 deletions(-) create mode 100644 .gitignore create mode 100644 docs/plans/2026-03-14-process-ui-elements.md create mode 100644 pkg/api/embed.go create mode 100644 pkg/api/ui/dist/core-process.js create mode 100644 ui/index.html create mode 100644 ui/package-lock.json create mode 100644 ui/package.json create mode 100644 ui/src/index.ts create mode 100644 ui/src/process-daemons.ts create mode 100644 ui/src/process-list.ts create mode 100644 ui/src/process-output.ts create mode 100644 ui/src/process-panel.ts create mode 100644 ui/src/process-runner.ts create mode 100644 ui/src/shared/api.ts create mode 100644 ui/src/shared/events.ts create mode 100644 ui/tsconfig.json create mode 100644 ui/vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..761c02a --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +ui/node_modules/ diff --git a/docs/plans/2026-03-14-process-ui-elements.md b/docs/plans/2026-03-14-process-ui-elements.md new file mode 100644 index 0000000..10c0c72 --- /dev/null +++ b/docs/plans/2026-03-14-process-ui-elements.md @@ -0,0 +1,163 @@ +# Process UI Elements — Lit Custom Elements + +**Date**: 2026-03-14 +**Status**: Complete + +## Overview + +Add Lit custom elements for process management, following the go-scm UI pattern exactly. The UI provides a tabbed panel with views for daemons, processes, process output, and pipeline runner status. + +## 1. Review Existing ProcessProvider + +The ProcessProvider (`pkg/api/provider.go`) already has: + +- **REST endpoints**: + - `GET /api/process/daemons` — List running daemons (auto-prunes dead PIDs) + - `GET /api/process/daemons/:code/:daemon` — Get single daemon status + - `POST /api/process/daemons/:code/:daemon/stop` — Stop daemon (SIGTERM + unregister) + - `GET /api/process/daemons/:code/:daemon/health` — Health check probe + +- **WS channels** (via `provider.Streamable`): + - `process.daemon.started` + - `process.daemon.stopped` + - `process.daemon.health` + +- **Implements**: `provider.Provider`, `provider.Streamable`, `provider.Describable` + +- **Missing**: `provider.Renderable` — needs `Element()` method returning `ElementSpec{Tag, Source}` + +- **Missing channels**: Process-level events (`process.output`, `process.started`, `process.exited`, `process.killed`) — the Go IPC actions exist but are not declared as WS channels + +### Changes needed to provider.go + +1. Add `provider.Renderable` interface compliance +2. Add `Element()` method returning `{Tag: "core-process-panel", Source: "/assets/core-process.js"}` +3. Add process-level WS channels to `Channels()` + +## 2. Create ui/ directory + +Scaffold the same structure as go-scm/ui: + +``` +ui/ +├── package.json # @core/process-ui, lit dep +├── tsconfig.json # ES2021 + decorators +├── vite.config.ts # lib mode → ../pkg/api/ui/dist/core-process.js +├── index.html # Demo page +└── src/ + ├── index.ts # Bundle entry, exports all elements + ├── process-panel.ts # — top-level tabbed panel + ├── process-daemons.ts # — daemon registry list + ├── process-list.ts # — running processes + ├── process-output.ts # — live stdout/stderr stream + ├── process-runner.ts # — pipeline run results + └── shared/ + ├── api.ts # ProcessApi fetch wrapper + └── events.ts # WS event filter for process.* events +``` + +## 3. Lit Elements + +### 3.1 shared/api.ts — ProcessApi + +Typed fetch wrapper for `/api/process/*` endpoints: + +- `listDaemons()` → `GET /daemons` +- `getDaemon(code, daemon)` → `GET /daemons/:code/:daemon` +- `stopDaemon(code, daemon)` → `POST /daemons/:code/:daemon/stop` +- `healthCheck(code, daemon)` → `GET /daemons/:code/:daemon/health` + +### 3.2 shared/events.ts — connectProcessEvents + +WebSocket event filter for `process.*` channels. Same pattern as go-scm `connectScmEvents`. + +### 3.3 process-panel.ts — `` + +Top-level panel with tabs: Daemons / Processes / Pipelines. HLCRF layout: +- H: Title bar ("Process Manager") + Refresh button +- H-L: Tab navigation +- C: Active tab content +- F: WS connection status + last event + +### 3.4 process-daemons.ts — `` + +Daemon registry list showing: +- Code + Daemon name +- PID + Started timestamp +- Health status badge (healthy/unhealthy/no endpoint) +- Project + Binary metadata +- Stop button (POST stop endpoint) +- Health check button (GET health endpoint) + +### 3.5 process-list.ts — `` + +Running processes from Service.List(): +- Process ID + Command + Args +- Status badge (pending/running/exited/failed/killed) +- PID + uptime (since StartedAt) +- Exit code (if exited) +- Kill button (for running processes) +- Click to select → emits `process-selected` event for output viewer + +Note: This element requires process-level REST endpoints that do not exist in the current provider. The element will be built but will show placeholder state until those endpoints are added. + +### 3.6 process-output.ts — `` + +Live stdout/stderr stream for a selected process: +- Receives process ID via attribute +- Subscribes to `process.output` WS events filtered by ID +- Terminal-style output area with monospace font +- Auto-scroll to bottom +- Stream type indicator (stdout/stderr with colour coding) + +### 3.7 process-runner.ts — `` + +Pipeline execution results display: +- RunSpec list with name, command, status +- Dependency chain visualisation (After field) +- Pass/Fail/Skip badges +- Duration display +- Aggregate summary (passed/failed/skipped counts) + +Note: Like process-list, this needs runner REST endpoints. Built as display-ready element. + +## 4. Create pkg/api/embed.go + +```go +//go:embed all:ui/dist +var Assets embed.FS +``` + +Same pattern as go-scm. The `ui/dist/` directory must exist (created by `npm run build` in ui/). + +## 5. Update ProcessProvider + +Add to `provider.go`: + +1. Compile-time check: `_ provider.Renderable = (*ProcessProvider)(nil)` +2. `Element()` method +3. Extend `Channels()` with process-level events + +## 6. Build Verification + +1. `cd ui && npm install && npm run build` — produces `pkg/api/ui/dist/core-process.js` +2. `go build ./...` from go-process root — verifies embed and provider compile + +## Tasks + +- [x] Write implementation plan +- [x] Create ui/package.json, tsconfig.json, vite.config.ts +- [x] Create ui/index.html demo page +- [x] Create ui/src/shared/api.ts +- [x] Create ui/src/shared/events.ts +- [x] Create ui/src/process-panel.ts +- [x] Create ui/src/process-daemons.ts +- [x] Create ui/src/process-list.ts +- [x] Create ui/src/process-output.ts +- [x] Create ui/src/process-runner.ts +- [x] Create ui/src/index.ts +- [x] Create pkg/api/ui/dist/.gitkeep (placeholder for embed) +- [x] Create pkg/api/embed.go +- [x] Update pkg/api/provider.go — add Renderable + channels +- [x] Build UI: npm install + npm run build (57.50 kB, gzip: 13.64 kB) +- [x] Verify: go build ./... (pass) + go test ./pkg/api/... (8/8 pass) diff --git a/go.sum b/go.sum index f9fdb4e..ac63e7a 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,5 @@ +forge.lthn.ai/core/api v0.1.0 h1:ZKnQx+L9vxLQSEjwpsD1eNcIQrE4YKV1c2AlMtseM6o= forge.lthn.ai/core/go v0.1.0 h1:Ow/1NTajrrNPO0zgkskEyEGdx4SKpiNqTaqM0txNOYI= -forge.lthn.ai/core/go-api v0.1.2 h1:zGmU2CqCQ0n0cntNvprdc7HoucD4E631wBdZw+taK1w= -forge.lthn.ai/core/go-api v0.1.2/go.mod h1:/MGxVudBh3k4K2hOkAC74HMVWssPz4pPNQTbGyiBUDs= forge.lthn.ai/core/go-ws v0.1.3 h1:TzqFpEcDYcZUFFmrTznfEuVcVdnp2jsRNwAGHeTyXN0= forge.lthn.ai/core/go-ws v0.1.3/go.mod h1:iDbJuR1NT27czjtNIluxnEdLrnfsYQdEBIrsoZnpkCk= github.com/99designs/gqlgen v0.17.87 h1:pSnCIMhBQezAE8bc1GNmfdLXFmnWtWl1GRDFEE/nHP8= diff --git a/pkg/api/embed.go b/pkg/api/embed.go new file mode 100644 index 0000000..aaf19b1 --- /dev/null +++ b/pkg/api/embed.go @@ -0,0 +1,11 @@ +// SPDX-Licence-Identifier: EUPL-1.2 + +package api + +import "embed" + +// Assets holds the built UI bundle (core-process.js and related files). +// The directory is populated by running `npm run build` in the ui/ directory. +// +//go:embed all:ui/dist +var Assets embed.FS diff --git a/pkg/api/provider.go b/pkg/api/provider.go index 9ec4413..e7f35f2 100644 --- a/pkg/api/provider.go +++ b/pkg/api/provider.go @@ -18,7 +18,8 @@ import ( ) // ProcessProvider wraps the go-process daemon Registry as a service provider. -// It implements provider.Provider, provider.Streamable, and provider.Describable. +// It implements provider.Provider, provider.Streamable, provider.Describable, +// and provider.Renderable. type ProcessProvider struct { registry *process.Registry hub *ws.Hub @@ -29,6 +30,7 @@ var ( _ provider.Provider = (*ProcessProvider)(nil) _ provider.Streamable = (*ProcessProvider)(nil) _ provider.Describable = (*ProcessProvider)(nil) + _ provider.Renderable = (*ProcessProvider)(nil) ) // NewProvider creates a process provider backed by the given daemon registry. @@ -50,12 +52,24 @@ func (p *ProcessProvider) Name() string { return "process" } // BasePath implements api.RouteGroup. func (p *ProcessProvider) BasePath() string { return "/api/process" } +// Element implements provider.Renderable. +func (p *ProcessProvider) Element() provider.ElementSpec { + return provider.ElementSpec{ + Tag: "core-process-panel", + Source: "/assets/core-process.js", + } +} + // Channels implements provider.Streamable. func (p *ProcessProvider) Channels() []string { return []string{ "process.daemon.started", "process.daemon.stopped", "process.daemon.health", + "process.started", + "process.output", + "process.exited", + "process.killed", } } diff --git a/pkg/api/ui/dist/core-process.js b/pkg/api/ui/dist/core-process.js new file mode 100644 index 0000000..3d499ec --- /dev/null +++ b/pkg/api/ui/dist/core-process.js @@ -0,0 +1,1958 @@ +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +const V = globalThis, se = V.ShadowRoot && (V.ShadyCSS === void 0 || V.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype, re = Symbol(), ne = /* @__PURE__ */ new WeakMap(); +let $e = class { + constructor(e, t, i) { + if (this._$cssResult$ = !0, i !== re) throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead."); + this.cssText = e, this.t = t; + } + get styleSheet() { + let e = this.o; + const t = this.t; + if (se && e === void 0) { + const i = t !== void 0 && t.length === 1; + i && (e = ne.get(t)), e === void 0 && ((this.o = e = new CSSStyleSheet()).replaceSync(this.cssText), i && ne.set(t, e)); + } + return e; + } + toString() { + return this.cssText; + } +}; +const Ae = (s) => new $e(typeof s == "string" ? s : s + "", void 0, re), B = (s, ...e) => { + const t = s.length === 1 ? s[0] : e.reduce((i, r, n) => i + ((o) => { + if (o._$cssResult$ === !0) return o.cssText; + if (typeof o == "number") return o; + throw Error("Value passed to 'css' function must be a 'css' function result: " + o + ". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security."); + })(r) + s[n + 1], s[0]); + return new $e(t, s, re); +}, ke = (s, e) => { + if (se) s.adoptedStyleSheets = e.map((t) => t instanceof CSSStyleSheet ? t : t.styleSheet); + else for (const t of e) { + const i = document.createElement("style"), r = V.litNonce; + r !== void 0 && i.setAttribute("nonce", r), i.textContent = t.cssText, s.appendChild(i); + } +}, ae = se ? (s) => s : (s) => s instanceof CSSStyleSheet ? ((e) => { + let t = ""; + for (const i of e.cssRules) t += i.cssText; + return Ae(t); +})(s) : s; +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +const { is: Se, defineProperty: Pe, getOwnPropertyDescriptor: Ce, getOwnPropertyNames: Ee, getOwnPropertySymbols: Ue, getPrototypeOf: Oe } = Object, A = globalThis, le = A.trustedTypes, ze = le ? le.emptyScript : "", X = A.reactiveElementPolyfillSupport, j = (s, e) => s, J = { toAttribute(s, e) { + switch (e) { + case Boolean: + s = s ? ze : null; + break; + case Object: + case Array: + s = s == null ? s : JSON.stringify(s); + } + return s; +}, fromAttribute(s, e) { + let t = s; + switch (e) { + case Boolean: + t = s !== null; + break; + case Number: + t = s === null ? null : Number(s); + break; + case Object: + case Array: + try { + t = JSON.parse(s); + } catch { + t = null; + } + } + return t; +} }, ie = (s, e) => !Se(s, e), ce = { attribute: !0, type: String, converter: J, reflect: !1, useDefault: !1, hasChanged: ie }; +Symbol.metadata ?? (Symbol.metadata = Symbol("metadata")), A.litPropertyMetadata ?? (A.litPropertyMetadata = /* @__PURE__ */ new WeakMap()); +let D = class extends HTMLElement { + static addInitializer(e) { + this._$Ei(), (this.l ?? (this.l = [])).push(e); + } + static get observedAttributes() { + return this.finalize(), this._$Eh && [...this._$Eh.keys()]; + } + static createProperty(e, t = ce) { + if (t.state && (t.attribute = !1), this._$Ei(), this.prototype.hasOwnProperty(e) && ((t = Object.create(t)).wrapped = !0), this.elementProperties.set(e, t), !t.noAccessor) { + const i = Symbol(), r = this.getPropertyDescriptor(e, i, t); + r !== void 0 && Pe(this.prototype, e, r); + } + } + static getPropertyDescriptor(e, t, i) { + const { get: r, set: n } = Ce(this.prototype, e) ?? { get() { + return this[t]; + }, set(o) { + this[t] = o; + } }; + return { get: r, set(o) { + const l = r == null ? void 0 : r.call(this); + n == null || n.call(this, o), this.requestUpdate(e, l, i); + }, configurable: !0, enumerable: !0 }; + } + static getPropertyOptions(e) { + return this.elementProperties.get(e) ?? ce; + } + static _$Ei() { + if (this.hasOwnProperty(j("elementProperties"))) return; + const e = Oe(this); + e.finalize(), e.l !== void 0 && (this.l = [...e.l]), this.elementProperties = new Map(e.elementProperties); + } + static finalize() { + if (this.hasOwnProperty(j("finalized"))) return; + if (this.finalized = !0, this._$Ei(), this.hasOwnProperty(j("properties"))) { + const t = this.properties, i = [...Ee(t), ...Ue(t)]; + for (const r of i) this.createProperty(r, t[r]); + } + const e = this[Symbol.metadata]; + if (e !== null) { + const t = litPropertyMetadata.get(e); + if (t !== void 0) for (const [i, r] of t) this.elementProperties.set(i, r); + } + this._$Eh = /* @__PURE__ */ new Map(); + for (const [t, i] of this.elementProperties) { + const r = this._$Eu(t, i); + r !== void 0 && this._$Eh.set(r, t); + } + this.elementStyles = this.finalizeStyles(this.styles); + } + static finalizeStyles(e) { + const t = []; + if (Array.isArray(e)) { + const i = new Set(e.flat(1 / 0).reverse()); + for (const r of i) t.unshift(ae(r)); + } else e !== void 0 && t.push(ae(e)); + return t; + } + static _$Eu(e, t) { + const i = t.attribute; + return i === !1 ? void 0 : typeof i == "string" ? i : typeof e == "string" ? e.toLowerCase() : void 0; + } + constructor() { + super(), this._$Ep = void 0, this.isUpdatePending = !1, this.hasUpdated = !1, this._$Em = null, this._$Ev(); + } + _$Ev() { + var e; + this._$ES = new Promise((t) => this.enableUpdating = t), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), (e = this.constructor.l) == null || e.forEach((t) => t(this)); + } + addController(e) { + var t; + (this._$EO ?? (this._$EO = /* @__PURE__ */ new Set())).add(e), this.renderRoot !== void 0 && this.isConnected && ((t = e.hostConnected) == null || t.call(e)); + } + removeController(e) { + var t; + (t = this._$EO) == null || t.delete(e); + } + _$E_() { + const e = /* @__PURE__ */ new Map(), t = this.constructor.elementProperties; + for (const i of t.keys()) this.hasOwnProperty(i) && (e.set(i, this[i]), delete this[i]); + e.size > 0 && (this._$Ep = e); + } + createRenderRoot() { + const e = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions); + return ke(e, this.constructor.elementStyles), e; + } + connectedCallback() { + var e; + this.renderRoot ?? (this.renderRoot = this.createRenderRoot()), this.enableUpdating(!0), (e = this._$EO) == null || e.forEach((t) => { + var i; + return (i = t.hostConnected) == null ? void 0 : i.call(t); + }); + } + enableUpdating(e) { + } + disconnectedCallback() { + var e; + (e = this._$EO) == null || e.forEach((t) => { + var i; + return (i = t.hostDisconnected) == null ? void 0 : i.call(t); + }); + } + attributeChangedCallback(e, t, i) { + this._$AK(e, i); + } + _$ET(e, t) { + var n; + const i = this.constructor.elementProperties.get(e), r = this.constructor._$Eu(e, i); + if (r !== void 0 && i.reflect === !0) { + const o = (((n = i.converter) == null ? void 0 : n.toAttribute) !== void 0 ? i.converter : J).toAttribute(t, i.type); + this._$Em = e, o == null ? this.removeAttribute(r) : this.setAttribute(r, o), this._$Em = null; + } + } + _$AK(e, t) { + var n, o; + const i = this.constructor, r = i._$Eh.get(e); + if (r !== void 0 && this._$Em !== r) { + const l = i.getPropertyOptions(r), a = typeof l.converter == "function" ? { fromAttribute: l.converter } : ((n = l.converter) == null ? void 0 : n.fromAttribute) !== void 0 ? l.converter : J; + this._$Em = r; + const p = a.fromAttribute(t, l.type); + this[r] = p ?? ((o = this._$Ej) == null ? void 0 : o.get(r)) ?? p, this._$Em = null; + } + } + requestUpdate(e, t, i, r = !1, n) { + var o; + if (e !== void 0) { + const l = this.constructor; + if (r === !1 && (n = this[e]), i ?? (i = l.getPropertyOptions(e)), !((i.hasChanged ?? ie)(n, t) || i.useDefault && i.reflect && n === ((o = this._$Ej) == null ? void 0 : o.get(e)) && !this.hasAttribute(l._$Eu(e, i)))) return; + this.C(e, t, i); + } + this.isUpdatePending === !1 && (this._$ES = this._$EP()); + } + C(e, t, { useDefault: i, reflect: r, wrapped: n }, o) { + i && !(this._$Ej ?? (this._$Ej = /* @__PURE__ */ new Map())).has(e) && (this._$Ej.set(e, o ?? t ?? this[e]), n !== !0 || o !== void 0) || (this._$AL.has(e) || (this.hasUpdated || i || (t = void 0), this._$AL.set(e, t)), r === !0 && this._$Em !== e && (this._$Eq ?? (this._$Eq = /* @__PURE__ */ new Set())).add(e)); + } + async _$EP() { + this.isUpdatePending = !0; + try { + await this._$ES; + } catch (t) { + Promise.reject(t); + } + const e = this.scheduleUpdate(); + return e != null && await e, !this.isUpdatePending; + } + scheduleUpdate() { + return this.performUpdate(); + } + performUpdate() { + var i; + if (!this.isUpdatePending) return; + if (!this.hasUpdated) { + if (this.renderRoot ?? (this.renderRoot = this.createRenderRoot()), this._$Ep) { + for (const [n, o] of this._$Ep) this[n] = o; + this._$Ep = void 0; + } + const r = this.constructor.elementProperties; + if (r.size > 0) for (const [n, o] of r) { + const { wrapped: l } = o, a = this[n]; + l !== !0 || this._$AL.has(n) || a === void 0 || this.C(n, void 0, o, a); + } + } + let e = !1; + const t = this._$AL; + try { + e = this.shouldUpdate(t), e ? (this.willUpdate(t), (i = this._$EO) == null || i.forEach((r) => { + var n; + return (n = r.hostUpdate) == null ? void 0 : n.call(r); + }), this.update(t)) : this._$EM(); + } catch (r) { + throw e = !1, this._$EM(), r; + } + e && this._$AE(t); + } + willUpdate(e) { + } + _$AE(e) { + var t; + (t = this._$EO) == null || t.forEach((i) => { + var r; + return (r = i.hostUpdated) == null ? void 0 : r.call(i); + }), this.hasUpdated || (this.hasUpdated = !0, this.firstUpdated(e)), this.updated(e); + } + _$EM() { + this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = !1; + } + get updateComplete() { + return this.getUpdateComplete(); + } + getUpdateComplete() { + return this._$ES; + } + shouldUpdate(e) { + return !0; + } + update(e) { + this._$Eq && (this._$Eq = this._$Eq.forEach((t) => this._$ET(t, this[t]))), this._$EM(); + } + updated(e) { + } + firstUpdated(e) { + } +}; +D.elementStyles = [], D.shadowRootOptions = { mode: "open" }, D[j("elementProperties")] = /* @__PURE__ */ new Map(), D[j("finalized")] = /* @__PURE__ */ new Map(), X == null || X({ ReactiveElement: D }), (A.reactiveElementVersions ?? (A.reactiveElementVersions = [])).push("2.1.2"); +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +const N = globalThis, de = (s) => s, Z = N.trustedTypes, he = Z ? Z.createPolicy("lit-html", { createHTML: (s) => s }) : void 0, ye = "$lit$", x = `lit$${Math.random().toFixed(9).slice(2)}$`, ve = "?" + x, De = `<${ve}>`, E = document, I = () => E.createComment(""), L = (s) => s === null || typeof s != "object" && typeof s != "function", oe = Array.isArray, Te = (s) => oe(s) || typeof (s == null ? void 0 : s[Symbol.iterator]) == "function", Y = `[ +\f\r]`, H = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, pe = /-->/g, ue = />/g, S = RegExp(`>|${Y}(?:([^\\s"'>=/]+)(${Y}*=${Y}*(?:[^ +\f\r"'\`<>=]|("|')|))|$)`, "g"), me = /'/g, fe = /"/g, _e = /^(?:script|style|textarea|title)$/i, Me = (s) => (e, ...t) => ({ _$litType$: s, strings: e, values: t }), c = Me(1), T = Symbol.for("lit-noChange"), d = Symbol.for("lit-nothing"), ge = /* @__PURE__ */ new WeakMap(), P = E.createTreeWalker(E, 129); +function we(s, e) { + if (!oe(s) || !s.hasOwnProperty("raw")) throw Error("invalid template strings array"); + return he !== void 0 ? he.createHTML(e) : e; +} +const Re = (s, e) => { + const t = s.length - 1, i = []; + let r, n = e === 2 ? "" : e === 3 ? "" : "", o = H; + for (let l = 0; l < t; l++) { + const a = s[l]; + let p, m, h = -1, b = 0; + for (; b < a.length && (o.lastIndex = b, m = o.exec(a), m !== null); ) b = o.lastIndex, o === H ? m[1] === "!--" ? o = pe : m[1] !== void 0 ? o = ue : m[2] !== void 0 ? (_e.test(m[2]) && (r = RegExp("" ? (o = r ?? H, h = -1) : m[1] === void 0 ? h = -2 : (h = o.lastIndex - m[2].length, p = m[1], o = m[3] === void 0 ? S : m[3] === '"' ? fe : me) : o === fe || o === me ? o = S : o === pe || o === ue ? o = H : (o = S, r = void 0); + const w = o === S && s[l + 1].startsWith("/>") ? " " : ""; + n += o === H ? a + De : h >= 0 ? (i.push(p), a.slice(0, h) + ye + a.slice(h) + x + w) : a + x + (h === -2 ? l : w); + } + return [we(s, n + (s[t] || "") + (e === 2 ? "" : e === 3 ? "" : "")), i]; +}; +class q { + constructor({ strings: e, _$litType$: t }, i) { + let r; + this.parts = []; + let n = 0, o = 0; + const l = e.length - 1, a = this.parts, [p, m] = Re(e, t); + if (this.el = q.createElement(p, i), P.currentNode = this.el.content, t === 2 || t === 3) { + const h = this.el.content.firstChild; + h.replaceWith(...h.childNodes); + } + for (; (r = P.nextNode()) !== null && a.length < l; ) { + if (r.nodeType === 1) { + if (r.hasAttributes()) for (const h of r.getAttributeNames()) if (h.endsWith(ye)) { + const b = m[o++], w = r.getAttribute(h).split(x), K = /([.?@])?(.*)/.exec(b); + a.push({ type: 1, index: n, name: K[2], strings: w, ctor: K[1] === "." ? je : K[1] === "?" ? Ne : K[1] === "@" ? Ie : G }), r.removeAttribute(h); + } else h.startsWith(x) && (a.push({ type: 6, index: n }), r.removeAttribute(h)); + if (_e.test(r.tagName)) { + const h = r.textContent.split(x), b = h.length - 1; + if (b > 0) { + r.textContent = Z ? Z.emptyScript : ""; + for (let w = 0; w < b; w++) r.append(h[w], I()), P.nextNode(), a.push({ type: 2, index: ++n }); + r.append(h[b], I()); + } + } + } else if (r.nodeType === 8) if (r.data === ve) a.push({ type: 2, index: n }); + else { + let h = -1; + for (; (h = r.data.indexOf(x, h + 1)) !== -1; ) a.push({ type: 7, index: n }), h += x.length - 1; + } + n++; + } + } + static createElement(e, t) { + const i = E.createElement("template"); + return i.innerHTML = e, i; + } +} +function M(s, e, t = s, i) { + var o, l; + if (e === T) return e; + let r = i !== void 0 ? (o = t._$Co) == null ? void 0 : o[i] : t._$Cl; + const n = L(e) ? void 0 : e._$litDirective$; + return (r == null ? void 0 : r.constructor) !== n && ((l = r == null ? void 0 : r._$AO) == null || l.call(r, !1), n === void 0 ? r = void 0 : (r = new n(s), r._$AT(s, t, i)), i !== void 0 ? (t._$Co ?? (t._$Co = []))[i] = r : t._$Cl = r), r !== void 0 && (e = M(s, r._$AS(s, e.values), r, i)), e; +} +class He { + constructor(e, t) { + this._$AV = [], this._$AN = void 0, this._$AD = e, this._$AM = t; + } + get parentNode() { + return this._$AM.parentNode; + } + get _$AU() { + return this._$AM._$AU; + } + u(e) { + const { el: { content: t }, parts: i } = this._$AD, r = ((e == null ? void 0 : e.creationScope) ?? E).importNode(t, !0); + P.currentNode = r; + let n = P.nextNode(), o = 0, l = 0, a = i[0]; + for (; a !== void 0; ) { + if (o === a.index) { + let p; + a.type === 2 ? p = new W(n, n.nextSibling, this, e) : a.type === 1 ? p = new a.ctor(n, a.name, a.strings, this, e) : a.type === 6 && (p = new Le(n, this, e)), this._$AV.push(p), a = i[++l]; + } + o !== (a == null ? void 0 : a.index) && (n = P.nextNode(), o++); + } + return P.currentNode = E, r; + } + p(e) { + let t = 0; + for (const i of this._$AV) i !== void 0 && (i.strings !== void 0 ? (i._$AI(e, i, t), t += i.strings.length - 2) : i._$AI(e[t])), t++; + } +} +class W { + get _$AU() { + var e; + return ((e = this._$AM) == null ? void 0 : e._$AU) ?? this._$Cv; + } + constructor(e, t, i, r) { + this.type = 2, this._$AH = d, this._$AN = void 0, this._$AA = e, this._$AB = t, this._$AM = i, this.options = r, this._$Cv = (r == null ? void 0 : r.isConnected) ?? !0; + } + get parentNode() { + let e = this._$AA.parentNode; + const t = this._$AM; + return t !== void 0 && (e == null ? void 0 : e.nodeType) === 11 && (e = t.parentNode), e; + } + get startNode() { + return this._$AA; + } + get endNode() { + return this._$AB; + } + _$AI(e, t = this) { + e = M(this, e, t), L(e) ? e === d || e == null || e === "" ? (this._$AH !== d && this._$AR(), this._$AH = d) : e !== this._$AH && e !== T && this._(e) : e._$litType$ !== void 0 ? this.$(e) : e.nodeType !== void 0 ? this.T(e) : Te(e) ? this.k(e) : this._(e); + } + O(e) { + return this._$AA.parentNode.insertBefore(e, this._$AB); + } + T(e) { + this._$AH !== e && (this._$AR(), this._$AH = this.O(e)); + } + _(e) { + this._$AH !== d && L(this._$AH) ? this._$AA.nextSibling.data = e : this.T(E.createTextNode(e)), this._$AH = e; + } + $(e) { + var n; + const { values: t, _$litType$: i } = e, r = typeof i == "number" ? this._$AC(e) : (i.el === void 0 && (i.el = q.createElement(we(i.h, i.h[0]), this.options)), i); + if (((n = this._$AH) == null ? void 0 : n._$AD) === r) this._$AH.p(t); + else { + const o = new He(r, this), l = o.u(this.options); + o.p(t), this.T(l), this._$AH = o; + } + } + _$AC(e) { + let t = ge.get(e.strings); + return t === void 0 && ge.set(e.strings, t = new q(e)), t; + } + k(e) { + oe(this._$AH) || (this._$AH = [], this._$AR()); + const t = this._$AH; + let i, r = 0; + for (const n of e) r === t.length ? t.push(i = new W(this.O(I()), this.O(I()), this, this.options)) : i = t[r], i._$AI(n), r++; + r < t.length && (this._$AR(i && i._$AB.nextSibling, r), t.length = r); + } + _$AR(e = this._$AA.nextSibling, t) { + var i; + for ((i = this._$AP) == null ? void 0 : i.call(this, !1, !0, t); e !== this._$AB; ) { + const r = de(e).nextSibling; + de(e).remove(), e = r; + } + } + setConnected(e) { + var t; + this._$AM === void 0 && (this._$Cv = e, (t = this._$AP) == null || t.call(this, e)); + } +} +class G { + get tagName() { + return this.element.tagName; + } + get _$AU() { + return this._$AM._$AU; + } + constructor(e, t, i, r, n) { + this.type = 1, this._$AH = d, this._$AN = void 0, this.element = e, this.name = t, this._$AM = r, this.options = n, i.length > 2 || i[0] !== "" || i[1] !== "" ? (this._$AH = Array(i.length - 1).fill(new String()), this.strings = i) : this._$AH = d; + } + _$AI(e, t = this, i, r) { + const n = this.strings; + let o = !1; + if (n === void 0) e = M(this, e, t, 0), o = !L(e) || e !== this._$AH && e !== T, o && (this._$AH = e); + else { + const l = e; + let a, p; + for (e = n[0], a = 0; a < n.length - 1; a++) p = M(this, l[i + a], t, a), p === T && (p = this._$AH[a]), o || (o = !L(p) || p !== this._$AH[a]), p === d ? e = d : e !== d && (e += (p ?? "") + n[a + 1]), this._$AH[a] = p; + } + o && !r && this.j(e); + } + j(e) { + e === d ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, e ?? ""); + } +} +class je extends G { + constructor() { + super(...arguments), this.type = 3; + } + j(e) { + this.element[this.name] = e === d ? void 0 : e; + } +} +class Ne extends G { + constructor() { + super(...arguments), this.type = 4; + } + j(e) { + this.element.toggleAttribute(this.name, !!e && e !== d); + } +} +class Ie extends G { + constructor(e, t, i, r, n) { + super(e, t, i, r, n), this.type = 5; + } + _$AI(e, t = this) { + if ((e = M(this, e, t, 0) ?? d) === T) return; + const i = this._$AH, r = e === d && i !== d || e.capture !== i.capture || e.once !== i.once || e.passive !== i.passive, n = e !== d && (i === d || r); + r && this.element.removeEventListener(this.name, this, i), n && this.element.addEventListener(this.name, this, e), this._$AH = e; + } + handleEvent(e) { + var t; + typeof this._$AH == "function" ? this._$AH.call(((t = this.options) == null ? void 0 : t.host) ?? this.element, e) : this._$AH.handleEvent(e); + } +} +class Le { + constructor(e, t, i) { + this.element = e, this.type = 6, this._$AN = void 0, this._$AM = t, this.options = i; + } + get _$AU() { + return this._$AM._$AU; + } + _$AI(e) { + M(this, e); + } +} +const ee = N.litHtmlPolyfillSupport; +ee == null || ee(q, W), (N.litHtmlVersions ?? (N.litHtmlVersions = [])).push("3.3.2"); +const qe = (s, e, t) => { + const i = (t == null ? void 0 : t.renderBefore) ?? e; + let r = i._$litPart$; + if (r === void 0) { + const n = (t == null ? void 0 : t.renderBefore) ?? null; + i._$litPart$ = r = new W(e.insertBefore(I(), n), n, void 0, t ?? {}); + } + return r._$AI(s), r; +}; +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +const C = globalThis; +class $ extends D { + constructor() { + super(...arguments), this.renderOptions = { host: this }, this._$Do = void 0; + } + createRenderRoot() { + var t; + const e = super.createRenderRoot(); + return (t = this.renderOptions).renderBefore ?? (t.renderBefore = e.firstChild), e; + } + update(e) { + const t = this.render(); + this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(e), this._$Do = qe(t, this.renderRoot, this.renderOptions); + } + connectedCallback() { + var e; + super.connectedCallback(), (e = this._$Do) == null || e.setConnected(!0); + } + disconnectedCallback() { + var e; + super.disconnectedCallback(), (e = this._$Do) == null || e.setConnected(!1); + } + render() { + return T; + } +} +var be; +$._$litElement$ = !0, $.finalized = !0, (be = C.litElementHydrateSupport) == null || be.call(C, { LitElement: $ }); +const te = C.litElementPolyfillSupport; +te == null || te({ LitElement: $ }); +(C.litElementVersions ?? (C.litElementVersions = [])).push("4.2.2"); +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +const F = (s) => (e, t) => { + t !== void 0 ? t.addInitializer(() => { + customElements.define(s, e); + }) : customElements.define(s, e); +}; +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +const Be = { attribute: !0, type: String, converter: J, reflect: !1, hasChanged: ie }, We = (s = Be, e, t) => { + const { kind: i, metadata: r } = t; + let n = globalThis.litPropertyMetadata.get(r); + if (n === void 0 && globalThis.litPropertyMetadata.set(r, n = /* @__PURE__ */ new Map()), i === "setter" && ((s = Object.create(s)).wrapped = !0), n.set(t.name, s), i === "accessor") { + const { name: o } = t; + return { set(l) { + const a = e.get.call(this); + e.set.call(this, l), this.requestUpdate(o, a, s, !0, l); + }, init(l) { + return l !== void 0 && this.C(o, void 0, s, l), l; + } }; + } + if (i === "setter") { + const { name: o } = t; + return function(l) { + const a = this[o]; + e.call(this, l), this.requestUpdate(o, a, s, !0, l); + }; + } + throw Error("Unsupported decorator location: " + i); +}; +function f(s) { + return (e, t) => typeof t == "object" ? We(s, e, t) : ((i, r, n) => { + const o = r.hasOwnProperty(n); + return r.constructor.createProperty(n, i), o ? Object.getOwnPropertyDescriptor(r, n) : void 0; + })(s, e, t); +} +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +function u(s) { + return f({ ...s, state: !0, attribute: !1 }); +} +function xe(s, e) { + const t = new WebSocket(s); + return t.onmessage = (i) => { + var r, n, o, l; + try { + const a = JSON.parse(i.data); + ((n = (r = a.type) == null ? void 0 : r.startsWith) != null && n.call(r, "process.") || (l = (o = a.channel) == null ? void 0 : o.startsWith) != null && l.call(o, "process.")) && e(a); + } catch { + } + }, t; +} +class Fe { + constructor(e = "") { + this.baseUrl = e; + } + get base() { + return `${this.baseUrl}/api/process`; + } + async request(e, t) { + var n; + const r = await (await fetch(`${this.base}${e}`, t)).json(); + if (!r.success) + throw new Error(((n = r.error) == null ? void 0 : n.message) ?? "Request failed"); + return r.data; + } + /** List all alive daemons from the registry. */ + listDaemons() { + return this.request("/daemons"); + } + /** Get a single daemon entry by code and daemon name. */ + getDaemon(e, t) { + return this.request(`/daemons/${e}/${t}`); + } + /** Stop a daemon (SIGTERM + unregister). */ + stopDaemon(e, t) { + return this.request(`/daemons/${e}/${t}/stop`, { + method: "POST" + }); + } + /** Check daemon health endpoint. */ + healthCheck(e, t) { + return this.request(`/daemons/${e}/${t}/health`); + } +} +var Ke = Object.defineProperty, Ve = Object.getOwnPropertyDescriptor, k = (s, e, t, i) => { + for (var r = i > 1 ? void 0 : i ? Ve(e, t) : e, n = s.length - 1, o; n >= 0; n--) + (o = s[n]) && (r = (i ? o(e, t, r) : o(r)) || r); + return i && r && Ke(e, t, r), r; +}; +let g = class extends $ { + constructor() { + super(...arguments), this.apiUrl = "", this.daemons = [], this.loading = !0, this.error = "", this.stopping = /* @__PURE__ */ new Set(), this.checking = /* @__PURE__ */ new Set(), this.healthResults = /* @__PURE__ */ new Map(); + } + connectedCallback() { + super.connectedCallback(), this.api = new Fe(this.apiUrl), this.loadDaemons(); + } + async loadDaemons() { + this.loading = !0, this.error = ""; + try { + this.daemons = await this.api.listDaemons(); + } catch (s) { + this.error = s.message ?? "Failed to load daemons"; + } finally { + this.loading = !1; + } + } + daemonKey(s) { + return `${s.code}/${s.daemon}`; + } + async handleStop(s) { + const e = this.daemonKey(s); + this.stopping = /* @__PURE__ */ new Set([...this.stopping, e]); + try { + await this.api.stopDaemon(s.code, s.daemon), this.dispatchEvent( + new CustomEvent("daemon-stopped", { + detail: { code: s.code, daemon: s.daemon }, + bubbles: !0 + }) + ), await this.loadDaemons(); + } catch (t) { + this.error = t.message ?? "Failed to stop daemon"; + } finally { + const t = new Set(this.stopping); + t.delete(e), this.stopping = t; + } + } + async handleHealthCheck(s) { + const e = this.daemonKey(s); + this.checking = /* @__PURE__ */ new Set([...this.checking, e]); + try { + const t = await this.api.healthCheck(s.code, s.daemon), i = new Map(this.healthResults); + i.set(e, t), this.healthResults = i; + } catch (t) { + this.error = t.message ?? "Health check failed"; + } finally { + const t = new Set(this.checking); + t.delete(e), this.checking = t; + } + } + formatDate(s) { + try { + return new Date(s).toLocaleDateString("en-GB", { + day: "numeric", + month: "short", + year: "numeric", + hour: "2-digit", + minute: "2-digit" + }); + } catch { + return s; + } + } + renderHealthBadge(s) { + const e = this.daemonKey(s); + if (this.checking.has(e)) + return c`Checking\u2026`; + const t = this.healthResults.get(e); + return t ? c` + ${t.healthy ? "Healthy" : "Unhealthy"} + ` : s.health ? c`Unchecked` : c`No health endpoint`; + } + render() { + return this.loading ? c`
Loading daemons\u2026
` : c` + ${this.error ? c`
${this.error}
` : d} + ${this.daemons.length === 0 ? c`
No daemons registered.
` : c` +
+ ${this.daemons.map((s) => { + const e = this.daemonKey(s); + return c` +
+
+
+ ${s.code} + ${s.daemon} + ${this.renderHealthBadge(s)} +
+
+ PID ${s.pid} + Started ${this.formatDate(s.started)} + ${s.project ? c`${s.project}` : d} + ${s.binary ? c`${s.binary}` : d} +
+
+
+ ${s.health ? c` + + ` : d} + +
+
+ `; + })} +
+ `} + `; + } +}; +g.styles = B` + :host { + display: block; + font-family: system-ui, -apple-system, sans-serif; + } + + .list { + display: flex; + flex-direction: column; + gap: 0.5rem; + } + + .item { + border: 1px solid #e5e7eb; + border-radius: 0.5rem; + padding: 1rem; + background: #fff; + display: flex; + justify-content: space-between; + align-items: center; + transition: box-shadow 0.15s; + } + + .item:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + } + + .item-info { + flex: 1; + } + + .item-name { + font-weight: 600; + font-size: 0.9375rem; + display: flex; + align-items: center; + gap: 0.5rem; + } + + .item-code { + font-family: monospace; + font-size: 0.8125rem; + colour: #6366f1; + } + + .item-meta { + font-size: 0.75rem; + colour: #6b7280; + margin-top: 0.25rem; + display: flex; + gap: 1rem; + } + + .pid-badge { + font-family: monospace; + background: #f3f4f6; + padding: 0.0625rem 0.375rem; + border-radius: 0.25rem; + font-size: 0.6875rem; + } + + .health-badge { + font-size: 0.6875rem; + padding: 0.125rem 0.5rem; + border-radius: 1rem; + font-weight: 600; + } + + .health-badge.healthy { + background: #dcfce7; + colour: #166534; + } + + .health-badge.unhealthy { + background: #fef2f2; + colour: #991b1b; + } + + .health-badge.unknown { + background: #f3f4f6; + colour: #6b7280; + } + + .health-badge.checking { + background: #fef3c7; + colour: #92400e; + } + + .item-actions { + display: flex; + gap: 0.5rem; + } + + button { + padding: 0.375rem 0.75rem; + border-radius: 0.375rem; + font-size: 0.8125rem; + cursor: pointer; + transition: background 0.15s; + } + + button.health-btn { + background: #fff; + colour: #6366f1; + border: 1px solid #6366f1; + } + + button.health-btn:hover { + background: #eef2ff; + } + + button.health-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + button.stop-btn { + background: #fff; + colour: #dc2626; + border: 1px solid #dc2626; + } + + button.stop-btn:hover { + background: #fef2f2; + } + + button.stop-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + .empty { + text-align: center; + padding: 2rem; + colour: #9ca3af; + font-size: 0.875rem; + } + + .loading { + text-align: center; + padding: 2rem; + colour: #6b7280; + } + + .error { + colour: #dc2626; + padding: 0.75rem; + background: #fef2f2; + border-radius: 0.375rem; + font-size: 0.875rem; + margin-bottom: 1rem; + } + `; +k([ + f({ attribute: "api-url" }) +], g.prototype, "apiUrl", 2); +k([ + u() +], g.prototype, "daemons", 2); +k([ + u() +], g.prototype, "loading", 2); +k([ + u() +], g.prototype, "error", 2); +k([ + u() +], g.prototype, "stopping", 2); +k([ + u() +], g.prototype, "checking", 2); +k([ + u() +], g.prototype, "healthResults", 2); +g = k([ + F("core-process-daemons") +], g); +var Je = Object.defineProperty, Ze = Object.getOwnPropertyDescriptor, U = (s, e, t, i) => { + for (var r = i > 1 ? void 0 : i ? Ze(e, t) : e, n = s.length - 1, o; n >= 0; n--) + (o = s[n]) && (r = (i ? o(e, t, r) : o(r)) || r); + return i && r && Je(e, t, r), r; +}; +let y = class extends $ { + constructor() { + super(...arguments), this.apiUrl = "", this.selectedId = "", this.processes = [], this.loading = !1, this.error = "", this.killing = /* @__PURE__ */ new Set(); + } + connectedCallback() { + super.connectedCallback(), this.loadProcesses(); + } + async loadProcesses() { + this.loading = !1, this.processes = []; + } + handleSelect(s) { + this.dispatchEvent( + new CustomEvent("process-selected", { + detail: { id: s.id }, + bubbles: !0, + composed: !0 + }) + ); + } + formatUptime(s) { + try { + const e = Date.now() - new Date(s).getTime(), t = Math.floor(e / 1e3); + if (t < 60) return `${t}s`; + const i = Math.floor(t / 60); + return i < 60 ? `${i}m ${t % 60}s` : `${Math.floor(i / 60)}h ${i % 60}m`; + } catch { + return "unknown"; + } + } + render() { + return this.loading ? c`
Loading processes\u2026
` : c` + ${this.error ? c`
${this.error}
` : d} + ${this.processes.length === 0 ? c` +
+ Process list endpoints are pending. Processes will appear here once + the REST API for managed processes is available. +
+
No managed processes.
+ ` : c` +
+ ${this.processes.map( + (s) => { + var e; + return c` +
this.handleSelect(s)} + > +
+
+ ${s.command} ${((e = s.args) == null ? void 0 : e.join(" ")) ?? ""} + ${s.status} +
+
+ PID ${s.pid} + ${s.id} + ${s.dir ? c`${s.dir}` : d} + ${s.status === "running" ? c`Up ${this.formatUptime(s.startedAt)}` : d} + ${s.status === "exited" ? c` + exit ${s.exitCode} + ` : d} +
+
+ ${s.status === "running" ? c` +
+ +
+ ` : d} +
+ `; + } + )} +
+ `} + `; + } +}; +y.styles = B` + :host { + display: block; + font-family: system-ui, -apple-system, sans-serif; + } + + .list { + display: flex; + flex-direction: column; + gap: 0.375rem; + } + + .item { + border: 1px solid #e5e7eb; + border-radius: 0.5rem; + padding: 0.75rem 1rem; + background: #fff; + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + transition: box-shadow 0.15s, border-colour 0.15s; + } + + .item:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + } + + .item.selected { + border-colour: #6366f1; + box-shadow: 0 0 0 1px #6366f1; + } + + .item-info { + flex: 1; + } + + .item-command { + font-weight: 600; + font-size: 0.9375rem; + font-family: monospace; + display: flex; + align-items: center; + gap: 0.5rem; + } + + .item-meta { + font-size: 0.75rem; + colour: #6b7280; + margin-top: 0.25rem; + display: flex; + gap: 1rem; + } + + .status-badge { + font-size: 0.6875rem; + padding: 0.125rem 0.5rem; + border-radius: 1rem; + font-weight: 600; + } + + .status-badge.running { + background: #dbeafe; + colour: #1e40af; + } + + .status-badge.pending { + background: #fef3c7; + colour: #92400e; + } + + .status-badge.exited { + background: #dcfce7; + colour: #166534; + } + + .status-badge.failed { + background: #fef2f2; + colour: #991b1b; + } + + .status-badge.killed { + background: #fce7f3; + colour: #9d174d; + } + + .exit-code { + font-family: monospace; + font-size: 0.6875rem; + background: #f3f4f6; + padding: 0.0625rem 0.375rem; + border-radius: 0.25rem; + } + + .exit-code.nonzero { + background: #fef2f2; + colour: #991b1b; + } + + .pid-badge { + font-family: monospace; + background: #f3f4f6; + padding: 0.0625rem 0.375rem; + border-radius: 0.25rem; + font-size: 0.6875rem; + } + + .item-actions { + display: flex; + gap: 0.5rem; + } + + button.kill-btn { + padding: 0.375rem 0.75rem; + border-radius: 0.375rem; + font-size: 0.8125rem; + cursor: pointer; + transition: background 0.15s; + background: #fff; + colour: #dc2626; + border: 1px solid #dc2626; + } + + button.kill-btn:hover { + background: #fef2f2; + } + + button.kill-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + .empty { + text-align: center; + padding: 2rem; + colour: #9ca3af; + font-size: 0.875rem; + } + + .loading { + text-align: center; + padding: 2rem; + colour: #6b7280; + } + + .error { + colour: #dc2626; + padding: 0.75rem; + background: #fef2f2; + border-radius: 0.375rem; + font-size: 0.875rem; + margin-bottom: 1rem; + } + + .info-notice { + padding: 0.75rem; + background: #eff6ff; + border: 1px solid #bfdbfe; + border-radius: 0.375rem; + font-size: 0.8125rem; + colour: #1e40af; + margin-bottom: 1rem; + } + `; +U([ + f({ attribute: "api-url" }) +], y.prototype, "apiUrl", 2); +U([ + f({ attribute: "selected-id" }) +], y.prototype, "selectedId", 2); +U([ + u() +], y.prototype, "processes", 2); +U([ + u() +], y.prototype, "loading", 2); +U([ + u() +], y.prototype, "error", 2); +U([ + u() +], y.prototype, "killing", 2); +y = U([ + F("core-process-list") +], y); +var Ge = Object.defineProperty, Qe = Object.getOwnPropertyDescriptor, O = (s, e, t, i) => { + for (var r = i > 1 ? void 0 : i ? Qe(e, t) : e, n = s.length - 1, o; n >= 0; n--) + (o = s[n]) && (r = (i ? o(e, t, r) : o(r)) || r); + return i && r && Ge(e, t, r), r; +}; +let v = class extends $ { + constructor() { + super(...arguments), this.apiUrl = "", this.wsUrl = "", this.processId = "", this.lines = [], this.autoScroll = !0, this.connected = !1, this.ws = null; + } + connectedCallback() { + super.connectedCallback(), this.wsUrl && this.processId && this.connect(); + } + disconnectedCallback() { + super.disconnectedCallback(), this.disconnect(); + } + updated(s) { + (s.has("processId") || s.has("wsUrl")) && (this.disconnect(), this.lines = [], this.wsUrl && this.processId && this.connect()), this.autoScroll && this.scrollToBottom(); + } + connect() { + this.ws = xe(this.wsUrl, (s) => { + const e = s.data; + if (!e) return; + (s.channel ?? s.type ?? "") === "process.output" && e.id === this.processId && (this.lines = [ + ...this.lines, + { + text: e.line ?? "", + stream: e.stream === "stderr" ? "stderr" : "stdout", + timestamp: Date.now() + } + ]); + }), this.ws.onopen = () => { + this.connected = !0; + }, this.ws.onclose = () => { + this.connected = !1; + }; + } + disconnect() { + this.ws && (this.ws.close(), this.ws = null), this.connected = !1; + } + handleClear() { + this.lines = []; + } + handleAutoScrollToggle() { + this.autoScroll = !this.autoScroll; + } + scrollToBottom() { + var e; + const s = (e = this.shadowRoot) == null ? void 0 : e.querySelector(".output-body"); + s && (s.scrollTop = s.scrollHeight); + } + render() { + return this.processId ? c` +
+ Output: ${this.processId} +
+ + +
+
+
+ ${this.lines.length === 0 ? c`
Waiting for output\u2026
` : this.lines.map( + (s) => c` +
+ ${s.stream}${s.text} +
+ ` + )} +
+ ` : c`
Select a process to view its output.
`; + } +}; +v.styles = B` + :host { + display: block; + font-family: system-ui, -apple-system, sans-serif; + margin-top: 0.75rem; + } + + .output-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem 0.75rem; + background: #1e1e1e; + border-radius: 0.5rem 0.5rem 0 0; + colour: #d4d4d4; + font-size: 0.75rem; + } + + .output-title { + font-weight: 600; + } + + .output-actions { + display: flex; + gap: 0.5rem; + } + + button.clear-btn { + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; + font-size: 0.6875rem; + cursor: pointer; + background: #333; + colour: #d4d4d4; + border: 1px solid #555; + transition: background 0.15s; + } + + button.clear-btn:hover { + background: #444; + } + + .auto-scroll-toggle { + display: flex; + align-items: center; + gap: 0.25rem; + font-size: 0.6875rem; + colour: #d4d4d4; + cursor: pointer; + } + + .auto-scroll-toggle input { + cursor: pointer; + } + + .output-body { + background: #1e1e1e; + border-radius: 0 0 0.5rem 0.5rem; + padding: 0.5rem 0.75rem; + max-height: 24rem; + overflow-y: auto; + font-family: 'SF Mono', 'Monaco', 'Menlo', 'Consolas', monospace; + font-size: 0.8125rem; + line-height: 1.5; + } + + .line { + white-space: pre-wrap; + word-break: break-all; + } + + .line.stdout { + colour: #d4d4d4; + } + + .line.stderr { + colour: #f87171; + } + + .stream-tag { + display: inline-block; + width: 3rem; + font-size: 0.625rem; + font-weight: 600; + text-transform: uppercase; + opacity: 0.5; + margin-right: 0.5rem; + } + + .empty { + text-align: center; + padding: 2rem; + colour: #6b7280; + font-size: 0.8125rem; + } + + .waiting { + colour: #9ca3af; + font-style: italic; + padding: 1rem; + text-align: center; + font-size: 0.8125rem; + } + `; +O([ + f({ attribute: "api-url" }) +], v.prototype, "apiUrl", 2); +O([ + f({ attribute: "ws-url" }) +], v.prototype, "wsUrl", 2); +O([ + f({ attribute: "process-id" }) +], v.prototype, "processId", 2); +O([ + u() +], v.prototype, "lines", 2); +O([ + u() +], v.prototype, "autoScroll", 2); +O([ + u() +], v.prototype, "connected", 2); +v = O([ + F("core-process-output") +], v); +var Xe = Object.defineProperty, Ye = Object.getOwnPropertyDescriptor, Q = (s, e, t, i) => { + for (var r = i > 1 ? void 0 : i ? Ye(e, t) : e, n = s.length - 1, o; n >= 0; n--) + (o = s[n]) && (r = (i ? o(e, t, r) : o(r)) || r); + return i && r && Xe(e, t, r), r; +}; +let R = class extends $ { + constructor() { + super(...arguments), this.apiUrl = "", this.result = null, this.expandedOutputs = /* @__PURE__ */ new Set(); + } + connectedCallback() { + super.connectedCallback(), this.loadResults(); + } + async loadResults() { + } + toggleOutput(s) { + const e = new Set(this.expandedOutputs); + e.has(s) ? e.delete(s) : e.add(s), this.expandedOutputs = e; + } + formatDuration(s) { + return s < 1e6 ? `${(s / 1e3).toFixed(0)}µs` : s < 1e9 ? `${(s / 1e6).toFixed(0)}ms` : `${(s / 1e9).toFixed(2)}s`; + } + resultStatus(s) { + return s.skipped ? "skipped" : s.passed ? "passed" : "failed"; + } + render() { + if (!this.result) + return c` +
+ Pipeline runner endpoints are pending. Pass pipeline results via the + result property, or results will appear here once the REST + API for pipeline execution is available. +
+
No pipeline results.
+ `; + const { results: s, duration: e, passed: t, failed: i, skipped: r, success: n } = this.result; + return c` +
+ + ${n ? "Passed" : "Failed"} + +
+ ${t} + Passed +
+
+ ${i} + Failed +
+
+ ${r} + Skipped +
+ ${this.formatDuration(e)} +
+ +
+ ${s.map( + (o) => c` +
+
+
+ ${o.name} + ${this.resultStatus(o)} +
+ ${this.formatDuration(o.duration)} +
+
+ ${o.exitCode !== 0 && !o.skipped ? c`exit ${o.exitCode}` : d} +
+ ${o.error ? c`
${o.error}
` : d} + ${o.output ? c` + + ${this.expandedOutputs.has(o.name) ? c`
${o.output}
` : d} + ` : d} +
+ ` + )} +
+ `; + } +}; +R.styles = B` + :host { + display: block; + font-family: system-ui, -apple-system, sans-serif; + } + + .summary { + display: flex; + gap: 1rem; + padding: 0.75rem 1rem; + background: #fff; + border: 1px solid #e5e7eb; + border-radius: 0.5rem; + margin-bottom: 1rem; + align-items: center; + } + + .summary-stat { + display: flex; + flex-direction: column; + align-items: center; + } + + .summary-value { + font-weight: 700; + font-size: 1.25rem; + } + + .summary-label { + font-size: 0.6875rem; + colour: #6b7280; + text-transform: uppercase; + letter-spacing: 0.025em; + } + + .summary-value.passed { + colour: #166534; + } + + .summary-value.failed { + colour: #991b1b; + } + + .summary-value.skipped { + colour: #92400e; + } + + .summary-duration { + margin-left: auto; + font-size: 0.8125rem; + colour: #6b7280; + } + + .overall-badge { + font-size: 0.75rem; + padding: 0.25rem 0.75rem; + border-radius: 1rem; + font-weight: 600; + } + + .overall-badge.success { + background: #dcfce7; + colour: #166534; + } + + .overall-badge.failure { + background: #fef2f2; + colour: #991b1b; + } + + .list { + display: flex; + flex-direction: column; + gap: 0.375rem; + } + + .spec { + border: 1px solid #e5e7eb; + border-radius: 0.5rem; + padding: 0.75rem 1rem; + background: #fff; + transition: box-shadow 0.15s; + } + + .spec:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + } + + .spec-header { + display: flex; + justify-content: space-between; + align-items: center; + } + + .spec-name { + font-weight: 600; + font-size: 0.9375rem; + display: flex; + align-items: center; + gap: 0.5rem; + } + + .spec-meta { + font-size: 0.75rem; + colour: #6b7280; + margin-top: 0.25rem; + display: flex; + gap: 1rem; + } + + .result-badge { + font-size: 0.6875rem; + padding: 0.125rem 0.5rem; + border-radius: 1rem; + font-weight: 600; + } + + .result-badge.passed { + background: #dcfce7; + colour: #166534; + } + + .result-badge.failed { + background: #fef2f2; + colour: #991b1b; + } + + .result-badge.skipped { + background: #fef3c7; + colour: #92400e; + } + + .duration { + font-family: monospace; + font-size: 0.75rem; + colour: #6b7280; + } + + .deps { + font-size: 0.6875rem; + colour: #9ca3af; + } + + .spec-output { + margin-top: 0.5rem; + padding: 0.5rem 0.75rem; + background: #1e1e1e; + border-radius: 0.375rem; + font-family: 'SF Mono', 'Monaco', 'Menlo', 'Consolas', monospace; + font-size: 0.75rem; + line-height: 1.5; + colour: #d4d4d4; + white-space: pre-wrap; + word-break: break-all; + max-height: 12rem; + overflow-y: auto; + } + + .spec-error { + margin-top: 0.375rem; + font-size: 0.75rem; + colour: #dc2626; + } + + .toggle-output { + font-size: 0.6875rem; + colour: #6366f1; + cursor: pointer; + background: none; + border: none; + padding: 0; + margin-top: 0.375rem; + } + + .toggle-output:hover { + text-decoration: underline; + } + + .empty { + text-align: center; + padding: 2rem; + colour: #9ca3af; + font-size: 0.875rem; + } + + .info-notice { + padding: 0.75rem; + background: #eff6ff; + border: 1px solid #bfdbfe; + border-radius: 0.375rem; + font-size: 0.8125rem; + colour: #1e40af; + margin-bottom: 1rem; + } + `; +Q([ + f({ attribute: "api-url" }) +], R.prototype, "apiUrl", 2); +Q([ + f({ type: Object }) +], R.prototype, "result", 2); +Q([ + u() +], R.prototype, "expandedOutputs", 2); +R = Q([ + F("core-process-runner") +], R); +var et = Object.defineProperty, tt = Object.getOwnPropertyDescriptor, z = (s, e, t, i) => { + for (var r = i > 1 ? void 0 : i ? tt(e, t) : e, n = s.length - 1, o; n >= 0; n--) + (o = s[n]) && (r = (i ? o(e, t, r) : o(r)) || r); + return i && r && et(e, t, r), r; +}; +let _ = class extends $ { + constructor() { + super(...arguments), this.apiUrl = "", this.wsUrl = "", this.activeTab = "daemons", this.wsConnected = !1, this.lastEvent = "", this.selectedProcessId = "", this.ws = null, this.tabs = [ + { id: "daemons", label: "Daemons" }, + { id: "processes", label: "Processes" }, + { id: "pipelines", label: "Pipelines" } + ]; + } + connectedCallback() { + super.connectedCallback(), this.wsUrl && this.connectWs(); + } + disconnectedCallback() { + super.disconnectedCallback(), this.ws && (this.ws.close(), this.ws = null); + } + connectWs() { + this.ws = xe(this.wsUrl, (s) => { + this.lastEvent = s.channel ?? s.type ?? "", this.requestUpdate(); + }), this.ws.onopen = () => { + this.wsConnected = !0; + }, this.ws.onclose = () => { + this.wsConnected = !1; + }; + } + handleTabClick(s) { + this.activeTab = s; + } + handleRefresh() { + var e; + const s = (e = this.shadowRoot) == null ? void 0 : e.querySelector(".content"); + if (s) { + const t = s.firstElementChild; + t && "loadDaemons" in t ? t.loadDaemons() : t && "loadProcesses" in t ? t.loadProcesses() : t && "loadResults" in t && t.loadResults(); + } + } + handleProcessSelected(s) { + this.selectedProcessId = s.detail.id; + } + renderContent() { + switch (this.activeTab) { + case "daemons": + return c``; + case "processes": + return c` + + ${this.selectedProcessId ? c`` : d} + `; + case "pipelines": + return c``; + default: + return d; + } + } + render() { + const s = this.wsUrl ? this.wsConnected ? "connected" : "disconnected" : "idle"; + return c` +
+ Process Manager + +
+ +
+ ${this.tabs.map( + (e) => c` + + ` + )} +
+ +
${this.renderContent()}
+ + + `; + } +}; +_.styles = B` + :host { + display: flex; + flex-direction: column; + font-family: system-ui, -apple-system, sans-serif; + height: 100%; + background: #fafafa; + } + + /* H — Header */ + .header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem 1rem; + background: #fff; + border-bottom: 1px solid #e5e7eb; + } + + .title { + font-weight: 700; + font-size: 1rem; + colour: #111827; + } + + .refresh-btn { + padding: 0.375rem 0.75rem; + border: 1px solid #d1d5db; + border-radius: 0.375rem; + background: #fff; + font-size: 0.8125rem; + cursor: pointer; + transition: background 0.15s; + } + + .refresh-btn:hover { + background: #f3f4f6; + } + + /* H-L — Tabs */ + .tabs { + display: flex; + gap: 0; + background: #fff; + border-bottom: 1px solid #e5e7eb; + padding: 0 1rem; + } + + .tab { + padding: 0.625rem 1rem; + font-size: 0.8125rem; + font-weight: 500; + colour: #6b7280; + cursor: pointer; + border-bottom: 2px solid transparent; + transition: all 0.15s; + background: none; + border-top: none; + border-left: none; + border-right: none; + } + + .tab:hover { + colour: #374151; + } + + .tab.active { + colour: #6366f1; + border-bottom-colour: #6366f1; + } + + /* C — Content */ + .content { + flex: 1; + padding: 1rem; + overflow-y: auto; + } + + /* F — Footer / Status bar */ + .footer { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem 1rem; + background: #fff; + border-top: 1px solid #e5e7eb; + font-size: 0.75rem; + colour: #9ca3af; + } + + .ws-status { + display: flex; + align-items: center; + gap: 0.375rem; + } + + .ws-dot { + width: 0.5rem; + height: 0.5rem; + border-radius: 50%; + } + + .ws-dot.connected { + background: #22c55e; + } + + .ws-dot.disconnected { + background: #ef4444; + } + + .ws-dot.idle { + background: #d1d5db; + } + `; +z([ + f({ attribute: "api-url" }) +], _.prototype, "apiUrl", 2); +z([ + f({ attribute: "ws-url" }) +], _.prototype, "wsUrl", 2); +z([ + u() +], _.prototype, "activeTab", 2); +z([ + u() +], _.prototype, "wsConnected", 2); +z([ + u() +], _.prototype, "lastEvent", 2); +z([ + u() +], _.prototype, "selectedProcessId", 2); +_ = z([ + F("core-process-panel") +], _); +export { + Fe as ProcessApi, + g as ProcessDaemons, + y as ProcessList, + v as ProcessOutput, + _ as ProcessPanel, + R as ProcessRunner, + xe as connectProcessEvents +}; diff --git a/ui/index.html b/ui/index.html new file mode 100644 index 0000000..62672dc --- /dev/null +++ b/ui/index.html @@ -0,0 +1,79 @@ + + + + + + Core Process — Demo + + + + +

Core Process — Custom Element Demo

+ +
+ +
+ +

Standalone Elements

+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + diff --git a/ui/package-lock.json b/ui/package-lock.json new file mode 100644 index 0000000..abb2077 --- /dev/null +++ b/ui/package-lock.json @@ -0,0 +1,1213 @@ +{ + "name": "@core/process-ui", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@core/process-ui", + "version": "0.1.0", + "dependencies": { + "lit": "^3.2.0" + }, + "devDependencies": { + "typescript": "^5.7.0", + "vite": "^6.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@lit-labs/ssr-dom-shim": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.5.1.tgz", + "integrity": "sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==", + "license": "BSD-3-Clause" + }, + "node_modules/@lit/reactive-element": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.2.tgz", + "integrity": "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.5.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lit": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.2.tgz", + "integrity": "sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^2.1.0", + "lit-element": "^4.2.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-element": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.2.tgz", + "integrity": "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.5.0", + "@lit/reactive-element": "^2.1.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-html": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.2.tgz", + "integrity": "sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/ui/package.json b/ui/package.json new file mode 100644 index 0000000..6aa7273 --- /dev/null +++ b/ui/package.json @@ -0,0 +1,18 @@ +{ + "name": "@core/process-ui", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "lit": "^3.2.0" + }, + "devDependencies": { + "typescript": "^5.7.0", + "vite": "^6.0.0" + } +} diff --git a/ui/src/index.ts b/ui/src/index.ts new file mode 100644 index 0000000..110f85c --- /dev/null +++ b/ui/src/index.ts @@ -0,0 +1,11 @@ +// SPDX-Licence-Identifier: EUPL-1.2 + +// Bundle entry — exports all process management custom elements. + +export { ProcessPanel } from './process-panel.js'; +export { ProcessDaemons } from './process-daemons.js'; +export { ProcessList } from './process-list.js'; +export { ProcessOutput } from './process-output.js'; +export { ProcessRunner } from './process-runner.js'; +export { ProcessApi } from './shared/api.js'; +export { connectProcessEvents } from './shared/events.js'; diff --git a/ui/src/process-daemons.ts b/ui/src/process-daemons.ts new file mode 100644 index 0000000..6f62a80 --- /dev/null +++ b/ui/src/process-daemons.ts @@ -0,0 +1,334 @@ +// SPDX-Licence-Identifier: EUPL-1.2 + +import { LitElement, html, css, nothing } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; +import { ProcessApi, type DaemonEntry, type HealthResult } from './shared/api.js'; + +/** + * — Daemon registry list. + * Shows registered daemons with status badges, health indicators, and stop buttons. + */ +@customElement('core-process-daemons') +export class ProcessDaemons extends LitElement { + static styles = css` + :host { + display: block; + font-family: system-ui, -apple-system, sans-serif; + } + + .list { + display: flex; + flex-direction: column; + gap: 0.5rem; + } + + .item { + border: 1px solid #e5e7eb; + border-radius: 0.5rem; + padding: 1rem; + background: #fff; + display: flex; + justify-content: space-between; + align-items: center; + transition: box-shadow 0.15s; + } + + .item:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + } + + .item-info { + flex: 1; + } + + .item-name { + font-weight: 600; + font-size: 0.9375rem; + display: flex; + align-items: center; + gap: 0.5rem; + } + + .item-code { + font-family: monospace; + font-size: 0.8125rem; + colour: #6366f1; + } + + .item-meta { + font-size: 0.75rem; + colour: #6b7280; + margin-top: 0.25rem; + display: flex; + gap: 1rem; + } + + .pid-badge { + font-family: monospace; + background: #f3f4f6; + padding: 0.0625rem 0.375rem; + border-radius: 0.25rem; + font-size: 0.6875rem; + } + + .health-badge { + font-size: 0.6875rem; + padding: 0.125rem 0.5rem; + border-radius: 1rem; + font-weight: 600; + } + + .health-badge.healthy { + background: #dcfce7; + colour: #166534; + } + + .health-badge.unhealthy { + background: #fef2f2; + colour: #991b1b; + } + + .health-badge.unknown { + background: #f3f4f6; + colour: #6b7280; + } + + .health-badge.checking { + background: #fef3c7; + colour: #92400e; + } + + .item-actions { + display: flex; + gap: 0.5rem; + } + + button { + padding: 0.375rem 0.75rem; + border-radius: 0.375rem; + font-size: 0.8125rem; + cursor: pointer; + transition: background 0.15s; + } + + button.health-btn { + background: #fff; + colour: #6366f1; + border: 1px solid #6366f1; + } + + button.health-btn:hover { + background: #eef2ff; + } + + button.health-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + button.stop-btn { + background: #fff; + colour: #dc2626; + border: 1px solid #dc2626; + } + + button.stop-btn:hover { + background: #fef2f2; + } + + button.stop-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + .empty { + text-align: center; + padding: 2rem; + colour: #9ca3af; + font-size: 0.875rem; + } + + .loading { + text-align: center; + padding: 2rem; + colour: #6b7280; + } + + .error { + colour: #dc2626; + padding: 0.75rem; + background: #fef2f2; + border-radius: 0.375rem; + font-size: 0.875rem; + margin-bottom: 1rem; + } + `; + + @property({ attribute: 'api-url' }) apiUrl = ''; + + @state() private daemons: DaemonEntry[] = []; + @state() private loading = true; + @state() private error = ''; + @state() private stopping = new Set(); + @state() private checking = new Set(); + @state() private healthResults = new Map(); + + private api!: ProcessApi; + + connectedCallback() { + super.connectedCallback(); + this.api = new ProcessApi(this.apiUrl); + this.loadDaemons(); + } + + async loadDaemons() { + this.loading = true; + this.error = ''; + try { + this.daemons = await this.api.listDaemons(); + } catch (e: any) { + this.error = e.message ?? 'Failed to load daemons'; + } finally { + this.loading = false; + } + } + + private daemonKey(d: DaemonEntry): string { + return `${d.code}/${d.daemon}`; + } + + private async handleStop(d: DaemonEntry) { + const key = this.daemonKey(d); + this.stopping = new Set([...this.stopping, key]); + try { + await this.api.stopDaemon(d.code, d.daemon); + this.dispatchEvent( + new CustomEvent('daemon-stopped', { + detail: { code: d.code, daemon: d.daemon }, + bubbles: true, + }), + ); + await this.loadDaemons(); + } catch (e: any) { + this.error = e.message ?? 'Failed to stop daemon'; + } finally { + const next = new Set(this.stopping); + next.delete(key); + this.stopping = next; + } + } + + private async handleHealthCheck(d: DaemonEntry) { + const key = this.daemonKey(d); + this.checking = new Set([...this.checking, key]); + try { + const result = await this.api.healthCheck(d.code, d.daemon); + const next = new Map(this.healthResults); + next.set(key, result); + this.healthResults = next; + } catch (e: any) { + this.error = e.message ?? 'Health check failed'; + } finally { + const next = new Set(this.checking); + next.delete(key); + this.checking = next; + } + } + + private formatDate(iso: string): string { + try { + return new Date(iso).toLocaleDateString('en-GB', { + day: 'numeric', + month: 'short', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + } catch { + return iso; + } + } + + private renderHealthBadge(d: DaemonEntry) { + const key = this.daemonKey(d); + + if (this.checking.has(key)) { + return html`Checking\u2026`; + } + + const result = this.healthResults.get(key); + if (result) { + return html` + ${result.healthy ? 'Healthy' : 'Unhealthy'} + `; + } + + if (!d.health) { + return html`No health endpoint`; + } + + return html`Unchecked`; + } + + render() { + if (this.loading) { + return html`
Loading daemons\u2026
`; + } + + return html` + ${this.error ? html`
${this.error}
` : nothing} + ${this.daemons.length === 0 + ? html`
No daemons registered.
` + : html` +
+ ${this.daemons.map((d) => { + const key = this.daemonKey(d); + return html` +
+
+
+ ${d.code} + ${d.daemon} + ${this.renderHealthBadge(d)} +
+
+ PID ${d.pid} + Started ${this.formatDate(d.started)} + ${d.project ? html`${d.project}` : nothing} + ${d.binary ? html`${d.binary}` : nothing} +
+
+
+ ${d.health + ? html` + + ` + : nothing} + +
+
+ `; + })} +
+ `} + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'core-process-daemons': ProcessDaemons; + } +} diff --git a/ui/src/process-list.ts b/ui/src/process-list.ts new file mode 100644 index 0000000..6962acc --- /dev/null +++ b/ui/src/process-list.ts @@ -0,0 +1,301 @@ +// SPDX-Licence-Identifier: EUPL-1.2 + +import { LitElement, html, css, nothing } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; +import type { ProcessInfo } from './shared/api.js'; + +/** + * — Running processes with status and actions. + * + * Displays managed processes from the process service. Shows status badges, + * uptime, exit codes, and provides kill/select actions. + * + * Emits `process-selected` event when a process row is clicked, carrying + * the process ID for the output viewer. + * + * Note: Requires process-level REST endpoints (GET /processes, POST /processes/:id/kill) + * that are not yet in the provider. The element renders from WS events and local state + * until those endpoints are available. + */ +@customElement('core-process-list') +export class ProcessList extends LitElement { + static styles = css` + :host { + display: block; + font-family: system-ui, -apple-system, sans-serif; + } + + .list { + display: flex; + flex-direction: column; + gap: 0.375rem; + } + + .item { + border: 1px solid #e5e7eb; + border-radius: 0.5rem; + padding: 0.75rem 1rem; + background: #fff; + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + transition: box-shadow 0.15s, border-colour 0.15s; + } + + .item:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + } + + .item.selected { + border-colour: #6366f1; + box-shadow: 0 0 0 1px #6366f1; + } + + .item-info { + flex: 1; + } + + .item-command { + font-weight: 600; + font-size: 0.9375rem; + font-family: monospace; + display: flex; + align-items: center; + gap: 0.5rem; + } + + .item-meta { + font-size: 0.75rem; + colour: #6b7280; + margin-top: 0.25rem; + display: flex; + gap: 1rem; + } + + .status-badge { + font-size: 0.6875rem; + padding: 0.125rem 0.5rem; + border-radius: 1rem; + font-weight: 600; + } + + .status-badge.running { + background: #dbeafe; + colour: #1e40af; + } + + .status-badge.pending { + background: #fef3c7; + colour: #92400e; + } + + .status-badge.exited { + background: #dcfce7; + colour: #166534; + } + + .status-badge.failed { + background: #fef2f2; + colour: #991b1b; + } + + .status-badge.killed { + background: #fce7f3; + colour: #9d174d; + } + + .exit-code { + font-family: monospace; + font-size: 0.6875rem; + background: #f3f4f6; + padding: 0.0625rem 0.375rem; + border-radius: 0.25rem; + } + + .exit-code.nonzero { + background: #fef2f2; + colour: #991b1b; + } + + .pid-badge { + font-family: monospace; + background: #f3f4f6; + padding: 0.0625rem 0.375rem; + border-radius: 0.25rem; + font-size: 0.6875rem; + } + + .item-actions { + display: flex; + gap: 0.5rem; + } + + button.kill-btn { + padding: 0.375rem 0.75rem; + border-radius: 0.375rem; + font-size: 0.8125rem; + cursor: pointer; + transition: background 0.15s; + background: #fff; + colour: #dc2626; + border: 1px solid #dc2626; + } + + button.kill-btn:hover { + background: #fef2f2; + } + + button.kill-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + .empty { + text-align: center; + padding: 2rem; + colour: #9ca3af; + font-size: 0.875rem; + } + + .loading { + text-align: center; + padding: 2rem; + colour: #6b7280; + } + + .error { + colour: #dc2626; + padding: 0.75rem; + background: #fef2f2; + border-radius: 0.375rem; + font-size: 0.875rem; + margin-bottom: 1rem; + } + + .info-notice { + padding: 0.75rem; + background: #eff6ff; + border: 1px solid #bfdbfe; + border-radius: 0.375rem; + font-size: 0.8125rem; + colour: #1e40af; + margin-bottom: 1rem; + } + `; + + @property({ attribute: 'api-url' }) apiUrl = ''; + @property({ attribute: 'selected-id' }) selectedId = ''; + + @state() private processes: ProcessInfo[] = []; + @state() private loading = false; + @state() private error = ''; + @state() private killing = new Set(); + + connectedCallback() { + super.connectedCallback(); + this.loadProcesses(); + } + + async loadProcesses() { + // Process-level REST endpoints are not yet available. + // This element will populate via WS events once endpoints exist. + this.loading = false; + this.processes = []; + } + + private handleSelect(proc: ProcessInfo) { + this.dispatchEvent( + new CustomEvent('process-selected', { + detail: { id: proc.id }, + bubbles: true, + composed: true, + }), + ); + } + + private formatUptime(started: string): string { + try { + const ms = Date.now() - new Date(started).getTime(); + const seconds = Math.floor(ms / 1000); + if (seconds < 60) return `${seconds}s`; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m ${seconds % 60}s`; + const hours = Math.floor(minutes / 60); + return `${hours}h ${minutes % 60}m`; + } catch { + return 'unknown'; + } + } + + render() { + if (this.loading) { + return html`
Loading processes\u2026
`; + } + + return html` + ${this.error ? html`
${this.error}
` : nothing} + ${this.processes.length === 0 + ? html` +
+ Process list endpoints are pending. Processes will appear here once + the REST API for managed processes is available. +
+
No managed processes.
+ ` + : html` +
+ ${this.processes.map( + (proc) => html` +
this.handleSelect(proc)} + > +
+
+ ${proc.command} ${proc.args?.join(' ') ?? ''} + ${proc.status} +
+
+ PID ${proc.pid} + ${proc.id} + ${proc.dir ? html`${proc.dir}` : nothing} + ${proc.status === 'running' + ? html`Up ${this.formatUptime(proc.startedAt)}` + : nothing} + ${proc.status === 'exited' + ? html` + exit ${proc.exitCode} + ` + : nothing} +
+
+ ${proc.status === 'running' + ? html` +
+ +
+ ` + : nothing} +
+ `, + )} +
+ `} + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'core-process-list': ProcessList; + } +} diff --git a/ui/src/process-output.ts b/ui/src/process-output.ts new file mode 100644 index 0000000..e16fcbc --- /dev/null +++ b/ui/src/process-output.ts @@ -0,0 +1,252 @@ +// SPDX-Licence-Identifier: EUPL-1.2 + +import { LitElement, html, css, nothing } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; +import { connectProcessEvents, type ProcessEvent } from './shared/events.js'; + +interface OutputLine { + text: string; + stream: 'stdout' | 'stderr'; + timestamp: number; +} + +/** + * — Live stdout/stderr stream for a selected process. + * + * Connects to the WS endpoint and filters for process.output events matching + * the given process-id. Displays output in a terminal-style scrollable area + * with colour-coded stream indicators (stdout/stderr). + */ +@customElement('core-process-output') +export class ProcessOutput extends LitElement { + static styles = css` + :host { + display: block; + font-family: system-ui, -apple-system, sans-serif; + margin-top: 0.75rem; + } + + .output-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem 0.75rem; + background: #1e1e1e; + border-radius: 0.5rem 0.5rem 0 0; + colour: #d4d4d4; + font-size: 0.75rem; + } + + .output-title { + font-weight: 600; + } + + .output-actions { + display: flex; + gap: 0.5rem; + } + + button.clear-btn { + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; + font-size: 0.6875rem; + cursor: pointer; + background: #333; + colour: #d4d4d4; + border: 1px solid #555; + transition: background 0.15s; + } + + button.clear-btn:hover { + background: #444; + } + + .auto-scroll-toggle { + display: flex; + align-items: center; + gap: 0.25rem; + font-size: 0.6875rem; + colour: #d4d4d4; + cursor: pointer; + } + + .auto-scroll-toggle input { + cursor: pointer; + } + + .output-body { + background: #1e1e1e; + border-radius: 0 0 0.5rem 0.5rem; + padding: 0.5rem 0.75rem; + max-height: 24rem; + overflow-y: auto; + font-family: 'SF Mono', 'Monaco', 'Menlo', 'Consolas', monospace; + font-size: 0.8125rem; + line-height: 1.5; + } + + .line { + white-space: pre-wrap; + word-break: break-all; + } + + .line.stdout { + colour: #d4d4d4; + } + + .line.stderr { + colour: #f87171; + } + + .stream-tag { + display: inline-block; + width: 3rem; + font-size: 0.625rem; + font-weight: 600; + text-transform: uppercase; + opacity: 0.5; + margin-right: 0.5rem; + } + + .empty { + text-align: center; + padding: 2rem; + colour: #6b7280; + font-size: 0.8125rem; + } + + .waiting { + colour: #9ca3af; + font-style: italic; + padding: 1rem; + text-align: center; + font-size: 0.8125rem; + } + `; + + @property({ attribute: 'api-url' }) apiUrl = ''; + @property({ attribute: 'ws-url' }) wsUrl = ''; + @property({ attribute: 'process-id' }) processId = ''; + + @state() private lines: OutputLine[] = []; + @state() private autoScroll = true; + @state() private connected = false; + + private ws: WebSocket | null = null; + + connectedCallback() { + super.connectedCallback(); + if (this.wsUrl && this.processId) { + this.connect(); + } + } + + disconnectedCallback() { + super.disconnectedCallback(); + this.disconnect(); + } + + updated(changed: Map) { + if (changed.has('processId') || changed.has('wsUrl')) { + this.disconnect(); + this.lines = []; + if (this.wsUrl && this.processId) { + this.connect(); + } + } + + if (this.autoScroll) { + this.scrollToBottom(); + } + } + + private connect() { + this.ws = connectProcessEvents(this.wsUrl, (event: ProcessEvent) => { + const data = event.data; + if (!data) return; + + // Filter for output events matching our process ID + const channel = event.channel ?? event.type ?? ''; + if (channel === 'process.output' && data.id === this.processId) { + this.lines = [ + ...this.lines, + { + text: data.line ?? '', + stream: data.stream === 'stderr' ? 'stderr' : 'stdout', + timestamp: Date.now(), + }, + ]; + } + }); + + this.ws.onopen = () => { + this.connected = true; + }; + this.ws.onclose = () => { + this.connected = false; + }; + } + + private disconnect() { + if (this.ws) { + this.ws.close(); + this.ws = null; + } + this.connected = false; + } + + private handleClear() { + this.lines = []; + } + + private handleAutoScrollToggle() { + this.autoScroll = !this.autoScroll; + } + + private scrollToBottom() { + const body = this.shadowRoot?.querySelector('.output-body'); + if (body) { + body.scrollTop = body.scrollHeight; + } + } + + render() { + if (!this.processId) { + return html`
Select a process to view its output.
`; + } + + return html` +
+ Output: ${this.processId} +
+ + +
+
+
+ ${this.lines.length === 0 + ? html`
Waiting for output\u2026
` + : this.lines.map( + (line) => html` +
+ ${line.stream}${line.text} +
+ `, + )} +
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'core-process-output': ProcessOutput; + } +} diff --git a/ui/src/process-panel.ts b/ui/src/process-panel.ts new file mode 100644 index 0000000..703c72f --- /dev/null +++ b/ui/src/process-panel.ts @@ -0,0 +1,275 @@ +// SPDX-Licence-Identifier: EUPL-1.2 + +import { LitElement, html, css, nothing } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; +import { connectProcessEvents, type ProcessEvent } from './shared/events.js'; + +// Side-effect imports to register child elements +import './process-daemons.js'; +import './process-list.js'; +import './process-output.js'; +import './process-runner.js'; + +type TabId = 'daemons' | 'processes' | 'pipelines'; + +/** + * — Top-level process management panel with tabs. + * + * Arranges child elements in HLCRF layout: + * - H: Title bar with refresh button + * - H-L: Navigation tabs + * - C: Active tab content (one of the child elements) + * - F: Status bar (connection state, last refresh) + */ +@customElement('core-process-panel') +export class ProcessPanel extends LitElement { + static styles = css` + :host { + display: flex; + flex-direction: column; + font-family: system-ui, -apple-system, sans-serif; + height: 100%; + background: #fafafa; + } + + /* H — Header */ + .header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem 1rem; + background: #fff; + border-bottom: 1px solid #e5e7eb; + } + + .title { + font-weight: 700; + font-size: 1rem; + colour: #111827; + } + + .refresh-btn { + padding: 0.375rem 0.75rem; + border: 1px solid #d1d5db; + border-radius: 0.375rem; + background: #fff; + font-size: 0.8125rem; + cursor: pointer; + transition: background 0.15s; + } + + .refresh-btn:hover { + background: #f3f4f6; + } + + /* H-L — Tabs */ + .tabs { + display: flex; + gap: 0; + background: #fff; + border-bottom: 1px solid #e5e7eb; + padding: 0 1rem; + } + + .tab { + padding: 0.625rem 1rem; + font-size: 0.8125rem; + font-weight: 500; + colour: #6b7280; + cursor: pointer; + border-bottom: 2px solid transparent; + transition: all 0.15s; + background: none; + border-top: none; + border-left: none; + border-right: none; + } + + .tab:hover { + colour: #374151; + } + + .tab.active { + colour: #6366f1; + border-bottom-colour: #6366f1; + } + + /* C — Content */ + .content { + flex: 1; + padding: 1rem; + overflow-y: auto; + } + + /* F — Footer / Status bar */ + .footer { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem 1rem; + background: #fff; + border-top: 1px solid #e5e7eb; + font-size: 0.75rem; + colour: #9ca3af; + } + + .ws-status { + display: flex; + align-items: center; + gap: 0.375rem; + } + + .ws-dot { + width: 0.5rem; + height: 0.5rem; + border-radius: 50%; + } + + .ws-dot.connected { + background: #22c55e; + } + + .ws-dot.disconnected { + background: #ef4444; + } + + .ws-dot.idle { + background: #d1d5db; + } + `; + + @property({ attribute: 'api-url' }) apiUrl = ''; + @property({ attribute: 'ws-url' }) wsUrl = ''; + + @state() private activeTab: TabId = 'daemons'; + @state() private wsConnected = false; + @state() private lastEvent = ''; + @state() private selectedProcessId = ''; + + private ws: WebSocket | null = null; + + connectedCallback() { + super.connectedCallback(); + if (this.wsUrl) { + this.connectWs(); + } + } + + disconnectedCallback() { + super.disconnectedCallback(); + if (this.ws) { + this.ws.close(); + this.ws = null; + } + } + + private connectWs() { + this.ws = connectProcessEvents(this.wsUrl, (event: ProcessEvent) => { + this.lastEvent = event.channel ?? event.type ?? ''; + this.requestUpdate(); + }); + this.ws.onopen = () => { + this.wsConnected = true; + }; + this.ws.onclose = () => { + this.wsConnected = false; + }; + } + + private handleTabClick(tab: TabId) { + this.activeTab = tab; + } + + private handleRefresh() { + const content = this.shadowRoot?.querySelector('.content'); + if (content) { + const child = content.firstElementChild; + if (child && 'loadDaemons' in child) { + (child as any).loadDaemons(); + } else if (child && 'loadProcesses' in child) { + (child as any).loadProcesses(); + } else if (child && 'loadResults' in child) { + (child as any).loadResults(); + } + } + } + + private handleProcessSelected(e: CustomEvent<{ id: string }>) { + this.selectedProcessId = e.detail.id; + } + + private renderContent() { + switch (this.activeTab) { + case 'daemons': + return html``; + case 'processes': + return html` + + ${this.selectedProcessId + ? html`` + : nothing} + `; + case 'pipelines': + return html``; + default: + return nothing; + } + } + + private tabs: { id: TabId; label: string }[] = [ + { id: 'daemons', label: 'Daemons' }, + { id: 'processes', label: 'Processes' }, + { id: 'pipelines', label: 'Pipelines' }, + ]; + + render() { + const wsState = this.wsUrl + ? this.wsConnected + ? 'connected' + : 'disconnected' + : 'idle'; + + return html` +
+ Process Manager + +
+ +
+ ${this.tabs.map( + (tab) => html` + + `, + )} +
+ +
${this.renderContent()}
+ + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'core-process-panel': ProcessPanel; + } +} diff --git a/ui/src/process-runner.ts b/ui/src/process-runner.ts new file mode 100644 index 0000000..e824eef --- /dev/null +++ b/ui/src/process-runner.ts @@ -0,0 +1,325 @@ +// SPDX-Licence-Identifier: EUPL-1.2 + +import { LitElement, html, css, nothing } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; +import type { RunResult, RunAllResult } from './shared/api.js'; + +/** + * — Pipeline execution status display. + * + * Shows RunSpec execution results with pass/fail/skip badges, duration, + * dependency chains, and aggregate summary. + * + * Note: Pipeline runner REST endpoints are not yet in the provider. + * This element renders from WS events and accepts data via properties + * until those endpoints are available. + */ +@customElement('core-process-runner') +export class ProcessRunner extends LitElement { + static styles = css` + :host { + display: block; + font-family: system-ui, -apple-system, sans-serif; + } + + .summary { + display: flex; + gap: 1rem; + padding: 0.75rem 1rem; + background: #fff; + border: 1px solid #e5e7eb; + border-radius: 0.5rem; + margin-bottom: 1rem; + align-items: center; + } + + .summary-stat { + display: flex; + flex-direction: column; + align-items: center; + } + + .summary-value { + font-weight: 700; + font-size: 1.25rem; + } + + .summary-label { + font-size: 0.6875rem; + colour: #6b7280; + text-transform: uppercase; + letter-spacing: 0.025em; + } + + .summary-value.passed { + colour: #166534; + } + + .summary-value.failed { + colour: #991b1b; + } + + .summary-value.skipped { + colour: #92400e; + } + + .summary-duration { + margin-left: auto; + font-size: 0.8125rem; + colour: #6b7280; + } + + .overall-badge { + font-size: 0.75rem; + padding: 0.25rem 0.75rem; + border-radius: 1rem; + font-weight: 600; + } + + .overall-badge.success { + background: #dcfce7; + colour: #166534; + } + + .overall-badge.failure { + background: #fef2f2; + colour: #991b1b; + } + + .list { + display: flex; + flex-direction: column; + gap: 0.375rem; + } + + .spec { + border: 1px solid #e5e7eb; + border-radius: 0.5rem; + padding: 0.75rem 1rem; + background: #fff; + transition: box-shadow 0.15s; + } + + .spec:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + } + + .spec-header { + display: flex; + justify-content: space-between; + align-items: center; + } + + .spec-name { + font-weight: 600; + font-size: 0.9375rem; + display: flex; + align-items: center; + gap: 0.5rem; + } + + .spec-meta { + font-size: 0.75rem; + colour: #6b7280; + margin-top: 0.25rem; + display: flex; + gap: 1rem; + } + + .result-badge { + font-size: 0.6875rem; + padding: 0.125rem 0.5rem; + border-radius: 1rem; + font-weight: 600; + } + + .result-badge.passed { + background: #dcfce7; + colour: #166534; + } + + .result-badge.failed { + background: #fef2f2; + colour: #991b1b; + } + + .result-badge.skipped { + background: #fef3c7; + colour: #92400e; + } + + .duration { + font-family: monospace; + font-size: 0.75rem; + colour: #6b7280; + } + + .deps { + font-size: 0.6875rem; + colour: #9ca3af; + } + + .spec-output { + margin-top: 0.5rem; + padding: 0.5rem 0.75rem; + background: #1e1e1e; + border-radius: 0.375rem; + font-family: 'SF Mono', 'Monaco', 'Menlo', 'Consolas', monospace; + font-size: 0.75rem; + line-height: 1.5; + colour: #d4d4d4; + white-space: pre-wrap; + word-break: break-all; + max-height: 12rem; + overflow-y: auto; + } + + .spec-error { + margin-top: 0.375rem; + font-size: 0.75rem; + colour: #dc2626; + } + + .toggle-output { + font-size: 0.6875rem; + colour: #6366f1; + cursor: pointer; + background: none; + border: none; + padding: 0; + margin-top: 0.375rem; + } + + .toggle-output:hover { + text-decoration: underline; + } + + .empty { + text-align: center; + padding: 2rem; + colour: #9ca3af; + font-size: 0.875rem; + } + + .info-notice { + padding: 0.75rem; + background: #eff6ff; + border: 1px solid #bfdbfe; + border-radius: 0.375rem; + font-size: 0.8125rem; + colour: #1e40af; + margin-bottom: 1rem; + } + `; + + @property({ attribute: 'api-url' }) apiUrl = ''; + @property({ type: Object }) result: RunAllResult | null = null; + + @state() private expandedOutputs = new Set(); + + connectedCallback() { + super.connectedCallback(); + this.loadResults(); + } + + async loadResults() { + // Pipeline runner REST endpoints are not yet available. + // Results can be passed in via the `result` property. + } + + private toggleOutput(name: string) { + const next = new Set(this.expandedOutputs); + if (next.has(name)) { + next.delete(name); + } else { + next.add(name); + } + this.expandedOutputs = next; + } + + private formatDuration(ns: number): string { + if (ns < 1_000_000) return `${(ns / 1000).toFixed(0)}\u00b5s`; + if (ns < 1_000_000_000) return `${(ns / 1_000_000).toFixed(0)}ms`; + return `${(ns / 1_000_000_000).toFixed(2)}s`; + } + + private resultStatus(r: RunResult): string { + if (r.skipped) return 'skipped'; + if (r.passed) return 'passed'; + return 'failed'; + } + + render() { + if (!this.result) { + return html` +
+ Pipeline runner endpoints are pending. Pass pipeline results via the + result property, or results will appear here once the REST + API for pipeline execution is available. +
+
No pipeline results.
+ `; + } + + const { results, duration, passed, failed, skipped, success } = this.result; + + return html` +
+ + ${success ? 'Passed' : 'Failed'} + +
+ ${passed} + Passed +
+
+ ${failed} + Failed +
+
+ ${skipped} + Skipped +
+ ${this.formatDuration(duration)} +
+ +
+ ${results.map( + (r) => html` +
+
+
+ ${r.name} + ${this.resultStatus(r)} +
+ ${this.formatDuration(r.duration)} +
+
+ ${r.exitCode !== 0 && !r.skipped + ? html`exit ${r.exitCode}` + : nothing} +
+ ${r.error ? html`
${r.error}
` : nothing} + ${r.output + ? html` + + ${this.expandedOutputs.has(r.name) + ? html`
${r.output}
` + : nothing} + ` + : nothing} +
+ `, + )} +
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'core-process-runner': ProcessRunner; + } +} diff --git a/ui/src/shared/api.ts b/ui/src/shared/api.ts new file mode 100644 index 0000000..bd74a09 --- /dev/null +++ b/ui/src/shared/api.ts @@ -0,0 +1,105 @@ +// SPDX-Licence-Identifier: EUPL-1.2 + +/** + * Daemon entry as returned by the process registry REST API. + */ +export interface DaemonEntry { + code: string; + daemon: string; + pid: number; + health?: string; + project?: string; + binary?: string; + started: string; +} + +/** + * Health check result from the daemon health endpoint. + */ +export interface HealthResult { + healthy: boolean; + address: string; + reason?: string; +} + +/** + * Process info snapshot as returned by the process service. + */ +export interface ProcessInfo { + id: string; + command: string; + args: string[]; + dir: string; + startedAt: string; + status: 'pending' | 'running' | 'exited' | 'failed' | 'killed'; + exitCode: number; + duration: number; + pid: number; +} + +/** + * Pipeline run result for a single spec. + */ +export interface RunResult { + name: string; + exitCode: number; + duration: number; + output: string; + error?: string; + skipped: boolean; + passed: boolean; +} + +/** + * Aggregate pipeline run result. + */ +export interface RunAllResult { + results: RunResult[]; + duration: number; + passed: number; + failed: number; + skipped: number; + success: boolean; +} + +/** + * ProcessApi provides a typed fetch wrapper for the /api/process/* endpoints. + */ +export class ProcessApi { + constructor(private baseUrl: string = '') {} + + private get base(): string { + return `${this.baseUrl}/api/process`; + } + + private async request(path: string, opts?: RequestInit): Promise { + const res = await fetch(`${this.base}${path}`, opts); + const json = await res.json(); + if (!json.success) { + throw new Error(json.error?.message ?? 'Request failed'); + } + return json.data as T; + } + + /** List all alive daemons from the registry. */ + listDaemons(): Promise { + return this.request('/daemons'); + } + + /** Get a single daemon entry by code and daemon name. */ + getDaemon(code: string, daemon: string): Promise { + return this.request(`/daemons/${code}/${daemon}`); + } + + /** Stop a daemon (SIGTERM + unregister). */ + stopDaemon(code: string, daemon: string): Promise<{ stopped: boolean }> { + return this.request<{ stopped: boolean }>(`/daemons/${code}/${daemon}/stop`, { + method: 'POST', + }); + } + + /** Check daemon health endpoint. */ + healthCheck(code: string, daemon: string): Promise { + return this.request(`/daemons/${code}/${daemon}/health`); + } +} diff --git a/ui/src/shared/events.ts b/ui/src/shared/events.ts new file mode 100644 index 0000000..715b2e1 --- /dev/null +++ b/ui/src/shared/events.ts @@ -0,0 +1,32 @@ +// SPDX-Licence-Identifier: EUPL-1.2 + +export interface ProcessEvent { + type: string; + channel?: string; + data?: any; + timestamp?: string; +} + +/** + * Connects to a WebSocket endpoint and dispatches process events to a handler. + * Returns the WebSocket instance for lifecycle management. + */ +export function connectProcessEvents( + wsUrl: string, + handler: (event: ProcessEvent) => void, +): WebSocket { + const ws = new WebSocket(wsUrl); + + ws.onmessage = (e: MessageEvent) => { + try { + const event: ProcessEvent = JSON.parse(e.data); + if (event.type?.startsWith?.('process.') || event.channel?.startsWith?.('process.')) { + handler(event); + } + } catch { + // Ignore malformed messages + } + }; + + return ws; +} diff --git a/ui/tsconfig.json b/ui/tsconfig.json new file mode 100644 index 0000000..e7540a6 --- /dev/null +++ b/ui/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2021", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2021", "DOM", "DOM.Iterable"], + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "experimentalDecorators": true, + "useDefineForClassFields": false, + "declaration": true, + "sourceMap": true, + "isolatedModules": true + }, + "include": ["src"] +} diff --git a/ui/vite.config.ts b/ui/vite.config.ts new file mode 100644 index 0000000..33f4896 --- /dev/null +++ b/ui/vite.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from 'vite'; +import { resolve } from 'path'; + +export default defineConfig({ + build: { + lib: { + entry: resolve(__dirname, 'src/index.ts'), + name: 'CoreProcess', + fileName: 'core-process', + formats: ['es'], + }, + outDir: resolve(__dirname, '../pkg/api/ui/dist'), + emptyOutDir: true, + rollupOptions: { + output: { + entryFileNames: 'core-process.js', + }, + }, + }, +});