@floless/app 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/floless-server.cjs +43 -2
- package/dist/skills/floless-app-workflows/SKILL.md +20 -0
- package/dist/web/app.css +4 -0
- package/dist/web/aware.js +191 -6
- package/dist/web/index.html +1 -0
- package/package.json +1 -1
package/dist/floless-server.cjs
CHANGED
|
@@ -52609,7 +52609,7 @@ function appVersion() {
|
|
|
52609
52609
|
return resolveVersion({
|
|
52610
52610
|
isSea: isSea2(),
|
|
52611
52611
|
sqVersionXml: readSqVersionXml(),
|
|
52612
|
-
define: true ? "0.
|
|
52612
|
+
define: true ? "0.15.0" : void 0,
|
|
52613
52613
|
pkgVersion: readPkgVersion()
|
|
52614
52614
|
});
|
|
52615
52615
|
}
|
|
@@ -52619,7 +52619,7 @@ function resolveChannel(s) {
|
|
|
52619
52619
|
return "dev";
|
|
52620
52620
|
}
|
|
52621
52621
|
function appChannel() {
|
|
52622
|
-
return resolveChannel({ isSea: isSea2(), define: true ? "0.
|
|
52622
|
+
return resolveChannel({ isSea: isSea2(), define: true ? "0.15.0" : void 0 });
|
|
52623
52623
|
}
|
|
52624
52624
|
|
|
52625
52625
|
// oauth-presets.ts
|
|
@@ -53211,6 +53211,9 @@ function hostRunBlocked(agent, agentsDir) {
|
|
|
53211
53211
|
if (!hostWatcherActive()) return null;
|
|
53212
53212
|
return "a host watcher is live for this host \u2014 stop it before running manually (one live model = one consumer)";
|
|
53213
53213
|
}
|
|
53214
|
+
function foregroundSessionId(appId) {
|
|
53215
|
+
return `__run__:${appId}`;
|
|
53216
|
+
}
|
|
53214
53217
|
function startSession(spec, deps = defaultDeps) {
|
|
53215
53218
|
if (spec.hostBacked && hostWatcherActive(spec.id)) {
|
|
53216
53219
|
return { ok: false, reason: "another host watcher is live \u2014 stop it first (one live model = one watcher)" };
|
|
@@ -53883,6 +53886,8 @@ function isGatedAwareRoute(url, method) {
|
|
|
53883
53886
|
if (path === "/api/routines" || path.startsWith("/api/routines/")) {
|
|
53884
53887
|
return m === "POST" || m === "PATCH";
|
|
53885
53888
|
}
|
|
53889
|
+
if (path.startsWith("/api/trigger-run/")) return false;
|
|
53890
|
+
if (path === "/api/trigger-run") return m === "POST";
|
|
53886
53891
|
return AWARE_ROUTES.some((p) => path.startsWith(p));
|
|
53887
53892
|
}
|
|
53888
53893
|
|
|
@@ -57363,6 +57368,42 @@ async function startServer() {
|
|
|
57363
57368
|
const running = cancelActiveRun();
|
|
57364
57369
|
return { ok: true, running };
|
|
57365
57370
|
});
|
|
57371
|
+
app.post(
|
|
57372
|
+
"/api/trigger-run",
|
|
57373
|
+
async (req, reply) => {
|
|
57374
|
+
const { id, inputs } = req.body ?? {};
|
|
57375
|
+
if (!id) return reply.status(400).send({ ok: false, error: "id required" });
|
|
57376
|
+
let appSpec;
|
|
57377
|
+
try {
|
|
57378
|
+
appSpec = readApp(id);
|
|
57379
|
+
} catch (err) {
|
|
57380
|
+
if (err instanceof AppNotFoundError) return reply.status(404).send({ ok: false, error: "app not found" });
|
|
57381
|
+
throw err;
|
|
57382
|
+
}
|
|
57383
|
+
if (!appSpec.triggerSource) {
|
|
57384
|
+
return reply.status(400).send({ ok: false, error: "not a streaming workflow \u2014 use \u25B6 Run for a one-shot" });
|
|
57385
|
+
}
|
|
57386
|
+
if (!appSpec.runnable) {
|
|
57387
|
+
return reply.send({ ok: false, error: "needs Compile \u2014 Run is gated on a fresh lock", blocked: true });
|
|
57388
|
+
}
|
|
57389
|
+
const sessionId = foregroundSessionId(id);
|
|
57390
|
+
const res = startSession({
|
|
57391
|
+
id: sessionId,
|
|
57392
|
+
workflow: id,
|
|
57393
|
+
sourceNodeId: appSpec.triggerSource.nodeId,
|
|
57394
|
+
hostBacked: isHostBacked(appSpec.triggerSource.agent),
|
|
57395
|
+
inputs
|
|
57396
|
+
});
|
|
57397
|
+
if (!res.ok) return reply.send({ ok: false, error: res.reason, blocked: true });
|
|
57398
|
+
return { ok: true, sessionId };
|
|
57399
|
+
}
|
|
57400
|
+
);
|
|
57401
|
+
app.post("/api/trigger-run/:id/stop", async (req, reply) => {
|
|
57402
|
+
const id = req.params.id;
|
|
57403
|
+
if (!/^[A-Za-z0-9._-]+$/.test(id)) return reply.status(400).send({ ok: false, error: "invalid app id" });
|
|
57404
|
+
await stopSession(foregroundSessionId(id));
|
|
57405
|
+
return { ok: true };
|
|
57406
|
+
});
|
|
57366
57407
|
app.get("/api/latest-run/:id", async (req, reply) => {
|
|
57367
57408
|
const id = req.params.id;
|
|
57368
57409
|
if (!/^[A-Za-z0-9._-]+$/.test(id)) {
|
|
@@ -258,6 +258,26 @@ manifest edit + recompile, no engine. See the design doc for the Stage-1 plan.
|
|
|
258
258
|
it renders the returned `html` in the HTML Viewer and caches it; otherwise it fills the Execution
|
|
259
259
|
trace. The secondary **`Simulate`** header button is `simulate:true` (every node stubbed from its
|
|
260
260
|
output-schema, no host) — a composition check.
|
|
261
|
+
- **▶ Run on a STREAMING/watch app = a LIVE FOREGROUND session** (not the finite run). When the
|
|
262
|
+
open app's source node is a `lifecycle:start` watch (`app.triggerSource != null`), the finite
|
|
263
|
+
`/api/run` never completes (a stream doesn't end → 60 s timeout). Instead Run starts an
|
|
264
|
+
**ephemeral** trigger session — `POST /api/trigger-run {id,inputs}` / `POST
|
|
265
|
+
/api/trigger-run/:id/stop` — that re-renders the report in the HTML Viewer on **every** fired
|
|
266
|
+
event (e.g. each Tekla selection change) until Stop, reusing the background-routine machinery
|
|
267
|
+
(`server/trigger-sessions.ts`: long-lived `aware app run` OUTSIDE the run lock, host-exclusivity
|
|
268
|
+
guard, per-event report HTML on the `trigger-report` SSE). The session id is namespaced
|
|
269
|
+
`foregroundSessionId()` → `__run__:<app>` so it can't collide with a routine id in the shared SSE
|
|
270
|
+
id space; `route-gate` gates the spawning POST and exempts the `/stop` teardown. Per-event repaint
|
|
271
|
+
reuses the existing `applyTriggerReport`→`paintReport` path (gated on `workflow === currentId`) —
|
|
272
|
+
**zero** new render plumbing. The watch node's `once:true` (AWARE `tekla.watch` input) makes the
|
|
273
|
+
same session fire once then exit ("Run complete"); `once:false` (default) streams continuously.
|
|
274
|
+
**Two UI gotchas a real Playwright pass caught (inference missed both):** (1) the always-on header
|
|
275
|
+
**■ Stop run is OCCLUDED by the report-modal backdrop**, so a live session shown in the Viewer
|
|
276
|
+
needs its OWN reachable Stop — the in-modal `#report-stop` (`web/index.html` action row,
|
|
277
|
+
warn-toned); (2) `paintGate()` recomputes Run-disabled / Stop-hidden on **every** gate repaint
|
|
278
|
+
(an fs-change → `loadApp` mid-session repaints), so it MUST include the live-session flag
|
|
279
|
+
(`foregroundTrigger`) exactly like `syncRunControls`, or a repaint re-arms Run and hides Stop
|
|
280
|
+
while the watcher is still live.
|
|
261
281
|
- **Per-node run status on the canvas + a Stop button.** During a run each node card paints
|
|
262
282
|
running → ✓ done / ✗ failed from the trace (`pushTrace`), and the report-run overlay shows a
|
|
263
283
|
**Stop** button. See `references/dev-server-and-run-trace.md` for the trace event kinds, the live
|
package/dist/web/app.css
CHANGED
|
@@ -2056,6 +2056,10 @@ body {
|
|
|
2056
2056
|
.report-actions button { padding: 6px 12px; }
|
|
2057
2057
|
.report-actions button.ghost { background: var(--surface-3); color: var(--text-muted); }
|
|
2058
2058
|
.report-actions button.ghost:hover { color: var(--text); border-color: var(--accent-dim); }
|
|
2059
|
+
/* The in-modal live-session Stop reads as a stop (warn-toned), matching the header
|
|
2060
|
+
■ Stop run — not a neutral ghost link like Share/Open. Existing --warn token only. */
|
|
2061
|
+
.report-actions button.report-stop { background: transparent; color: var(--warn); border: 1px solid color-mix(in srgb, var(--warn) 45%, transparent); font-weight: 600; letter-spacing: 0.04em; }
|
|
2062
|
+
.report-actions button.report-stop:hover { background: color-mix(in srgb, var(--warn) 16%, transparent); border-color: var(--warn); color: var(--text); }
|
|
2059
2063
|
#report-close { font-size: 18px; line-height: 1; padding: 4px 10px; }
|
|
2060
2064
|
.report-stage { position: relative; flex: 1; background: #0f172a; }
|
|
2061
2065
|
#report-frame { width: 100%; height: 100%; border: 0; display: block; background: #0f172a; }
|
package/dist/web/aware.js
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
const $reportOpen = document.getElementById('report-open');
|
|
25
25
|
const $reportShare = document.getElementById('report-share');
|
|
26
26
|
const $reportClose = document.getElementById('report-close');
|
|
27
|
+
const $reportStop = document.getElementById('report-stop');
|
|
27
28
|
const $stopRunBtn = document.getElementById('stop-run-btn');
|
|
28
29
|
|
|
29
30
|
// One persistent array the inspect Execution tab reads; we mutate in place so
|
|
@@ -330,8 +331,13 @@
|
|
|
330
331
|
|
|
331
332
|
// Keep Run disabled (and the header Stop shown) if a gate re-paint lands mid-run (#39) —
|
|
332
333
|
// never let a re-render re-arm Run while a run is still in flight.
|
|
333
|
-
|
|
334
|
-
|
|
334
|
+
// …including a live foreground trigger session (▶ Run on a streaming app): a
|
|
335
|
+
// gate repaint mid-session (e.g. an fs-change → loadApp) must not re-arm Run or
|
|
336
|
+
// hide the Stop while the watcher is still live. The in-modal Stop tracks the
|
|
337
|
+
// same state (it's the reachable Stop while the Viewer covers the header one).
|
|
338
|
+
$runBtn.disabled = !app.runnable || reportRunning || state.running || !!foregroundTrigger;
|
|
339
|
+
if ($stopRunBtn) $stopRunBtn.hidden = !(reportRunning || state.running || foregroundTrigger);
|
|
340
|
+
if ($reportStop) $reportStop.hidden = !foregroundTrigger;
|
|
335
341
|
$runBtn.dataset.tip = app.runnable
|
|
336
342
|
? 'Run the approved workflow'
|
|
337
343
|
: app.runState === 'drift'
|
|
@@ -911,6 +917,16 @@
|
|
|
911
917
|
// double-click LOAD the last result instantly instead of re-running.
|
|
912
918
|
const lastReportByApp = new Map();
|
|
913
919
|
|
|
920
|
+
// A live FOREGROUND trigger session — ▶ Run on a streaming/watch app (one whose
|
|
921
|
+
// source is a lifecycle:start watch). The finite run can't complete for a stream,
|
|
922
|
+
// so Run hosts a live session that re-renders the report in the Viewer on every
|
|
923
|
+
// fired event until Stop. `{ appId, sessionId, firedCount }` while live, else null.
|
|
924
|
+
// Distinct from a background routine (⏱ Routines) and the finite report run
|
|
925
|
+
// (reportRunning). Its session id is server-namespaced with this prefix so its SSE
|
|
926
|
+
// snapshots route here, never to the Routines list.
|
|
927
|
+
const FG_SESSION_PREFIX = '__run__:';
|
|
928
|
+
let foregroundTrigger = null;
|
|
929
|
+
|
|
914
930
|
// Reflect run-in-flight state in the ALWAYS-VISIBLE header so a run is stoppable even
|
|
915
931
|
// when the HTML Viewer modal is closed (#39). The header ■ Stop run appears for the whole
|
|
916
932
|
// duration of either run path (the modal run `reportRunning` and the inline run
|
|
@@ -919,7 +935,10 @@
|
|
|
919
935
|
// the user, on the modal's × tooltip, that closing leaves the run going (Stop is in the
|
|
920
936
|
// header) — the canvas keeps showing progress (markCanvasRunning), by design.
|
|
921
937
|
function syncRunControls() {
|
|
922
|
-
const running = reportRunning || state.running;
|
|
938
|
+
const running = reportRunning || state.running || !!foregroundTrigger;
|
|
939
|
+
// The in-modal Stop is reachable only for a live foreground session (the header
|
|
940
|
+
// Stop is occluded by the report-modal backdrop). Show it while one is live.
|
|
941
|
+
if ($reportStop) $reportStop.hidden = !foregroundTrigger;
|
|
923
942
|
if ($stopRunBtn) {
|
|
924
943
|
$stopRunBtn.hidden = !running;
|
|
925
944
|
// Reset to a fresh, clickable Stop each run; stopRun() flips it to "Cancelling…".
|
|
@@ -945,7 +964,22 @@
|
|
|
945
964
|
// pulse, and asks the server to kill the `aware app run` child. The in-flight
|
|
946
965
|
// /api/run then rejects via api() and the catch renders the cancelled state.
|
|
947
966
|
async function stopRun() {
|
|
948
|
-
if (!reportRunning && !state.running) return;
|
|
967
|
+
if (!reportRunning && !state.running && !foregroundTrigger) return;
|
|
968
|
+
// A live foreground session ends via its own stop route (it runs outside the
|
|
969
|
+
// run lock, so /api/run/stop would no-op). Clear the flag first so the late
|
|
970
|
+
// `stopped` SSE snapshot is a harmless no-op rather than a double-handle.
|
|
971
|
+
if (foregroundTrigger) {
|
|
972
|
+
const ft = foregroundTrigger;
|
|
973
|
+
foregroundTrigger = null;
|
|
974
|
+
if ($stopRunBtn) { $stopRunBtn.disabled = true; $stopRunBtn.textContent = 'Stopping…'; }
|
|
975
|
+
const ovStop = document.querySelector('.overlay-stop');
|
|
976
|
+
if (ovStop) { ovStop.disabled = true; ovStop.textContent = 'Stopping…'; }
|
|
977
|
+
try { await api(`/api/trigger-run/${encodeURIComponent(ft.appId)}/stop`, { method: 'POST' }); } catch { /* best effort — the session may already be gone */ }
|
|
978
|
+
// A deliberate stop needs no toast — the subtitle change is the confirmation.
|
|
979
|
+
paintForegroundStopped('stopped');
|
|
980
|
+
syncRunControls();
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
949
983
|
cancelRequested = true;
|
|
950
984
|
clearNodeStatus();
|
|
951
985
|
// Reflect "cancelling" on BOTH Stop surfaces — the in-modal overlay and the header.
|
|
@@ -1308,6 +1342,10 @@
|
|
|
1308
1342
|
});
|
|
1309
1343
|
|
|
1310
1344
|
$reportClose.onclick = () => hideModal($reportModal);
|
|
1345
|
+
// The in-modal Stop for a live foreground session: the always-on header Stop sits
|
|
1346
|
+
// BEHIND the report modal's backdrop, so a session shown in the Viewer needs its
|
|
1347
|
+
// own reachable Stop. Visibility is driven by syncRunControls (foregroundTrigger).
|
|
1348
|
+
if ($reportStop) $reportStop.onclick = () => stopRun();
|
|
1311
1349
|
$reportModal.addEventListener('click', (e) => { if (e.target === $reportModal) hideModal($reportModal); });
|
|
1312
1350
|
// The Stop button is rebuilt into the overlay each run — delegate so one
|
|
1313
1351
|
// listener survives every innerHTML swap.
|
|
@@ -2127,7 +2165,12 @@
|
|
|
2127
2165
|
const app = currentId && apps.get(currentId);
|
|
2128
2166
|
if (!app) return;
|
|
2129
2167
|
if (!app.runnable) { showToast($runBtn.dataset.tip, 'warn'); return; }
|
|
2130
|
-
if (state.running || reportRunning) return;
|
|
2168
|
+
if (state.running || reportRunning || foregroundTrigger) return;
|
|
2169
|
+
|
|
2170
|
+
// Streaming/watch app → a LIVE FOREGROUND session, not a finite run (which would
|
|
2171
|
+
// never complete and time out at 60s). It re-renders the report in the Viewer on
|
|
2172
|
+
// every fired event until Stop. (Simulate still takes the normal stubbed path.)
|
|
2173
|
+
if (!simulate && app.triggerSource) { startForegroundTrigger(app); return; }
|
|
2131
2174
|
|
|
2132
2175
|
markCanvasRunning(); // immediate during-run feedback; the trace refines per node
|
|
2133
2176
|
|
|
@@ -2782,7 +2825,11 @@
|
|
|
2782
2825
|
loadRoutinesData();
|
|
2783
2826
|
if (window.flolessPanels) window.flolessPanels.refreshData(); // routine runs also refresh bindings
|
|
2784
2827
|
} else if (m.type === 'trigger-session-changed') {
|
|
2785
|
-
|
|
2828
|
+
// A foreground live-run session (▶ Run on a streaming app) shares this SSE
|
|
2829
|
+
// event with background routines; its server-namespaced id routes it to the
|
|
2830
|
+
// Viewer handler instead of the Routines list.
|
|
2831
|
+
if (typeof m.id === 'string' && m.id.startsWith(FG_SESSION_PREFIX)) applyForegroundSnapshot(m.id, m.snapshot);
|
|
2832
|
+
else applyTriggerSnapshot(m.id, m.snapshot);
|
|
2786
2833
|
} else if (m.type === 'trigger-report') {
|
|
2787
2834
|
applyTriggerReport(m);
|
|
2788
2835
|
} else if (m.type === 'connect-result') {
|
|
@@ -3489,6 +3536,144 @@
|
|
|
3489
3536
|
$reportOverlay.append(mkEl('div', null, 'Listening for the next selection…'));
|
|
3490
3537
|
}
|
|
3491
3538
|
|
|
3539
|
+
// ── Foreground live run (▶ Run on a streaming/watch app) ──────────────────────
|
|
3540
|
+
// Start a live session: the Viewer re-renders the report on every fired event
|
|
3541
|
+
// (e.g. each Tekla selection change) until Stop. Reuses the exact trigger-report /
|
|
3542
|
+
// trigger-session-changed SSE path the background routine already drives — the
|
|
3543
|
+
// server hosts the long-lived `aware app run`; here we open the Viewer, surface
|
|
3544
|
+
// Stop, and reflect the live state. We never compose HTML — the Viewer relays
|
|
3545
|
+
// exactly what the node returns (applyTriggerReport paints each fresh report).
|
|
3546
|
+
async function startForegroundTrigger(app) {
|
|
3547
|
+
if (foregroundTrigger) return;
|
|
3548
|
+
const id = currentId;
|
|
3549
|
+
$reportTitle.textContent = `HTML Viewer · ${app.displayName}`;
|
|
3550
|
+
$reportModal.dataset.nodeId = reportNodeId() || '';
|
|
3551
|
+
const cached = lastReportByApp.get(id);
|
|
3552
|
+
if (cached && cached.html) paintReport(cached.html);
|
|
3553
|
+
else { $reportFrame.srcdoc = ''; $reportShare.hidden = true; }
|
|
3554
|
+
$reportOverlay.hidden = false;
|
|
3555
|
+
$reportOverlay.replaceChildren();
|
|
3556
|
+
$reportOverlay.append(mkEl('div', 'spinner'), mkEl('div', null, 'Starting live session…'));
|
|
3557
|
+
$reportSub.innerHTML = `Live run of <code>${escapeHtml(id)}</code> — the report re-renders on every event.`;
|
|
3558
|
+
showModal($reportModal);
|
|
3559
|
+
try {
|
|
3560
|
+
const res = await api('/api/trigger-run', { method: 'POST', body: JSON.stringify({ id, inputs: currentInputs() }) });
|
|
3561
|
+
foregroundTrigger = { appId: id, sessionId: res.sessionId, firedCount: 0 };
|
|
3562
|
+
syncRunControls(); // header ■ Stop run becomes reachable; Run disabled while live
|
|
3563
|
+
paintForegroundListening();
|
|
3564
|
+
appendNarration(`Live session started for <strong>${escapeHtml(id)}</strong> — the report updates on every event. Click <strong>■ Stop run</strong> (top right) to end it.`);
|
|
3565
|
+
} catch (e) {
|
|
3566
|
+
const msg = (e && e.message) ? e.message : String(e);
|
|
3567
|
+
// A refusal (a watcher already holds this host) names the concrete path back —
|
|
3568
|
+
// never a dead end. Other failures are recoverable; the server returns them
|
|
3569
|
+
// in-band, so a failed start is a viewer message + toast, not a console fault.
|
|
3570
|
+
const blocked = !!(e.body && e.body.blocked);
|
|
3571
|
+
$reportShare.hidden = true; // the overlay is the primary content — nothing to share
|
|
3572
|
+
$reportOverlay.hidden = false;
|
|
3573
|
+
$reportOverlay.replaceChildren();
|
|
3574
|
+
if (blocked) {
|
|
3575
|
+
$reportOverlay.append(
|
|
3576
|
+
mkEl('div', null, 'One live model, one watcher — a session is already watching this host.'),
|
|
3577
|
+
mkEl('div', null, 'To run here, stop it first: open ⏱ Routines and disable the watcher, then click ▶ Run workflow.'),
|
|
3578
|
+
);
|
|
3579
|
+
$reportSub.textContent = 'Live run blocked — a watcher already holds this host.';
|
|
3580
|
+
showToast('Blocked — stop the active watcher in ⏱ Routines first', 'warn');
|
|
3581
|
+
} else {
|
|
3582
|
+
$reportOverlay.append(mkEl('div', null, `Couldn't start the live session: ${msg}. Check the host is attached and the lock is fresh, then click ▶ Run workflow.`));
|
|
3583
|
+
$reportSub.textContent = 'Live session failed to start.';
|
|
3584
|
+
showToast('Live run failed: ' + msg, 'warn');
|
|
3585
|
+
}
|
|
3586
|
+
syncRunControls();
|
|
3587
|
+
}
|
|
3588
|
+
}
|
|
3589
|
+
|
|
3590
|
+
// SSE: a foreground live session's snapshot changed. Drives the Viewer's live
|
|
3591
|
+
// state (collecting on each fire; ended on stop/error). Ignores a stale snapshot
|
|
3592
|
+
// for a superseded/stopped session (foregroundTrigger already cleared).
|
|
3593
|
+
function applyForegroundSnapshot(id, snapshot) {
|
|
3594
|
+
if (!foregroundTrigger || id !== foregroundTrigger.sessionId || !snapshot) return;
|
|
3595
|
+
const prevFired = foregroundTrigger.firedCount || 0;
|
|
3596
|
+
foregroundTrigger.firedCount = snapshot.firedCount || 0;
|
|
3597
|
+
const fired = foregroundTrigger.firedCount > prevFired;
|
|
3598
|
+
const viewerOpen = currentId === foregroundTrigger.appId && $reportModal.classList.contains('show');
|
|
3599
|
+
if (snapshot.state === 'error') {
|
|
3600
|
+
foregroundTrigger = null;
|
|
3601
|
+
if (viewerOpen) {
|
|
3602
|
+
$reportShare.hidden = true; // the error overlay is the primary content now
|
|
3603
|
+
$reportOverlay.hidden = false;
|
|
3604
|
+
$reportOverlay.replaceChildren();
|
|
3605
|
+
$reportOverlay.append(mkEl('div', null, `Live session ended with an error: ${snapshot.error || 'the host stopped responding'}. Check Tekla is open and the workflow is compiled, then click ▶ Run workflow to try again.`));
|
|
3606
|
+
$reportSub.textContent = 'Live session error.';
|
|
3607
|
+
}
|
|
3608
|
+
showToast('Live session ended: ' + (snapshot.error || 'host error'), 'warn');
|
|
3609
|
+
syncRunControls();
|
|
3610
|
+
return;
|
|
3611
|
+
}
|
|
3612
|
+
if (snapshot.state === 'stopped') {
|
|
3613
|
+
// The stream ended on its own — a one-shot (`once`) snapshot fired then exited,
|
|
3614
|
+
// or the host closed cleanly. A clean self-exit reads as "Run complete".
|
|
3615
|
+
foregroundTrigger = null;
|
|
3616
|
+
if (viewerOpen) paintForegroundStopped('complete');
|
|
3617
|
+
syncRunControls();
|
|
3618
|
+
return;
|
|
3619
|
+
}
|
|
3620
|
+
// listening: a new fire → "collecting" spinner until applyTriggerReport paints it.
|
|
3621
|
+
if (fired && viewerOpen && !reportRunning) showTriggerCollecting();
|
|
3622
|
+
}
|
|
3623
|
+
|
|
3624
|
+
// The live-but-idle state: the session is listening and no event has fired yet.
|
|
3625
|
+
// A watch app shows nothing until the user next acts in the host, so the copy
|
|
3626
|
+
// DIRECTS that action. If a previous report is cached, keep it visible behind a
|
|
3627
|
+
// live pip (NOT a spinner — nothing is computing); else an instructional prompt
|
|
3628
|
+
// with a Stop. Never a dead end, never a spinner that won't resolve on its own.
|
|
3629
|
+
function paintForegroundListening() {
|
|
3630
|
+
if (!$reportModal.classList.contains('show')) return;
|
|
3631
|
+
const cached = currentId && lastReportByApp.get(currentId);
|
|
3632
|
+
if (cached && cached.html) {
|
|
3633
|
+
$reportOverlay.hidden = true;
|
|
3634
|
+
$reportOverlay.replaceChildren();
|
|
3635
|
+
$reportShare.hidden = false; // the cached report is the primary content
|
|
3636
|
+
$reportSub.innerHTML = `<span class="live-pip" aria-hidden="true"></span>Live · waiting for the next change — change your selection in Tekla to refresh the report.`;
|
|
3637
|
+
} else {
|
|
3638
|
+
$reportFrame.srcdoc = '';
|
|
3639
|
+
$reportShare.hidden = true;
|
|
3640
|
+
$reportOverlay.hidden = false;
|
|
3641
|
+
$reportOverlay.replaceChildren();
|
|
3642
|
+
$reportOverlay.append(
|
|
3643
|
+
mkEl('div', null, 'Listening for events — change your selection in Tekla to generate the first report.'),
|
|
3644
|
+
Object.assign(mkEl('button', 'overlay-stop', '■ Stop run'), { type: 'button' }),
|
|
3645
|
+
);
|
|
3646
|
+
$reportSub.innerHTML = `<span class="live-pip" aria-hidden="true"></span>Live · listening…`;
|
|
3647
|
+
}
|
|
3648
|
+
}
|
|
3649
|
+
|
|
3650
|
+
// The session ended. `reason` distinguishes a clean self-exit ('complete' — e.g. a
|
|
3651
|
+
// one-shot `once` snapshot) from a deliberate Stop ('stopped'). Either way keep the
|
|
3652
|
+
// last report visible; the path back is ▶ Run workflow.
|
|
3653
|
+
function paintForegroundStopped(reason) {
|
|
3654
|
+
if (!$reportModal.classList.contains('show')) return;
|
|
3655
|
+
const complete = reason === 'complete';
|
|
3656
|
+
const cached = currentId && lastReportByApp.get(currentId);
|
|
3657
|
+
if (complete) showToast('Run complete', 'ok');
|
|
3658
|
+
if (cached && cached.html) {
|
|
3659
|
+
$reportOverlay.hidden = true;
|
|
3660
|
+
$reportOverlay.replaceChildren();
|
|
3661
|
+
$reportShare.hidden = false;
|
|
3662
|
+
$reportSub.innerHTML = complete
|
|
3663
|
+
? `Run complete · last report above — click <strong>▶ Run workflow</strong> to run again.`
|
|
3664
|
+
: `Live session stopped · last report above — click <strong>▶ Run workflow</strong> to start again.`;
|
|
3665
|
+
} else {
|
|
3666
|
+
$reportFrame.srcdoc = '';
|
|
3667
|
+
$reportShare.hidden = true;
|
|
3668
|
+
$reportOverlay.hidden = false;
|
|
3669
|
+
$reportOverlay.replaceChildren();
|
|
3670
|
+
$reportOverlay.append(mkEl('div', null, complete
|
|
3671
|
+
? 'Run complete. Click ▶ Run workflow to run again.'
|
|
3672
|
+
: 'Live session stopped. Click ▶ Run workflow to start again.'));
|
|
3673
|
+
$reportSub.textContent = complete ? 'Run complete.' : 'Live session stopped.';
|
|
3674
|
+
}
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3492
3677
|
function renderRoutinesList() {
|
|
3493
3678
|
const $list = document.getElementById('routines-list');
|
|
3494
3679
|
const $quota = document.getElementById('rtn-quota');
|
package/dist/web/index.html
CHANGED
|
@@ -289,6 +289,7 @@
|
|
|
289
289
|
<div class="modal-sub" id="report-sub">Rendered from the live run — never composed by the UI.</div>
|
|
290
290
|
</div>
|
|
291
291
|
<div class="report-actions">
|
|
292
|
+
<button id="report-stop" class="ghost report-stop" hidden data-tip="Stop the live session — the report stops updating on each change">■ Stop run</button>
|
|
292
293
|
<button id="report-share" class="ghost" hidden data-tip="Share this report as a link anyone can view — hosted on floless.io">↗ Share</button>
|
|
293
294
|
<button id="report-open" class="ghost" data-tip="Open the report in a new browser tab">↗ Open</button>
|
|
294
295
|
<button id="report-close" data-tip="Close">×</button>
|