@floless/app 0.19.0 → 0.21.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 +71 -93
- package/dist/web/app.css +3 -1
- package/dist/web/aware.js +86 -22
- package/dist/web/index.html +7 -0
- package/package.json +1 -1
package/dist/floless-server.cjs
CHANGED
|
@@ -50958,6 +50958,17 @@ function appProvider(id) {
|
|
|
50958
50958
|
return "other";
|
|
50959
50959
|
}
|
|
50960
50960
|
}
|
|
50961
|
+
function appBaked(id) {
|
|
50962
|
+
try {
|
|
50963
|
+
const dir = appDir(id);
|
|
50964
|
+
const sourcePath = firstWithExt(dir, ".app") ?? firstWithExt(dir, ".flo");
|
|
50965
|
+
if (!sourcePath) return false;
|
|
50966
|
+
const src = asRecord((0, import_yaml.parse)((0, import_node_fs3.readFileSync)(sourcePath, "utf8")));
|
|
50967
|
+
return src["exposes-as-agent"] === true;
|
|
50968
|
+
} catch {
|
|
50969
|
+
return false;
|
|
50970
|
+
}
|
|
50971
|
+
}
|
|
50961
50972
|
function readApp(id) {
|
|
50962
50973
|
const dir = appDir(id);
|
|
50963
50974
|
const sourcePath = firstWithExt(dir, ".app") ?? firstWithExt(dir, ".flo");
|
|
@@ -51933,6 +51944,41 @@ var aware = {
|
|
|
51933
51944
|
if (code !== 0) throw new AwareError(`aware app uninstall failed (exit ${code})`, { stdout, stderr });
|
|
51934
51945
|
return { output: stdout.trim() };
|
|
51935
51946
|
},
|
|
51947
|
+
/**
|
|
51948
|
+
* Rename an installed app in place (`aware app rename <old> <new>`, CLI ≥ 0.68).
|
|
51949
|
+
* AWARE owns the whole move — the app dir, the source `app:` field, the
|
|
51950
|
+
* regenerated lock (so the Run gate stays green, no drift), and the synthesized
|
|
51951
|
+
* agent of a baked (`exposes-as-agent`) app. `compiled` reflects whether AWARE
|
|
51952
|
+
* refreshed the lock (false → the app needs a manual Compile). Throws
|
|
51953
|
+
* AwareUnsupportedError on a pre-0.68 CLI so app-lifecycle can say "update AWARE"
|
|
51954
|
+
* instead of treating an old CLI as a hard fault.
|
|
51955
|
+
*/
|
|
51956
|
+
async rename(oldId, newId) {
|
|
51957
|
+
const { code, stdout, stderr } = await runRaw(["app", "rename", assertId(oldId), assertId(newId)]);
|
|
51958
|
+
if (code !== 0) {
|
|
51959
|
+
if (isInvokeUnsupported({ stdout, stderr })) {
|
|
51960
|
+
throw new AwareUnsupportedError("aware CLI does not support `app rename` (needs \u2265 0.68) \u2014 update AWARE", { stdout, stderr });
|
|
51961
|
+
}
|
|
51962
|
+
throw new AwareError(`aware app rename ${oldId} \u2192 ${newId} failed (exit ${code})`, { stdout, stderr, code });
|
|
51963
|
+
}
|
|
51964
|
+
return { compiled: /lock refreshed/i.test(stdout), output: stdout.trim() };
|
|
51965
|
+
},
|
|
51966
|
+
/**
|
|
51967
|
+
* Duplicate an installed app into an independent copy (`aware app duplicate
|
|
51968
|
+
* <src> <new>`, CLI ≥ 0.68). The original is untouched; a baked copy gets its
|
|
51969
|
+
* own synthesized agent. Same `compiled` + unsupported-degradation contract as
|
|
51970
|
+
* rename.
|
|
51971
|
+
*/
|
|
51972
|
+
async duplicate(srcId, newId) {
|
|
51973
|
+
const { code, stdout, stderr } = await runRaw(["app", "duplicate", assertId(srcId), assertId(newId)]);
|
|
51974
|
+
if (code !== 0) {
|
|
51975
|
+
if (isInvokeUnsupported({ stdout, stderr })) {
|
|
51976
|
+
throw new AwareUnsupportedError("aware CLI does not support `app duplicate` (needs \u2265 0.68) \u2014 update AWARE", { stdout, stderr });
|
|
51977
|
+
}
|
|
51978
|
+
throw new AwareError(`aware app duplicate ${srcId} \u2192 ${newId} failed (exit ${code})`, { stdout, stderr, code });
|
|
51979
|
+
}
|
|
51980
|
+
return { compiled: /lock refreshed/i.test(stdout), output: stdout.trim() };
|
|
51981
|
+
},
|
|
51936
51982
|
/**
|
|
51937
51983
|
* Install a first-party agent by registry id (`aware agent install <id>`). The
|
|
51938
51984
|
* registry install pulls a large tarball (~200s observed) — far over runRaw's 60s
|
|
@@ -52683,7 +52729,7 @@ function appVersion() {
|
|
|
52683
52729
|
return resolveVersion({
|
|
52684
52730
|
isSea: isSea2(),
|
|
52685
52731
|
sqVersionXml: readSqVersionXml(),
|
|
52686
|
-
define: true ? "0.
|
|
52732
|
+
define: true ? "0.21.0" : void 0,
|
|
52687
52733
|
pkgVersion: readPkgVersion()
|
|
52688
52734
|
});
|
|
52689
52735
|
}
|
|
@@ -52693,7 +52739,7 @@ function resolveChannel(s) {
|
|
|
52693
52739
|
return "dev";
|
|
52694
52740
|
}
|
|
52695
52741
|
function appChannel() {
|
|
52696
|
-
return resolveChannel({ isSea: isSea2(), define: true ? "0.
|
|
52742
|
+
return resolveChannel({ isSea: isSea2(), define: true ? "0.21.0" : void 0 });
|
|
52697
52743
|
}
|
|
52698
52744
|
|
|
52699
52745
|
// oauth-presets.ts
|
|
@@ -54104,49 +54150,15 @@ function appDirPath(id) {
|
|
|
54104
54150
|
function appInstalled(id) {
|
|
54105
54151
|
return APP_ID3.test(id) && (0, import_node_fs16.existsSync)(appDirPath(id));
|
|
54106
54152
|
}
|
|
54107
|
-
function findSource(dir) {
|
|
54108
|
-
const files = (0, import_node_fs16.readdirSync)(dir);
|
|
54109
|
-
const hit = files.find((f) => f.toLowerCase().endsWith(".app")) ?? files.find((f) => f.toLowerCase().endsWith(".flo"));
|
|
54110
|
-
return hit ? (0, import_node_path14.join)(dir, hit) : null;
|
|
54111
|
-
}
|
|
54112
|
-
function rewriteAppId(text, newId) {
|
|
54113
|
-
const re = /^(app:[ \t]*)(["']?)([^"'#\r\n]+?)\2([ \t]*(?:#[^\r\n]*)?)$/m;
|
|
54114
|
-
if (!re.test(text)) throw new AppLifecycleError("source has no top-level app: field", 422);
|
|
54115
|
-
return text.replace(re, (_m, pre, quote, _id, trail) => `${pre}${quote}${newId}${quote}${trail}`);
|
|
54116
|
-
}
|
|
54117
|
-
function isExposedAsAgent(dir) {
|
|
54118
|
-
const src = findSource(dir);
|
|
54119
|
-
return src ? /^exposes-as-agent:[ \t]*true\b/m.test((0, import_node_fs16.readFileSync)(src, "utf8")) : false;
|
|
54120
|
-
}
|
|
54121
54153
|
function logCascade(what, e) {
|
|
54122
54154
|
console.error(`[app-lifecycle] cascade step failed (${what}):`, e instanceof Error ? e.message : e);
|
|
54123
54155
|
}
|
|
54124
|
-
function
|
|
54125
|
-
|
|
54126
|
-
|
|
54127
|
-
|
|
54128
|
-
|
|
54129
|
-
|
|
54130
|
-
} catch {
|
|
54131
|
-
}
|
|
54132
|
-
}
|
|
54133
|
-
}
|
|
54134
|
-
}
|
|
54135
|
-
function restampSource(dir, newId) {
|
|
54136
|
-
const src = findSource(dir);
|
|
54137
|
-
if (!src) throw new AppLifecycleError("app has no source file", 422);
|
|
54138
|
-
const text = rewriteAppId((0, import_node_fs16.readFileSync)(src, "utf8"), newId);
|
|
54139
|
-
const ext = src.toLowerCase().endsWith(".flo") ? ".flo" : ".app";
|
|
54140
|
-
const newSrc = (0, import_node_path14.join)(dir, `${newId}${ext}`);
|
|
54141
|
-
(0, import_node_fs16.writeFileSync)(newSrc, text);
|
|
54142
|
-
if (src !== newSrc) {
|
|
54143
|
-
try {
|
|
54144
|
-
(0, import_node_fs16.unlinkSync)(src);
|
|
54145
|
-
} catch {
|
|
54146
|
-
}
|
|
54147
|
-
}
|
|
54148
|
-
removeLocks(dir);
|
|
54149
|
-
return newSrc;
|
|
54156
|
+
function mapAwareError(e) {
|
|
54157
|
+
const code = e instanceof AwareError ? e.detail?.code : void 0;
|
|
54158
|
+
if (code === 8) throw new AppLifecycleError("a workflow with that name already exists", 409);
|
|
54159
|
+
if (code === 7) throw new AppLifecycleError("workflow not found", 404);
|
|
54160
|
+
if (code === 3) throw new AppLifecycleError(e instanceof Error ? e.message : "invalid name", 400);
|
|
54161
|
+
throw e;
|
|
54150
54162
|
}
|
|
54151
54163
|
function assertNewId(newId) {
|
|
54152
54164
|
if (!newId || !APP_ID3.test(newId) || !safeSegment(newId)) {
|
|
@@ -54154,39 +54166,16 @@ function assertNewId(newId) {
|
|
|
54154
54166
|
}
|
|
54155
54167
|
}
|
|
54156
54168
|
var defaultDeps2 = {
|
|
54157
|
-
|
|
54169
|
+
rename: (oldId, newId) => aware.rename(oldId, newId),
|
|
54170
|
+
duplicate: (srcId, newId) => aware.duplicate(srcId, newId),
|
|
54158
54171
|
uninstall: (id) => aware.uninstall(id)
|
|
54159
54172
|
};
|
|
54160
|
-
async function tryCompile(deps, sourcePath) {
|
|
54161
|
-
try {
|
|
54162
|
-
await deps.compile(sourcePath);
|
|
54163
|
-
return true;
|
|
54164
|
-
} catch {
|
|
54165
|
-
return false;
|
|
54166
|
-
}
|
|
54167
|
-
}
|
|
54168
54173
|
async function renameApp(oldId, newId, deps = defaultDeps2) {
|
|
54169
54174
|
if (!appInstalled(oldId)) throw new AppLifecycleError(`workflow not found: ${oldId}`, 404);
|
|
54170
54175
|
assertNewId(newId);
|
|
54171
54176
|
if (newId === oldId) throw new AppLifecycleError("that is already its name", 400);
|
|
54172
54177
|
if (appInstalled(newId)) throw new AppLifecycleError(`a workflow named "${newId}" already exists`, 409);
|
|
54173
|
-
|
|
54174
|
-
throw new AppLifecycleError("this workflow is baked into an agent \u2014 rename isn\u2019t supported yet (the agent wouldn\u2019t follow). Un-bake it first, or rename it from the terminal.", 409);
|
|
54175
|
-
}
|
|
54176
|
-
const oldDir = appDirPath(oldId);
|
|
54177
|
-
const newDir = appDirPath(newId);
|
|
54178
|
-
(0, import_node_fs16.renameSync)(oldDir, newDir);
|
|
54179
|
-
let newSrc;
|
|
54180
|
-
try {
|
|
54181
|
-
newSrc = restampSource(newDir, newId);
|
|
54182
|
-
} catch (e) {
|
|
54183
|
-
try {
|
|
54184
|
-
(0, import_node_fs16.renameSync)(newDir, oldDir);
|
|
54185
|
-
} catch {
|
|
54186
|
-
}
|
|
54187
|
-
throw e;
|
|
54188
|
-
}
|
|
54189
|
-
const compiled = await tryCompile(deps, newSrc);
|
|
54178
|
+
const { compiled } = await deps.rename(oldId, newId).catch(mapAwareError);
|
|
54190
54179
|
try {
|
|
54191
54180
|
renameVisualInputs(oldId, newId);
|
|
54192
54181
|
} catch (e) {
|
|
@@ -54203,27 +54192,7 @@ async function duplicateApp(srcId, newId, deps = defaultDeps2) {
|
|
|
54203
54192
|
if (!appInstalled(srcId)) throw new AppLifecycleError(`workflow not found: ${srcId}`, 404);
|
|
54204
54193
|
assertNewId(newId);
|
|
54205
54194
|
if (appInstalled(newId)) throw new AppLifecycleError(`a workflow named "${newId}" already exists`, 409);
|
|
54206
|
-
|
|
54207
|
-
throw new AppLifecycleError("this workflow is baked into an agent \u2014 duplicate isn\u2019t supported yet (the copy\u2019s agent wouldn\u2019t be installed). Un-bake it first, or copy it from the terminal.", 409);
|
|
54208
|
-
}
|
|
54209
|
-
const newDir = appDirPath(newId);
|
|
54210
|
-
try {
|
|
54211
|
-
(0, import_node_fs16.mkdirSync)(newDir);
|
|
54212
|
-
} catch {
|
|
54213
|
-
throw new AppLifecycleError(`a workflow named "${newId}" already exists`, 409);
|
|
54214
|
-
}
|
|
54215
|
-
let newSrc;
|
|
54216
|
-
try {
|
|
54217
|
-
(0, import_node_fs16.cpSync)(appDirPath(srcId), newDir, { recursive: true });
|
|
54218
|
-
newSrc = restampSource(newDir, newId);
|
|
54219
|
-
} catch (e) {
|
|
54220
|
-
try {
|
|
54221
|
-
(0, import_node_fs16.rmSync)(newDir, { recursive: true, force: true });
|
|
54222
|
-
} catch {
|
|
54223
|
-
}
|
|
54224
|
-
throw e;
|
|
54225
|
-
}
|
|
54226
|
-
const compiled = await tryCompile(deps, newSrc);
|
|
54195
|
+
const { compiled } = await deps.duplicate(srcId, newId).catch(mapAwareError);
|
|
54227
54196
|
try {
|
|
54228
54197
|
copyVisualInputs(srcId, newId);
|
|
54229
54198
|
} catch (e) {
|
|
@@ -54233,7 +54202,7 @@ async function duplicateApp(srcId, newId, deps = defaultDeps2) {
|
|
|
54233
54202
|
}
|
|
54234
54203
|
async function deleteApp(id, deps = defaultDeps2) {
|
|
54235
54204
|
if (!appInstalled(id)) throw new AppLifecycleError(`workflow not found: ${id}`, 404);
|
|
54236
|
-
await deps.uninstall(id);
|
|
54205
|
+
await deps.uninstall(id).catch(mapAwareError);
|
|
54237
54206
|
try {
|
|
54238
54207
|
deleteVisualInputs(id);
|
|
54239
54208
|
} catch (e) {
|
|
@@ -57273,7 +57242,7 @@ async function startServer() {
|
|
|
57273
57242
|
}
|
|
57274
57243
|
});
|
|
57275
57244
|
await app.register(import_static.default, { root: WEB_ROOT, prefix: "/" });
|
|
57276
|
-
const MIN_AWARE = "0.
|
|
57245
|
+
const MIN_AWARE = "0.68.0";
|
|
57277
57246
|
const isWin3 = process.platform === "win32";
|
|
57278
57247
|
const has = (cmd) => {
|
|
57279
57248
|
try {
|
|
@@ -57499,7 +57468,7 @@ async function startServer() {
|
|
|
57499
57468
|
onSeatLost((reason) => broadcast({ type: "seat-taken", reason }));
|
|
57500
57469
|
app.get("/api/apps", async () => {
|
|
57501
57470
|
const apps = await aware.list();
|
|
57502
|
-
return { ok: true, apps: apps.map((a) => ({ ...a, provider: appProvider(a.id) })) };
|
|
57471
|
+
return { ok: true, apps: apps.map((a) => ({ ...a, provider: appProvider(a.id), baked: appBaked(a.id) })) };
|
|
57503
57472
|
});
|
|
57504
57473
|
app.get("/api/agents", async () => {
|
|
57505
57474
|
const agents = await aware.agentList();
|
|
@@ -57810,7 +57779,16 @@ async function startServer() {
|
|
|
57810
57779
|
broadcast({ type: "run-started", id, dryRun: !!dryRun, simulate: !!simulate, runId: result.runId });
|
|
57811
57780
|
for (const ev of events) broadcast({ type: "trace", id, event: ev });
|
|
57812
57781
|
broadcast({ type: "run-ended", id, runId: result.runId });
|
|
57813
|
-
const
|
|
57782
|
+
const extracted = extractReportHtml(events);
|
|
57783
|
+
let report = null;
|
|
57784
|
+
if (extracted) {
|
|
57785
|
+
let interactive = false;
|
|
57786
|
+
try {
|
|
57787
|
+
interactive = readApp(id).nodes.find((n) => n.id === extracted.nodeId)?.agent === "viewer-3d";
|
|
57788
|
+
} catch {
|
|
57789
|
+
}
|
|
57790
|
+
report = { ...interactive ? extracted : withBadge(extracted), interactive };
|
|
57791
|
+
}
|
|
57814
57792
|
const credentialIssue = !simulate && hasAuthFailure(events) ? await detectCredentialIssue(id) : null;
|
|
57815
57793
|
return { ok: true, runId: result.runId, tracePath: result.tracePath, events, report, credentialIssue };
|
|
57816
57794
|
}
|
package/dist/web/app.css
CHANGED
|
@@ -2209,7 +2209,9 @@ body {
|
|
|
2209
2209
|
.report-actions button.report-stop:hover { background: color-mix(in srgb, var(--warn) 16%, transparent); border-color: var(--warn); color: var(--text); }
|
|
2210
2210
|
#report-close { font-size: 18px; line-height: 1; padding: 4px 10px; }
|
|
2211
2211
|
.report-stage { position: relative; flex: 1; background: #0f172a; }
|
|
2212
|
-
|
|
2212
|
+
/* Only the SHOWN frame fills the stage — `:not([hidden])` so `display:block` never defeats the
|
|
2213
|
+
`hidden` attribute on the inactive frame (else it keeps its height and pushes the other off-screen). */
|
|
2214
|
+
#report-frame:not([hidden]), #report-frame-3d:not([hidden]) { width: 100%; height: 100%; border: 0; display: block; background: #0f172a; }
|
|
2213
2215
|
|
|
2214
2216
|
/* Live "On trigger" report: a pulsing pip in the viewer subtitle + a brief edge
|
|
2215
2217
|
flash on the stage when a trigger routine (e.g. tekla-selection-report) pushes a
|
package/dist/web/aware.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
const $notesStrip = document.getElementById('notes-strip');
|
|
19
19
|
const $reportModal = document.getElementById('report-modal');
|
|
20
20
|
const $reportFrame = document.getElementById('report-frame');
|
|
21
|
+
const $reportFrame3d = document.getElementById('report-frame-3d');
|
|
21
22
|
const $reportOverlay = document.getElementById('report-overlay');
|
|
22
23
|
const $reportTitle = document.getElementById('report-title');
|
|
23
24
|
const $reportSub = document.getElementById('report-sub');
|
|
@@ -582,7 +583,7 @@
|
|
|
582
583
|
// <button>s without nesting interactive elements (#85). Selection stays driven by
|
|
583
584
|
// the delegated $wfList click + comboMove highlight (aria-activedescendant).
|
|
584
585
|
const aid = escapeAttr(a.id);
|
|
585
|
-
html += `<div class="wf-option" role="option" id="wf-opt-${aid}" data-id="${aid}" data-search="${escapeAttr((a.id + ' ' + p).toLowerCase())}" tabindex="-1" aria-selected="false">`
|
|
586
|
+
html += `<div class="wf-option" role="option" id="wf-opt-${aid}" data-id="${aid}" data-baked="${a.baked ? '1' : ''}" data-search="${escapeAttr((a.id + ' ' + p).toLowerCase())}" tabindex="-1" aria-selected="false">`
|
|
586
587
|
+ `<span class="wf-option-name">${escapeHtml(a.id)}</span><span class="wf-option-meta">${meta}</span>`
|
|
587
588
|
+ `<span class="wf-option-actions">`
|
|
588
589
|
+ `<button type="button" class="wf-act act-edit" data-wf-act="rename" data-id="${aid}" tabindex="-1" data-tip="Rename" aria-label="Rename ${aid}">✎</button>`
|
|
@@ -708,7 +709,7 @@
|
|
|
708
709
|
const act = e.target.closest('[data-wf-act]');
|
|
709
710
|
if (act) {
|
|
710
711
|
const id = act.dataset.id;
|
|
711
|
-
if (act.dataset.wfAct === 'rename') openRenameWorkflow(id);
|
|
712
|
+
if (act.dataset.wfAct === 'rename') openRenameWorkflow(id, act.closest('.wf-option')?.dataset.baked === '1');
|
|
712
713
|
else if (act.dataset.wfAct === 'duplicate') openDuplicateWorkflow(id);
|
|
713
714
|
else if (act.dataset.wfAct === 'delete') confirmDeleteWorkflow(id);
|
|
714
715
|
return;
|
|
@@ -737,11 +738,17 @@
|
|
|
737
738
|
appInputValues.delete(id); dirtyBaseline.delete(id); apps.delete(id);
|
|
738
739
|
}
|
|
739
740
|
|
|
740
|
-
async function openRenameWorkflow(oldId) {
|
|
741
|
+
async function openRenameWorkflow(oldId, baked = false) {
|
|
741
742
|
closeCombo(false);
|
|
743
|
+
// A baked workflow is a reusable agent; renaming it also renames that agent, so any
|
|
744
|
+
// OTHER workflow that uses it as a node will reference the old name and need updating.
|
|
745
|
+
// Warn (informed consent) — not a blocker; the user may be renaming an unreferenced
|
|
746
|
+
// one (#226 UX review).
|
|
747
|
+
const sub = 'This is the name you pick here, and what .flo files and your terminal AI use to refer to it.'
|
|
748
|
+
+ (baked ? ' Renaming a baked workflow also renames its synthesized agent — other workflows that use it as a node will need updating.' : '');
|
|
742
749
|
const res = await formModal({
|
|
743
750
|
title: 'Rename workflow',
|
|
744
|
-
sub
|
|
751
|
+
sub,
|
|
745
752
|
fields: [{ name: 'name', label: 'New name', type: 'text', value: oldId, placeholder: 'e.g. tekla-bom-by-phase' }],
|
|
746
753
|
okLabel: 'Rename',
|
|
747
754
|
});
|
|
@@ -838,10 +845,10 @@
|
|
|
838
845
|
function reportNodeId() {
|
|
839
846
|
const app = currentId && apps.get(currentId);
|
|
840
847
|
if (!app || !app.nodes.length) return null;
|
|
841
|
-
// An html-report agent node renders
|
|
842
|
-
// an `html` field,
|
|
843
|
-
// report producer regardless of exec code; prefer it directly as the viewer node.
|
|
844
|
-
const htmlReportNode = app.nodes.find((n) => n.agent === 'html-report');
|
|
848
|
+
// An html-report (static) or viewer-3d (interactive 3D) agent node renders HTML — its
|
|
849
|
+
// `render` command returns an `html` field, exactly what extractReportHtml keys on. So
|
|
850
|
+
// it's a report producer regardless of exec code; prefer it directly as the viewer node.
|
|
851
|
+
const htmlReportNode = app.nodes.find((n) => n.agent === 'html-report' || n.agent === 'viewer-3d');
|
|
845
852
|
if (htmlReportNode) return htmlReportNode.id;
|
|
846
853
|
// Otherwise, qualify the app only if some node actually PRODUCES report HTML — its exec
|
|
847
854
|
// code returns an `html` field (the `data.result.html` extractReportHtml keys
|
|
@@ -953,7 +960,11 @@
|
|
|
953
960
|
// clear anything we injected last render, so re-renders never stack
|
|
954
961
|
card.querySelectorAll('.node-action, .node-inputs').forEach((b) => b.remove());
|
|
955
962
|
if (isInput) addNodeInputs(card); // current values, above the button
|
|
956
|
-
if (isReport)
|
|
963
|
+
if (isReport) {
|
|
964
|
+
const rNode = app && app.nodes.find((n) => n.id === rid);
|
|
965
|
+
const viewLabel = rNode && rNode.agent === 'viewer-3d' ? 'View 3D ▸' : 'View report ▸';
|
|
966
|
+
addNodeAction(card, viewLabel, () => showReport(id));
|
|
967
|
+
}
|
|
957
968
|
if (isInput) addNodeAction(card, 'Set inputs ▸', () => openInputsDialog());
|
|
958
969
|
if (isRebake) addNodeAction(card, 'Re-read & re-bake ▸', () => openRebakeDialog());
|
|
959
970
|
});
|
|
@@ -1245,7 +1256,37 @@
|
|
|
1245
1256
|
|
|
1246
1257
|
// Share is visible exactly when the frame holds a report (the empty state HIDES it —
|
|
1247
1258
|
// never a disabled affordance with no path forward).
|
|
1248
|
-
|
|
1259
|
+
// The last HTML painted (static or 3D) — so ↗ Open works for both surfaces.
|
|
1260
|
+
let lastPaintedHtml = '';
|
|
1261
|
+
// Paint a report into the viewer. A STATIC report uses the scriptless srcdoc frame; an
|
|
1262
|
+
// INTERACTIVE 3D doc (viewer-3d) uses the script-enabled frame via a data: URL (opaque
|
|
1263
|
+
// origin, cannot touch the app page) and keeps a "Loading 3D view…" overlay until the doc
|
|
1264
|
+
// posts `viewer-ready`. Exactly one frame is shown at a time.
|
|
1265
|
+
function paintReport(html, interactive) {
|
|
1266
|
+
lastPaintedHtml = html || '';
|
|
1267
|
+
if (interactive) {
|
|
1268
|
+
$reportFrame.hidden = true; $reportFrame.srcdoc = '';
|
|
1269
|
+
$reportFrame3d.hidden = false;
|
|
1270
|
+
$reportFrame3d.src = 'data:text/html;charset=utf-8,' + encodeURIComponent(html);
|
|
1271
|
+
$reportShare.hidden = true; // a script doc can't be statically hosted — hide, don't disable
|
|
1272
|
+
$reportOverlay.hidden = false; $reportOverlay.replaceChildren();
|
|
1273
|
+
$reportOverlay.append(mkEl('div', 'spinner'), mkEl('div', null, 'Loading 3D view…'));
|
|
1274
|
+
} else {
|
|
1275
|
+
$reportFrame3d.hidden = true; $reportFrame3d.removeAttribute('src');
|
|
1276
|
+
$reportFrame.hidden = false; $reportFrame.srcdoc = html;
|
|
1277
|
+
$reportOverlay.hidden = true; $reportOverlay.replaceChildren();
|
|
1278
|
+
$reportShare.hidden = false;
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
// Reset the viewer to "no report shown": clear BOTH frames + the Open buffer, so a stale
|
|
1283
|
+
// interactive 3D doc can't linger (its WebGL loop running) behind an overlay, and ↗ Open
|
|
1284
|
+
// can't reopen a previous app's report. Used by every empty / transition state.
|
|
1285
|
+
function clearReportFrames() {
|
|
1286
|
+
$reportFrame.srcdoc = ''; $reportFrame.hidden = false;
|
|
1287
|
+
$reportFrame3d.hidden = true; $reportFrame3d.removeAttribute('src');
|
|
1288
|
+
lastPaintedHtml = '';
|
|
1289
|
+
}
|
|
1249
1290
|
|
|
1250
1291
|
// Double-click the HTML Viewer node → LOAD the last report (no run; running is
|
|
1251
1292
|
// the header "▶ Run workflow" button's job). Shows a prompt if nothing has run yet.
|
|
@@ -1258,10 +1299,16 @@
|
|
|
1258
1299
|
if (reportRunning) return; // a run is in flight — its overlay already shows progress
|
|
1259
1300
|
const cached = lastReportByApp.get(currentId);
|
|
1260
1301
|
if (cached && cached.html) {
|
|
1261
|
-
|
|
1262
|
-
|
|
1302
|
+
const sfx = cached.label ? ' · ' + cached.label : '';
|
|
1303
|
+
if (cached.interactive) {
|
|
1304
|
+
$reportTitle.textContent = `3D Viewer · ${app.displayName}`;
|
|
1305
|
+
$reportSub.textContent = `Last 3D view${sfx} — drag to orbit, scroll to zoom, click an element to inspect.`;
|
|
1306
|
+
} else {
|
|
1307
|
+
$reportSub.textContent = `Last report${sfx} — rendered from the run's output, never composed by the UI.`;
|
|
1308
|
+
}
|
|
1309
|
+
paintReport(cached.html, cached.interactive);
|
|
1263
1310
|
} else {
|
|
1264
|
-
|
|
1311
|
+
clearReportFrames();
|
|
1265
1312
|
$reportShare.hidden = true; // nothing to share — hide, don't disable
|
|
1266
1313
|
$reportOverlay.hidden = false;
|
|
1267
1314
|
$reportOverlay.innerHTML = `<div>No report yet. Click <strong>▶ Run workflow</strong> (top right) to run it against the live model, then double-click to view it any time.</div>`;
|
|
@@ -1286,7 +1333,7 @@
|
|
|
1286
1333
|
const inputBadge = Object.entries(inputs).map(([k, v]) => `${k}=${v}`).join(' · ');
|
|
1287
1334
|
$reportTitle.textContent = `HTML Viewer · ${app.displayName}`;
|
|
1288
1335
|
$reportSub.innerHTML = `Live run of <code>${escapeHtml(nodeId)}</code>${inputBadge ? ' · ' + escapeHtml(inputBadge) : ''} — rendered from the node's output, never composed by the UI.`;
|
|
1289
|
-
|
|
1336
|
+
clearReportFrames(); // clear any previous report (static or 3D) before this run paints a fresh one
|
|
1290
1337
|
$reportShare.hidden = true; // the frame is blank — only a painted report is shareable
|
|
1291
1338
|
$reportOverlay.hidden = false;
|
|
1292
1339
|
$reportOverlay.innerHTML = opts.debug
|
|
@@ -1307,14 +1354,19 @@
|
|
|
1307
1354
|
// body, not real data) — prompt reconnect instead of rendering the noise.
|
|
1308
1355
|
if (res.credentialIssue) { surfaceCredentialIssue(res.credentialIssue); return; }
|
|
1309
1356
|
const html = res.report && res.report.html;
|
|
1357
|
+
const interactive = !!(res.report && res.report.interactive);
|
|
1310
1358
|
if (!html) {
|
|
1311
1359
|
$reportOverlay.innerHTML = `<div>Run completed but <code>${escapeHtml(nodeId)}</code> returned no <code>html</code>. Check the Execution tab.</div>`;
|
|
1312
1360
|
return;
|
|
1313
1361
|
}
|
|
1314
|
-
|
|
1315
|
-
|
|
1362
|
+
if (interactive) {
|
|
1363
|
+
$reportTitle.textContent = `3D Viewer · ${app.displayName}`;
|
|
1364
|
+
$reportSub.innerHTML = `Live 3D view from <code>${escapeHtml(nodeId)}</code>${inputBadge ? ' · ' + escapeHtml(inputBadge) : ''} — drag to orbit, scroll to zoom, click an element to inspect.`;
|
|
1365
|
+
}
|
|
1366
|
+
paintReport(html, interactive);
|
|
1367
|
+
lastReportByApp.set(currentId, { nodeId, label: inputBadge, html, interactive });
|
|
1316
1368
|
$reportModal.dataset.html = '1';
|
|
1317
|
-
appendNarration(`Rendered the report from <strong>${escapeHtml(nodeId)}</strong>${inputBadge ? ' (' + escapeHtml(inputBadge) + ')' : ''} — live run against the model.`);
|
|
1369
|
+
appendNarration(`Rendered the ${interactive ? '3D view' : 'report'} from <strong>${escapeHtml(nodeId)}</strong>${inputBadge ? ' (' + escapeHtml(inputBadge) + ')' : ''} — live run against the model.`);
|
|
1318
1370
|
} catch (e) {
|
|
1319
1371
|
const msg = e && e.message ? e.message : String(e);
|
|
1320
1372
|
if (cancelRequested) {
|
|
@@ -1362,7 +1414,7 @@
|
|
|
1362
1414
|
const CRED_BODY_IDLE = 'Sign in again and then run the workflow — it takes about 30 seconds.';
|
|
1363
1415
|
|
|
1364
1416
|
function showCredentialReconnect(cred) {
|
|
1365
|
-
|
|
1417
|
+
clearReportFrames();
|
|
1366
1418
|
$reportShare.hidden = true;
|
|
1367
1419
|
$reportOverlay.hidden = false;
|
|
1368
1420
|
$reportOverlay.replaceChildren();
|
|
@@ -1674,10 +1726,22 @@
|
|
|
1674
1726
|
// The Stop button is rebuilt into the overlay each run — delegate so one
|
|
1675
1727
|
// listener survives every innerHTML swap.
|
|
1676
1728
|
$reportOverlay.addEventListener('click', (e) => { if (e.target.closest('.overlay-stop')) stopRun(); });
|
|
1729
|
+
// The interactive 3D doc (viewer-3d) posts a load handshake from its opaque-origin frame:
|
|
1730
|
+
// `viewer-ready` clears the loading overlay; `viewer-error` (CDN/script/timeout) shows a
|
|
1731
|
+
// retry. Guard: only when the 3D frame is the active surface and the message is from it.
|
|
1732
|
+
window.addEventListener('message', (e) => {
|
|
1733
|
+
if ($reportFrame3d.hidden || e.source !== $reportFrame3d.contentWindow) return;
|
|
1734
|
+
const t = e.data && e.data.type;
|
|
1735
|
+
if (t === 'viewer-ready') { $reportOverlay.hidden = true; $reportOverlay.replaceChildren(); }
|
|
1736
|
+
else if (t === 'viewer-error') {
|
|
1737
|
+
$reportOverlay.hidden = false; $reportOverlay.replaceChildren();
|
|
1738
|
+
$reportOverlay.append(mkEl('div', null, '3D view failed to load — check your connection, then click ▶ Run workflow to try again.'));
|
|
1739
|
+
}
|
|
1740
|
+
});
|
|
1677
1741
|
// The header ■ Stop run — the always-reachable twin of the overlay Stop (#39).
|
|
1678
1742
|
if ($stopRunBtn) $stopRunBtn.onclick = () => stopRun();
|
|
1679
1743
|
$reportOpen.onclick = () => {
|
|
1680
|
-
const html = $reportFrame.srcdoc;
|
|
1744
|
+
const html = lastPaintedHtml || $reportFrame.srcdoc;
|
|
1681
1745
|
if (!html) return;
|
|
1682
1746
|
// Open via a blob: URL — an opaque origin, so the report can't script the
|
|
1683
1747
|
// app origin (mirrors the iframe's sandbox intent).
|
|
@@ -4033,7 +4097,7 @@
|
|
|
4033
4097
|
$reportModal.dataset.nodeId = reportNodeId() || '';
|
|
4034
4098
|
const cached = lastReportByApp.get(id);
|
|
4035
4099
|
if (cached && cached.html) paintReport(cached.html);
|
|
4036
|
-
else {
|
|
4100
|
+
else { clearReportFrames(); $reportShare.hidden = true; }
|
|
4037
4101
|
$reportOverlay.hidden = false;
|
|
4038
4102
|
$reportOverlay.replaceChildren();
|
|
4039
4103
|
$reportOverlay.append(mkEl('div', 'spinner'), mkEl('div', null, 'Starting live session…'));
|
|
@@ -4118,7 +4182,7 @@
|
|
|
4118
4182
|
$reportShare.hidden = false; // the cached report is the primary content
|
|
4119
4183
|
$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.`;
|
|
4120
4184
|
} else {
|
|
4121
|
-
|
|
4185
|
+
clearReportFrames();
|
|
4122
4186
|
$reportShare.hidden = true;
|
|
4123
4187
|
$reportOverlay.hidden = false;
|
|
4124
4188
|
$reportOverlay.replaceChildren();
|
|
@@ -4146,7 +4210,7 @@
|
|
|
4146
4210
|
? `Run complete · last report above — click <strong>▶ Run workflow</strong> to run again.`
|
|
4147
4211
|
: `Live session stopped · last report above — click <strong>▶ Run workflow</strong> to start again.`;
|
|
4148
4212
|
} else {
|
|
4149
|
-
|
|
4213
|
+
clearReportFrames();
|
|
4150
4214
|
$reportShare.hidden = true;
|
|
4151
4215
|
$reportOverlay.hidden = false;
|
|
4152
4216
|
$reportOverlay.replaceChildren();
|
package/dist/web/index.html
CHANGED
|
@@ -319,6 +319,13 @@
|
|
|
319
319
|
Scripts stay BLOCKED (no allow-scripts), so even though the report is now
|
|
320
320
|
same-origin it has no JS to touch the app page — static reports only. -->
|
|
321
321
|
<iframe id="report-frame" sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox" title="Report"></iframe>
|
|
322
|
+
<!-- Interactive output (the viewer-3d agent) needs scripts, so it cannot use the
|
|
323
|
+
scriptless static frame above. Loaded via a `data:text/html` URL → an opaque
|
|
324
|
+
origin (cannot reach the app page, cookies, or same-origin APIs). Minimal
|
|
325
|
+
sandbox: `allow-scripts` ONLY — the 3D doc has no links/popups of its own
|
|
326
|
+
(↗ Open is a parent-side action), and NO allow-same-origin. Shown only for
|
|
327
|
+
interactive reports; the static frame is used otherwise. -->
|
|
328
|
+
<iframe id="report-frame-3d" sandbox="allow-scripts" title="3D view" hidden></iframe>
|
|
322
329
|
<div class="report-overlay" id="report-overlay" hidden></div>
|
|
323
330
|
</div>
|
|
324
331
|
</div>
|