@floless/app 0.69.0 → 0.70.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.
@@ -4,10 +4,11 @@ display-name: Dummy Report
4
4
  description: |
5
5
  A self-contained test workflow that produces a styled HTML report — no Tekla
6
6
  model required. Neither node touches `model`, so it runs on the host bridge with
7
- nothing open. Use it to preview the in-app HTML Viewer and the "Made with
8
- FloLess" report badge end to end without a real model. Two real tekla/exec nodes:
9
- a title input → a report node that builds the HTML inline and is the terminal the
10
- viewer renders.
7
+ nothing open. Use it to preview the in-app HTML Viewer, the "Made with FloLess"
8
+ report badge, and the viewer's "Open file" / "Reveal in folder" buttons end to
9
+ end without a real model. Two real tekla/exec nodes: a title input a report
10
+ node that builds the HTML inline, saves a copy to a file in your temp folder, and
11
+ is the terminal the viewer renders.
11
12
  exposes-as-agent: false
12
13
  inputs:
13
14
  title:
@@ -47,11 +48,14 @@ nodes:
47
48
  args:
48
49
  title: "{{ title-input.result.title }}"
49
50
  code: |
50
- // Dummy report — builds a styled HTML report inline and returns it as this
51
- // terminal node's `html`, so the floless.app HTML Viewer renders it on
52
- // double-click. Touches NO `model` (runs with no Tekla open) and writes no
53
- // file. Pure string build, manual HTML-escape (no System.Net available).
51
+ // Dummy report — builds a styled HTML report inline, saves a copy to a file
52
+ // in your temp folder, and returns the report as this terminal node's `html`
53
+ // (so the floless.app HTML Viewer renders it on double-click) plus that file's
54
+ // path as `saved_path` (so the viewer's "Open file" / "Reveal in folder"
55
+ // buttons have a real file to act on). Touches NO `model`, so it runs with no
56
+ // Tekla open. Manual HTML-escape (no System.Net available).
54
57
  using System;
58
+ using System.IO;
55
59
  using System.Text;
56
60
 
57
61
  string title = "FloLess Test Report";
@@ -99,6 +103,20 @@ nodes:
99
103
  sb.Append("<div class=\"ft\">This is a dummy report for testing the FloLess HTML viewer.</div>");
100
104
  sb.Append("</div></body></html>");
101
105
 
102
- return new { ok = true, title, html = sb.ToString() };
106
+ string html = sb.ToString();
107
+
108
+ // Also SAVE the report to a real file on disk so the viewer's "Open file" /
109
+ // "Reveal in folder" buttons have something to act on. Best-effort: a write
110
+ // failure just means no saved file (the report still renders inline).
111
+ string savedPath = null;
112
+ try
113
+ {
114
+ var outPath = Path.Combine(Path.GetTempPath(), "floless-dummy-report.html");
115
+ File.WriteAllText(outPath, html);
116
+ savedPath = outPath;
117
+ }
118
+ catch { savedPath = null; }
119
+
120
+ return new { ok = true, title, html, saved_path = savedPath };
103
121
  connections:
104
122
  - { from: title-input, to: report }
package/dist/web/aware.js CHANGED
@@ -24,6 +24,8 @@
24
24
  const $reportSub = document.getElementById('report-sub');
25
25
  const $reportOpen = document.getElementById('report-open');
26
26
  const $reportShare = document.getElementById('report-share');
27
+ const $reportOpenFile = document.getElementById('report-open-file');
28
+ const $reportRevealFile = document.getElementById('report-reveal-file');
27
29
  const $reportClose = document.getElementById('report-close');
28
30
  const $reportStop = document.getElementById('report-stop');
29
31
  const $stopRunBtn = document.getElementById('stop-run-btn');
@@ -1699,12 +1701,26 @@
1699
1701
  // never a disabled affordance with no path forward).
1700
1702
  // The last HTML painted (static or 3D) — so ↗ Open works for both surfaces.
1701
1703
  let lastPaintedHtml = '';
1704
+ // The workflow-written output file the current report reported (its run's
1705
+ // `saved_path`), or '' when the run wrote none. Drives the ⧉ Open file / ▤ Reveal
1706
+ // buttons — both hide together when there's no file (#209). Hidden is the default:
1707
+ // paintReport clears it every paint, and only a run/cached-show with a real path
1708
+ // re-shows it — so a trigger/3D/other paint can never leak a previous file's path.
1709
+ let currentSavedPath = '';
1710
+ function showSavedPathActions(path) {
1711
+ currentSavedPath = typeof path === 'string' ? path : '';
1712
+ const has = !!currentSavedPath;
1713
+ $reportOpenFile.hidden = !has;
1714
+ $reportRevealFile.hidden = !has;
1715
+ }
1716
+
1702
1717
  // Paint a report into the viewer. A STATIC report uses the scriptless srcdoc frame; an
1703
1718
  // INTERACTIVE 3D doc (viewer-3d) uses the script-enabled frame via a data: URL (opaque
1704
1719
  // origin, cannot touch the app page) and keeps a "Loading 3D view…" overlay until the doc
1705
1720
  // posts `viewer-ready`. Exactly one frame is shown at a time.
1706
1721
  function paintReport(html, interactive) {
1707
1722
  lastPaintedHtml = html || '';
1723
+ showSavedPathActions(''); // default off; the caller re-shows if this report saved a file
1708
1724
  if (interactive) {
1709
1725
  $reportFrame.hidden = true; $reportFrame.srcdoc = '';
1710
1726
  $reportFrame3d.hidden = false;
@@ -1727,6 +1743,7 @@
1727
1743
  $reportFrame.srcdoc = ''; $reportFrame.hidden = false;
1728
1744
  $reportFrame3d.hidden = true; $reportFrame3d.removeAttribute('src');
1729
1745
  lastPaintedHtml = '';
1746
+ showSavedPathActions(''); // no report shown → no saved-file actions
1730
1747
  }
1731
1748
 
1732
1749
  // Double-click the HTML Viewer node → LOAD the last report (no run; running is
@@ -1748,6 +1765,7 @@
1748
1765
  $reportSub.textContent = `Last report${sfx} — rendered from the run's output, never composed by the UI.`;
1749
1766
  }
1750
1767
  paintReport(cached.html, cached.interactive);
1768
+ showSavedPathActions(cached.savedPath); // re-show Open file / Reveal if this run saved one
1751
1769
  } else {
1752
1770
  clearReportFrames();
1753
1771
  $reportShare.hidden = true; // nothing to share — hide, don't disable
@@ -2109,8 +2127,12 @@
2109
2127
  $reportSub.innerHTML = `Live 3D view from <code>${escapeHtml(nodeId)}</code>${inputBadge ? ' · ' + escapeHtml(inputBadge) : ''} — drag to orbit, scroll to zoom, click an element to inspect.`;
2110
2128
  }
2111
2129
  paintReport(html, interactive);
2130
+ // If a node wrote an output file, the run reports its path — surface the
2131
+ // ⧉ Open file / ▤ Reveal buttons and cache it so a later double-click restores them (#209).
2132
+ const savedPath = res && typeof res.savedPath === 'string' ? res.savedPath : '';
2133
+ showSavedPathActions(savedPath);
2112
2134
  markFirstRunDone(app.id); // a real run rendered → this workflow is no longer "new" (app captured pre-await; a mid-run switch must not mark the wrong workflow)
2113
- lastReportByApp.set(currentId, { nodeId, label: inputBadge, html, interactive });
2135
+ lastReportByApp.set(currentId, { nodeId, label: inputBadge, html, interactive, savedPath });
2114
2136
  $reportModal.dataset.html = '1';
2115
2137
  appendNarration(`Rendered the ${interactive ? '3D view' : 'report'} from <strong>${escapeHtml(nodeId)}</strong>${inputBadge ? ' (' + escapeHtml(inputBadge) + ')' : ''} — live run against the model.`);
2116
2138
  } catch (e) {
@@ -2664,6 +2686,22 @@
2664
2686
  });
2665
2687
  // The header ■ Stop run — the always-reachable twin of the overlay Stop (#39).
2666
2688
  if ($stopRunBtn) $stopRunBtn.onclick = () => stopRun();
2689
+ // ⧉ Open file / ▤ Reveal in folder — act on the run's saved output file via the OS.
2690
+ // The sandboxed report can't (file:// is blocked from an http page), so the local
2691
+ // host shells it (#209). Fire-and-forget: a 400 (file moved/deleted, or bad type)
2692
+ // surfaces as a toast, never a console error.
2693
+ async function savedFileAction(endpoint, failMsg) {
2694
+ if (!currentSavedPath) return; // button is hidden in this state; belt-and-braces
2695
+ try {
2696
+ await api(endpoint, { method: 'POST', body: JSON.stringify({ path: currentSavedPath }) });
2697
+ } catch (e) {
2698
+ const detail = e && e.body && e.body.error;
2699
+ showToast(detail ? `${failMsg} — ${detail}` : failMsg, 'warn');
2700
+ }
2701
+ }
2702
+ $reportOpenFile.onclick = () => savedFileAction('/api/open-file', 'Couldn’t open the file');
2703
+ $reportRevealFile.onclick = () => savedFileAction('/api/reveal', 'Couldn’t reveal the file');
2704
+
2667
2705
  $reportOpen.onclick = () => {
2668
2706
  const html = lastPaintedHtml || $reportFrame.srcdoc;
2669
2707
  if (!html) return;
@@ -6392,12 +6430,15 @@
6392
6430
  window.flolessBridge = {
6393
6431
  api,
6394
6432
  // Open report HTML in the existing sandboxed HTML Viewer (same modal + srcdoc
6395
- // iframe the canvas report node uses — no new sandbox surface).
6396
- showHtmlReport(title, html) {
6433
+ // iframe the canvas report node uses — no new sandbox surface). `savedPath` is
6434
+ // optional: pass the run's saved_path so a dashboard-panel run surfaces the same
6435
+ // ⧉ Open file / ▤ Reveal buttons as a canvas run (#209); omit → they stay hidden.
6436
+ showHtmlReport(title, html, savedPath) {
6397
6437
  $reportTitle.textContent = title;
6398
6438
  $reportSub.textContent = "Rendered from the run's output — never composed by the UI.";
6399
6439
  showModal($reportModal);
6400
6440
  paintReport(html);
6441
+ showSavedPathActions(savedPath);
6401
6442
  },
6402
6443
  // Refresh the footer requests counter after the Customize box queues a request.
6403
6444
  loadRequests,
@@ -395,6 +395,8 @@
395
395
  <div class="report-actions">
396
396
  <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>
397
397
  <button id="report-share" class="ghost" hidden data-tip="Share this report as a link anyone can view — hosted on floless.io">↗ Share</button>
398
+ <button id="report-open-file" class="ghost" hidden data-tip="Open the saved file in its default app">⧉ Open file</button>
399
+ <button id="report-reveal-file" class="ghost" hidden data-tip="Show the saved file in its folder">▤ Reveal in folder</button>
398
400
  <button id="report-open" class="ghost" data-tip="Open the report in a new browser tab">↗ Open</button>
399
401
  <button id="report-close" data-tip="Close">×</button>
400
402
  </div>
@@ -398,7 +398,8 @@
398
398
  const payload = payloadFor(source);
399
399
  const html = payload && typeof payload === 'object' && typeof payload.html === 'string' ? payload.html : null;
400
400
  if (!html) { showToast('No report yet — run the workflow first.', 'info'); return; }
401
- if (bridge.showHtmlReport) bridge.showHtmlReport(title || 'Report', html);
401
+ const savedPath = payload && typeof payload.saved_path === 'string' ? payload.saved_path : null;
402
+ if (bridge.showHtmlReport) bridge.showHtmlReport(title || 'Report', html, savedPath);
402
403
  }
403
404
 
404
405
  async function runAction(btn) {
@@ -415,7 +416,7 @@
415
416
  return;
416
417
  }
417
418
  const html = res.report && res.report.html;
418
- if (html && bridge.showHtmlReport) bridge.showHtmlReport(`${appId} · report`, html);
419
+ if (html && bridge.showHtmlReport) bridge.showHtmlReport(`${appId} · report`, html, res.savedPath);
419
420
  else showToast(`Run complete · ${appId}`, 'ok');
420
421
  } catch (err) {
421
422
  const cred = err && err.body && err.body.credentialIssue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floless/app",
3
- "version": "0.69.0",
3
+ "version": "0.70.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": {