@floless/app 0.12.3 → 0.13.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.
@@ -52609,7 +52609,7 @@ function appVersion() {
52609
52609
  return resolveVersion({
52610
52610
  isSea: isSea2(),
52611
52611
  sqVersionXml: readSqVersionXml(),
52612
- define: true ? "0.12.3" : void 0,
52612
+ define: true ? "0.13.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.12.3" : void 0 });
52622
+ return resolveChannel({ isSea: isSea2(), define: true ? "0.13.0" : void 0 });
52623
52623
  }
52624
52624
 
52625
52625
  // oauth-presets.ts
@@ -52755,6 +52755,19 @@ function withBadge(report, opts) {
52755
52755
  return { ...report, html: injectBadge(report.html, opts) };
52756
52756
  }
52757
52757
 
52758
+ // report-html.ts
52759
+ function extractReportHtml(events) {
52760
+ let found = null;
52761
+ for (const ev of events) {
52762
+ const data = ev?.data;
52763
+ if (!data || typeof data !== "object") continue;
52764
+ const result = data.result;
52765
+ const html = typeof data.html === "string" ? data.html : result && typeof result.html === "string" ? result.html : null;
52766
+ if (html) found = { nodeId: ev.node ?? null, html };
52767
+ }
52768
+ return found;
52769
+ }
52770
+
52758
52771
  // index.ts
52759
52772
  var import_node_crypto6 = require("node:crypto");
52760
52773
 
@@ -53180,7 +53193,8 @@ function deriveTriggerProgress(events, sourceNodeId2) {
53180
53193
  }
53181
53194
  var defaultDeps = {
53182
53195
  start: (id, opts) => aware.startTrigger(id, opts),
53183
- onChange: (id, snapshot) => broadcast({ type: "trigger-session-changed", id, snapshot })
53196
+ onChange: (id, snapshot) => broadcast({ type: "trigger-session-changed", id, snapshot }),
53197
+ onReport: (id, workflow, report) => broadcast({ type: "trigger-report", id, workflow, nodeId: report.nodeId, html: report.html })
53184
53198
  };
53185
53199
  var sessions = /* @__PURE__ */ new Map();
53186
53200
  function isLive(s) {
@@ -53213,6 +53227,7 @@ function startSession(spec, deps = defaultDeps) {
53213
53227
  const emit = () => {
53214
53228
  if (current()) deps.onChange(spec.id, session.snapshot);
53215
53229
  };
53230
+ let lastReportCount = 0;
53216
53231
  const handle = deps.start(spec.workflow, { inputs: spec.inputs });
53217
53232
  session.handle = handle;
53218
53233
  handle.onTrace((events) => {
@@ -53221,6 +53236,17 @@ function startSession(spec, deps = defaultDeps) {
53221
53236
  session.snapshot.firedCount = p.firedCount;
53222
53237
  session.snapshot.lastEvent = p.lastEvent;
53223
53238
  emit();
53239
+ let reportCount = 0;
53240
+ for (const ev of events) {
53241
+ const d = ev?.data;
53242
+ const r = d && d.result;
53243
+ if (ev.kind === "node-output" && d && (typeof d.html === "string" || r && typeof r.html === "string")) reportCount++;
53244
+ }
53245
+ if (reportCount > lastReportCount) {
53246
+ lastReportCount = reportCount;
53247
+ const raw = extractReportHtml(events);
53248
+ if (raw && raw.html) deps.onReport?.(spec.id, spec.workflow, withBadge(raw));
53249
+ }
53224
53250
  });
53225
53251
  handle.onError((msg) => {
53226
53252
  if (!current()) return;
@@ -56812,17 +56838,6 @@ var WEB_ROOT = [(0, import_node_path20.join)(__dirname4, "web"), (0, import_node
56812
56838
  ) ?? (0, import_node_path20.join)(__dirname4, "..", "web");
56813
56839
  var PORT2 = Number(process.env.PORT ?? 4317);
56814
56840
  var HOST = "127.0.0.1";
56815
- function extractReportHtml(events) {
56816
- let found = null;
56817
- for (const ev of events) {
56818
- const data = ev?.data;
56819
- if (!data || typeof data !== "object") continue;
56820
- const result = data.result;
56821
- const html = typeof data.html === "string" ? data.html : result && typeof result.html === "string" ? result.html : null;
56822
- if (html) found = { nodeId: ev.node ?? null, html };
56823
- }
56824
- return found;
56825
- }
56826
56841
  var crashHandlersInstalled = false;
56827
56842
  function installCrashHandlers() {
56828
56843
  if (crashHandlersInstalled) return;
@@ -268,8 +268,15 @@ manifest edit + recompile, no engine. See the design doc for the Stage-1 plan.
268
268
  per-app cache (`lastReportByApp`), instantly, **no run**. If nothing has run yet, it shows a
269
269
  "click ▶ Run workflow" prompt (never a spinner). Do NOT make this trigger a run — a live run is
270
270
  ~15s and showing a spinner over stale content reads as "stuck".
271
- - Rendered in a **sandboxed iframe `srcdoc`** (`sandbox="allow-same-origin"`, scripts
272
- disabled). The UI never builds the HTML; it relays exactly what the exec returned.
271
+ - Rendered in a **sandboxed iframe `srcdoc`** (`sandbox="allow-same-origin allow-popups
272
+ allow-popups-to-escape-sandbox"`, **no `allow-scripts`** static reports only). The UI never
273
+ builds the HTML; it relays exactly what the exec returned. **`allow-same-origin` is load-bearing —
274
+ never drop it:** without it the srcdoc gets an opaque origin, and Chrome (149+) does NOT repaint an
275
+ opaque-origin sandboxed srcdoc on the 2nd+ assignment — and `runReport` always clears `srcdoc=''`
276
+ first, so the report HTML is ALWAYS a 2nd assignment → **blank viewer**. Chromium that repaints
277
+ reassigned srcdoc (e.g. Playwright's bundled build, and the very first render) renders fine, which
278
+ masks the bug — verify report-viewer changes in real Chrome, not just Playwright. Regressed once
279
+ (09813c8 added badge popups but replaced the whole sandbox value, dropping the flag); restored.
273
280
  - **`Simulate` can't run an exec-data-carrying app**: stubs have no `result.<field>`, so the
274
281
  cross-node templates (`{{ bom.result.html }}`) render undefined and the run aborts
275
282
  (`template render: undefined value`, exit 3). `/api/run` returns that **in-band** (HTTP 200
package/dist/web/app.css CHANGED
@@ -2059,6 +2059,27 @@ body {
2059
2059
  #report-close { font-size: 18px; line-height: 1; padding: 4px 10px; }
2060
2060
  .report-stage { position: relative; flex: 1; background: #0f172a; }
2061
2061
  #report-frame { width: 100%; height: 100%; border: 0; display: block; background: #0f172a; }
2062
+
2063
+ /* Live "On trigger" report: a pulsing pip in the viewer subtitle + a brief edge
2064
+ flash on the stage when a trigger routine (e.g. tekla-selection-report) pushes a
2065
+ fresh report into the already-open viewer. Brand blue — within the baseline. */
2066
+ .live-pip {
2067
+ display: inline-block; width: 7px; height: 7px; margin-right: 7px; border-radius: 999px;
2068
+ background: #3b82f6; vertical-align: middle; animation: live-pip-pulse 1.8s ease-out infinite;
2069
+ }
2070
+ @keyframes live-pip-pulse {
2071
+ 0% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.5); }
2072
+ 70% { box-shadow: 0 0 0 6px rgba(59, 130, 246, 0); }
2073
+ 100% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0); }
2074
+ }
2075
+ .report-live-pulse { animation: report-live-flash 0.6s ease-out; }
2076
+ @keyframes report-live-flash {
2077
+ 0% { box-shadow: inset 0 0 0 2px rgba(59, 130, 246, 0.85); }
2078
+ 100% { box-shadow: inset 0 0 0 2px rgba(59, 130, 246, 0); }
2079
+ }
2080
+ @media (prefers-reduced-motion: reduce) {
2081
+ .live-pip, .report-live-pulse { animation: none; }
2082
+ }
2062
2083
  .report-overlay {
2063
2084
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
2064
2085
  flex-direction: column; gap: 12px; background: rgba(7, 10, 15, 0.82);
package/dist/web/aware.js CHANGED
@@ -2756,6 +2756,8 @@
2756
2756
  if (window.flolessPanels) window.flolessPanels.refreshData(); // routine runs also refresh bindings
2757
2757
  } else if (m.type === 'trigger-session-changed') {
2758
2758
  applyTriggerSnapshot(m.id, m.snapshot);
2759
+ } else if (m.type === 'trigger-report') {
2760
+ applyTriggerReport(m);
2759
2761
  } else if (m.type === 'connect-result') {
2760
2762
  // A run's credential-reconnect card may be waiting on this integration —
2761
2763
  // drive its success/fail state from the same SSE result.
@@ -3378,9 +3380,22 @@
3378
3380
  if (!r) { loadRoutinesData(); return; } // unknown id (created in another tab) → resync
3379
3381
  const prevFired = (r.session && r.session.firedCount) || 0;
3380
3382
  r.session = snapshot;
3383
+ const fired = !!(snapshot && snapshot.firedCount > prevFired);
3384
+ // A new fire lands a beat BEFORE its report (the source node-output streams
3385
+ // first, then the exec runs ~1–3s and emits the report HTML). If the HTML
3386
+ // Viewer is open for this app, show a "collecting…" spinner over the last
3387
+ // report until applyTriggerReport paints the fresh one.
3388
+ const viewerOpenForApp = currentId === r.workflow && $reportModal.classList.contains('show') && !reportRunning;
3389
+ if (fired && viewerOpenForApp) {
3390
+ showTriggerCollecting();
3391
+ } else if (viewerOpenForApp && (snapshot.state === 'error' || snapshot.state === 'stopped') && $reportOverlay.querySelector('.spinner')) {
3392
+ // The session ended before this fire produced a report — don't spin forever.
3393
+ // Fall back to the last report if we have one, else an idle prompt.
3394
+ clearTriggerCollecting();
3395
+ }
3381
3396
  if (!$routinesModal.classList.contains('show')) return;
3382
3397
  renderRoutinesList();
3383
- if (snapshot && snapshot.firedCount > prevFired) flashRoutineRow(id);
3398
+ if (fired) flashRoutineRow(id);
3384
3399
  }
3385
3400
 
3386
3401
  // Briefly tint a row when it just fired. Re-applies on rapid repeats via a forced
@@ -3394,6 +3409,59 @@
3394
3409
  el.classList.add('rtn-fired-flash');
3395
3410
  }
3396
3411
 
3412
+ // A trigger routine (e.g. tekla-selection-report on each SelectionChange) just
3413
+ // produced a fresh report. Always cache it (so opening the viewer shows the
3414
+ // latest), and if the HTML Viewer is ALREADY open for THIS app, live-repaint it
3415
+ // — the user asked to watch the report update on each selection change. We never
3416
+ // auto-open the viewer, never hijack another app's open viewer, and never stomp
3417
+ // an in-flight manual run. (The live repaint is another srcdoc assignment, so it
3418
+ // depends on the report-frame `allow-same-origin` fix to repaint in Chrome.)
3419
+ function applyTriggerReport(m) {
3420
+ if (!m || !m.workflow || typeof m.html !== 'string') return;
3421
+ lastReportByApp.set(m.workflow, { nodeId: m.nodeId || null, label: 'live · on selection change', html: m.html });
3422
+ const open = $reportModal && $reportModal.classList.contains('show');
3423
+ if (!open || currentId !== m.workflow || reportRunning) return;
3424
+ paintReport(m.html); // unhides Share + clears the overlay
3425
+ const t = new Date().toLocaleTimeString();
3426
+ $reportSub.innerHTML =
3427
+ `<span class="live-pip" aria-hidden="true"></span>Live &middot; updated ${escapeHtml(t)} &middot; on selection change — rendered from the node's output, never composed by the UI.`;
3428
+ pulseReportFrame();
3429
+ }
3430
+
3431
+ // Brief edge-flash on the report stage when a live update lands, so the change
3432
+ // is noticeable. Forced reflow re-arms it on rapid repeats. CSS honors
3433
+ // prefers-reduced-motion.
3434
+ function pulseReportFrame() {
3435
+ const stage = document.getElementById('report-stage');
3436
+ if (!stage) return;
3437
+ stage.classList.remove('report-live-pulse');
3438
+ void stage.offsetWidth;
3439
+ stage.classList.add('report-live-pulse');
3440
+ }
3441
+
3442
+ // A trigger fired (new selection) but its report is still computing on the host.
3443
+ // Show a spinner over the viewer — the previous report dims behind the overlay —
3444
+ // until applyTriggerReport paints the fresh one. Built with DOM nodes (no
3445
+ // innerHTML); reuses the same .spinner the manual run overlay uses.
3446
+ function showTriggerCollecting() {
3447
+ $reportOverlay.hidden = false;
3448
+ $reportOverlay.replaceChildren();
3449
+ const sp = mkEl('div', 'spinner');
3450
+ const msg = mkEl('div', null, 'New selection — collecting report…');
3451
+ $reportOverlay.append(sp, msg);
3452
+ }
3453
+
3454
+ // The trigger session errored/stopped while a "collecting…" spinner was up (a
3455
+ // fire whose report never arrived) — clear it instead of spinning forever:
3456
+ // revert to the last cached report if there is one, else an idle prompt.
3457
+ function clearTriggerCollecting() {
3458
+ const cached = currentId && lastReportByApp.get(currentId);
3459
+ if (cached && cached.html) { paintReport(cached.html); return; }
3460
+ $reportOverlay.replaceChildren();
3461
+ $reportOverlay.hidden = false;
3462
+ $reportOverlay.append(mkEl('div', null, 'Listening for the next selection…'));
3463
+ }
3464
+
3397
3465
  function renderRoutinesList() {
3398
3466
  const $list = document.getElementById('routines-list');
3399
3467
  const $quota = document.getElementById('rtn-quota');
@@ -295,11 +295,19 @@
295
295
  </div>
296
296
  </div>
297
297
  <div class="report-stage" id="report-stage">
298
- <!-- allow-popups (+ escape): the report's own links — the "Made with FloLess"
299
- badge above all must open in a real, unsandboxed tab. Scripts stay
300
- blocked and there is NO allow-same-origin: the report remains an opaque
301
- origin with zero access to the app page. -->
302
- <iframe id="report-frame" sandbox="allow-popups allow-popups-to-escape-sandbox" title="Report"></iframe>
298
+ <!-- sandbox flags, each load-bearing:
299
+ allow-same-originREQUIRED. Without it the srcdoc document gets an
300
+ opaque origin, and Chrome (149+) fails to repaint an opaque-origin
301
+ sandboxed srcdoc on the 2nd+ assignment: every report after the first
302
+ render (and runReport always clears srcdoc='' first, so it's ALWAYS a
303
+ 2nd assignment) shows blank. Dropping this flag was a regression in the
304
+ badge-popups change (09813c8) that replaced the whole sandbox value.
305
+ Verified blank in Chrome 149; fine in Chromium that repaints reassigned srcdoc.
306
+ • allow-popups (+ escape) — the report's own links (the "Made with FloLess"
307
+ badge) must open in a real, top-level tab.
308
+ Scripts stay BLOCKED (no allow-scripts), so even though the report is now
309
+ same-origin it has no JS to touch the app page — static reports only. -->
310
+ <iframe id="report-frame" sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox" title="Report"></iframe>
303
311
  <div class="report-overlay" id="report-overlay" hidden></div>
304
312
  </div>
305
313
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floless/app",
3
- "version": "0.12.3",
3
+ "version": "0.13.0",
4
4
  "type": "module",
5
5
  "description": "Thin localhost host for floless.app — serves web/ and shells the aware CLI. No engine, no LLM.",
6
6
  "bin": {