@floless/app 0.12.3 → 0.14.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.14.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.14.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
@@ -1904,7 +1904,7 @@
1904
1904
  openNotesPopover({ component: 'app', version, anchorEl: $appUpdate, onUpdate: applyAppUpdate });
1905
1905
  };
1906
1906
  refreshUpdate();
1907
- setInterval(refreshUpdate, 6 * 60 * 60 * 1000); // re-check every 6h so a long-running window notices
1907
+ setInterval(refreshUpdate, 60 * 60 * 1000); // hourly backstop; the focus re-check (below) is the fast path
1908
1908
  }
1909
1909
 
1910
1910
  // Dev-override helper — the ONLY forced-visible path. Reveals a pill with a fixed
@@ -2002,7 +2002,34 @@
2002
2002
  openNotesPopover({ component: 'aware', version, anchorEl: $awareUpdate, onUpdate: applyAwareUpdate });
2003
2003
  };
2004
2004
  refreshAwareUpdate();
2005
- setInterval(refreshAwareUpdate, 6 * 60 * 60 * 1000); // re-check every 6h
2005
+ setInterval(refreshAwareUpdate, 60 * 60 * 1000); // hourly backstop; the focus re-check (below) is the fast path
2006
+ }
2007
+
2008
+ // Re-check both update pills the instant the tab/window regains focus, so returning
2009
+ // to a long-open tab surfaces a freshly-released version immediately instead of waiting
2010
+ // out the hourly backstop poll above — the gap that used to make users hard-refresh (F5)
2011
+ // to see a new release. Guards: only when the tab is actually visible (never poll for a
2012
+ // backgrounded tab); throttled so rapid focus/blur — or the interval firing alongside —
2013
+ // can't storm the feed; and skipped while a pill is mid-apply (disabled, "Updating…") so
2014
+ // a focus event never stomps an in-flight update. Each refresh is already a no-op when its
2015
+ // pill is absent (dev hides them unless force-flagged), so this is safe to wire whenever
2016
+ // either pill exists.
2017
+ if ($appUpdate || $awareUpdate) {
2018
+ const UPDATE_FOCUS_THROTTLE_MS = 10 * 1000;
2019
+ let lastUpdateCheck = Date.now(); // the on-load checks above just ran — don't immediately re-fire
2020
+ const recheckUpdatesOnFocus = () => {
2021
+ if (document.visibilityState !== 'visible') return;
2022
+ // Offline: a failing fetch lands in refreshUpdate's catch and would briefly hide an
2023
+ // already-shown pill — skip and let the next focus/interval (back online) re-check.
2024
+ if (!navigator.onLine) return;
2025
+ const now = Date.now();
2026
+ if (now - lastUpdateCheck < UPDATE_FOCUS_THROTTLE_MS) return;
2027
+ lastUpdateCheck = now;
2028
+ if ($appUpdate && !$appUpdate.disabled) refreshUpdate();
2029
+ if ($awareUpdate && !$awareUpdate.disabled) refreshAwareUpdate();
2030
+ };
2031
+ document.addEventListener('visibilitychange', recheckUpdatesOnFocus);
2032
+ window.addEventListener('focus', recheckUpdatesOnFocus);
2006
2033
  }
2007
2034
 
2008
2035
  // The AWARE version label, once it's a notes affordance (post in-place upgrade),
@@ -2756,6 +2783,8 @@
2756
2783
  if (window.flolessPanels) window.flolessPanels.refreshData(); // routine runs also refresh bindings
2757
2784
  } else if (m.type === 'trigger-session-changed') {
2758
2785
  applyTriggerSnapshot(m.id, m.snapshot);
2786
+ } else if (m.type === 'trigger-report') {
2787
+ applyTriggerReport(m);
2759
2788
  } else if (m.type === 'connect-result') {
2760
2789
  // A run's credential-reconnect card may be waiting on this integration —
2761
2790
  // drive its success/fail state from the same SSE result.
@@ -3378,9 +3407,22 @@
3378
3407
  if (!r) { loadRoutinesData(); return; } // unknown id (created in another tab) → resync
3379
3408
  const prevFired = (r.session && r.session.firedCount) || 0;
3380
3409
  r.session = snapshot;
3410
+ const fired = !!(snapshot && snapshot.firedCount > prevFired);
3411
+ // A new fire lands a beat BEFORE its report (the source node-output streams
3412
+ // first, then the exec runs ~1–3s and emits the report HTML). If the HTML
3413
+ // Viewer is open for this app, show a "collecting…" spinner over the last
3414
+ // report until applyTriggerReport paints the fresh one.
3415
+ const viewerOpenForApp = currentId === r.workflow && $reportModal.classList.contains('show') && !reportRunning;
3416
+ if (fired && viewerOpenForApp) {
3417
+ showTriggerCollecting();
3418
+ } else if (viewerOpenForApp && (snapshot.state === 'error' || snapshot.state === 'stopped') && $reportOverlay.querySelector('.spinner')) {
3419
+ // The session ended before this fire produced a report — don't spin forever.
3420
+ // Fall back to the last report if we have one, else an idle prompt.
3421
+ clearTriggerCollecting();
3422
+ }
3381
3423
  if (!$routinesModal.classList.contains('show')) return;
3382
3424
  renderRoutinesList();
3383
- if (snapshot && snapshot.firedCount > prevFired) flashRoutineRow(id);
3425
+ if (fired) flashRoutineRow(id);
3384
3426
  }
3385
3427
 
3386
3428
  // Briefly tint a row when it just fired. Re-applies on rapid repeats via a forced
@@ -3394,6 +3436,59 @@
3394
3436
  el.classList.add('rtn-fired-flash');
3395
3437
  }
3396
3438
 
3439
+ // A trigger routine (e.g. tekla-selection-report on each SelectionChange) just
3440
+ // produced a fresh report. Always cache it (so opening the viewer shows the
3441
+ // latest), and if the HTML Viewer is ALREADY open for THIS app, live-repaint it
3442
+ // — the user asked to watch the report update on each selection change. We never
3443
+ // auto-open the viewer, never hijack another app's open viewer, and never stomp
3444
+ // an in-flight manual run. (The live repaint is another srcdoc assignment, so it
3445
+ // depends on the report-frame `allow-same-origin` fix to repaint in Chrome.)
3446
+ function applyTriggerReport(m) {
3447
+ if (!m || !m.workflow || typeof m.html !== 'string') return;
3448
+ lastReportByApp.set(m.workflow, { nodeId: m.nodeId || null, label: 'live · on selection change', html: m.html });
3449
+ const open = $reportModal && $reportModal.classList.contains('show');
3450
+ if (!open || currentId !== m.workflow || reportRunning) return;
3451
+ paintReport(m.html); // unhides Share + clears the overlay
3452
+ const t = new Date().toLocaleTimeString();
3453
+ $reportSub.innerHTML =
3454
+ `<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.`;
3455
+ pulseReportFrame();
3456
+ }
3457
+
3458
+ // Brief edge-flash on the report stage when a live update lands, so the change
3459
+ // is noticeable. Forced reflow re-arms it on rapid repeats. CSS honors
3460
+ // prefers-reduced-motion.
3461
+ function pulseReportFrame() {
3462
+ const stage = document.getElementById('report-stage');
3463
+ if (!stage) return;
3464
+ stage.classList.remove('report-live-pulse');
3465
+ void stage.offsetWidth;
3466
+ stage.classList.add('report-live-pulse');
3467
+ }
3468
+
3469
+ // A trigger fired (new selection) but its report is still computing on the host.
3470
+ // Show a spinner over the viewer — the previous report dims behind the overlay —
3471
+ // until applyTriggerReport paints the fresh one. Built with DOM nodes (no
3472
+ // innerHTML); reuses the same .spinner the manual run overlay uses.
3473
+ function showTriggerCollecting() {
3474
+ $reportOverlay.hidden = false;
3475
+ $reportOverlay.replaceChildren();
3476
+ const sp = mkEl('div', 'spinner');
3477
+ const msg = mkEl('div', null, 'New selection — collecting report…');
3478
+ $reportOverlay.append(sp, msg);
3479
+ }
3480
+
3481
+ // The trigger session errored/stopped while a "collecting…" spinner was up (a
3482
+ // fire whose report never arrived) — clear it instead of spinning forever:
3483
+ // revert to the last cached report if there is one, else an idle prompt.
3484
+ function clearTriggerCollecting() {
3485
+ const cached = currentId && lastReportByApp.get(currentId);
3486
+ if (cached && cached.html) { paintReport(cached.html); return; }
3487
+ $reportOverlay.replaceChildren();
3488
+ $reportOverlay.hidden = false;
3489
+ $reportOverlay.append(mkEl('div', null, 'Listening for the next selection…'));
3490
+ }
3491
+
3397
3492
  function renderRoutinesList() {
3398
3493
  const $list = document.getElementById('routines-list');
3399
3494
  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.14.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": {