@floless/app 0.7.0 → 0.7.1

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.
@@ -52308,7 +52308,7 @@ function appVersion() {
52308
52308
  return resolveVersion({
52309
52309
  isSea: isSea2(),
52310
52310
  sqVersionXml: readSqVersionXml(),
52311
- define: true ? "0.7.0" : void 0,
52311
+ define: true ? "0.7.1" : void 0,
52312
52312
  pkgVersion: readPkgVersion()
52313
52313
  });
52314
52314
  }
@@ -52318,7 +52318,7 @@ function resolveChannel(s) {
52318
52318
  return "dev";
52319
52319
  }
52320
52320
  function appChannel() {
52321
- return resolveChannel({ isSea: isSea2(), define: true ? "0.7.0" : void 0 });
52321
+ return resolveChannel({ isSea: isSea2(), define: true ? "0.7.1" : void 0 });
52322
52322
  }
52323
52323
 
52324
52324
  // oauth-presets.ts
@@ -56614,6 +56614,30 @@ async function startServer() {
56614
56614
  broadcast({ type: "grafted", id: stage.agentId });
56615
56615
  return { ok: true, agentId: stage.agentId };
56616
56616
  });
56617
+ async function detectCredentialIssue(id) {
56618
+ try {
56619
+ const app2 = readApp(id);
56620
+ const agents = new Set(app2.nodes.map((n) => n.agent).filter((a) => !!a));
56621
+ if (!agents.size) return null;
56622
+ const live = await aware.connectList();
56623
+ for (const c of live) {
56624
+ if (agents.has(c.integration) && c.status !== "valid") {
56625
+ return { id: c.integration, label: friendlyIntegrationName(c.integration) };
56626
+ }
56627
+ }
56628
+ } catch {
56629
+ }
56630
+ return null;
56631
+ }
56632
+ function hasAuthFailure(events) {
56633
+ for (const ev of events) {
56634
+ const data = ev.data;
56635
+ const result = data?.result ?? data;
56636
+ const status = result && typeof result.status === "number" ? result.status : null;
56637
+ if (status === 401 || status === 403) return true;
56638
+ }
56639
+ return false;
56640
+ }
56617
56641
  app.post(
56618
56642
  "/api/run",
56619
56643
  async (req, reply) => {
@@ -56633,7 +56657,8 @@ async function startServer() {
56633
56657
  }
56634
56658
  const stderr = detail && typeof detail.stderr === "string" ? detail.stderr.trim() : "";
56635
56659
  app.log.warn({ detail: err.detail }, err.message);
56636
- return reply.send({ ok: false, error: err.message, reason: stderr || null, simulate: !!simulate });
56660
+ const credentialIssue2 = simulate ? null : await detectCredentialIssue(id);
56661
+ return reply.send({ ok: false, error: err.message, reason: stderr || null, simulate: !!simulate, credentialIssue: credentialIssue2 });
56637
56662
  }
56638
56663
  throw err;
56639
56664
  }
@@ -56642,7 +56667,8 @@ async function startServer() {
56642
56667
  for (const ev of events) broadcast({ type: "trace", id, event: ev });
56643
56668
  broadcast({ type: "run-ended", id, runId: result.runId });
56644
56669
  const report = extractReportHtml(events);
56645
- return { ok: true, runId: result.runId, tracePath: result.tracePath, events, report };
56670
+ const credentialIssue = !simulate && hasAuthFailure(events) ? await detectCredentialIssue(id) : null;
56671
+ return { ok: true, runId: result.runId, tracePath: result.tracePath, events, report, credentialIssue };
56646
56672
  }
56647
56673
  );
56648
56674
  app.post("/api/run/stop", async () => {
package/dist/web/app.css CHANGED
@@ -2065,6 +2065,42 @@ body {
2065
2065
  }
2066
2066
  .report-overlay .overlay-stop:disabled { opacity: 0.6; cursor: default; }
2067
2067
 
2068
+ /* Credential-failure reconnect card — shown in .report-overlay when a run hits a
2069
+ rejected OAuth token. Amber-topped (recoverable, needs attention); the Reconnect
2070
+ action keeps primary-blue (the one critical next step), Open Integrations is ghost. */
2071
+ .cred-fail-card {
2072
+ background: var(--surface); border: 1px solid var(--border-strong); border-top: 3px solid var(--warn);
2073
+ border-radius: 8px; padding: 22px 24px; max-width: 380px; width: 100%;
2074
+ display: flex; flex-direction: column; gap: 14px; text-align: left;
2075
+ }
2076
+ .cred-fail-status {
2077
+ display: flex; align-items: center; gap: 8px;
2078
+ font-size: 10px; text-transform: uppercase; letter-spacing: 0.14em; color: var(--text-dim);
2079
+ }
2080
+ .cred-fail-icon { color: var(--warn); font-size: 16px; line-height: 1; }
2081
+ .cred-fail-headline { font-size: 15px; font-weight: 600; color: var(--text); }
2082
+ .cred-fail-body { font-size: 13px; color: var(--text-muted); line-height: 1.55; }
2083
+ .cred-fail-err { font-size: 12px; color: var(--err); }
2084
+ .cred-fail-ok { font-size: 13px; color: var(--ok); font-weight: 600; }
2085
+ .cred-fail-actions { display: flex; align-items: center; gap: 8px; margin-top: 4px; }
2086
+ .cred-fail-actions button { font-family: var(--ui); font-size: 13px; line-height: 1.2; border-radius: 4px; cursor: pointer; }
2087
+ .cred-fail-actions .cred-reconnect {
2088
+ padding: 7px 16px; background: var(--accent); color: white; border: 1px solid var(--accent); font-weight: 600;
2089
+ }
2090
+ .cred-fail-actions .cred-reconnect:hover:not(:disabled) {
2091
+ background: var(--accent-bright); box-shadow: 0 0 14px var(--accent-glow);
2092
+ }
2093
+ .cred-fail-actions .cred-reconnect:disabled { opacity: 0.7; cursor: default; }
2094
+ .cred-fail-actions .cred-open-integrations {
2095
+ padding: 7px 12px; background: transparent; border: 1px solid var(--border-strong); color: var(--text-muted);
2096
+ }
2097
+ .cred-fail-actions .cred-open-integrations:hover { color: var(--text); border-color: var(--accent-dim); }
2098
+ .cred-reconnect-spinner {
2099
+ display: inline-block; width: 12px; height: 12px; vertical-align: -2px; margin-right: 6px;
2100
+ border: 2px solid rgba(255, 255, 255, 0.3); border-top-color: #fff; border-radius: 50%;
2101
+ animation: spin 0.8s linear infinite;
2102
+ }
2103
+
2068
2104
  /* Report + input nodes on the canvas carry a first-class action button
2069
2105
  ("View report ▸" / "Set inputs ▸"). Double-click still works as a shortcut. */
2070
2106
  .agent-card.report-node,
package/dist/web/aware.js CHANGED
@@ -106,7 +106,11 @@
106
106
  ...opts,
107
107
  });
108
108
  const body = await res.json().catch(() => ({ ok: false, error: `HTTP ${res.status}` }));
109
- if (!res.ok || body.ok === false) throw new Error(body.error || `HTTP ${res.status}`);
109
+ if (!res.ok || body.ok === false) {
110
+ const err = new Error(body.error || `HTTP ${res.status}`);
111
+ err.body = body; // preserve the in-band payload (e.g. credentialIssue) for callers
112
+ throw err;
113
+ }
110
114
  return body;
111
115
  }
112
116
 
@@ -959,6 +963,9 @@
959
963
  : await api('/api/run', { method: 'POST', body: JSON.stringify({ id: currentId, simulate: false, inputs }) });
960
964
  // Reflect the run in the Execution tab too.
961
965
  if (Array.isArray(res.events)) { liveTrace.length = 0; res.events.forEach(pushTrace); state.hasRun = true; if (state.currentTab === 'execution') renderInspect(); }
966
+ // A run can "succeed" yet carry a rejected token (the report would be an error
967
+ // body, not real data) — prompt reconnect instead of rendering the noise.
968
+ if (res.credentialIssue) { surfaceCredentialIssue(res.credentialIssue); return; }
962
969
  const html = res.report && res.report.html;
963
970
  if (!html) {
964
971
  $reportOverlay.innerHTML = `<div>Run completed but <code>${escapeHtml(nodeId)}</code> returned no <code>html</code>. Check the Execution tab.</div>`;
@@ -977,17 +984,115 @@
977
984
  $reportOverlay.innerHTML = `<div>Run cancelled. A node already running on the host may still finish there; click <strong>▶ Run workflow</strong> to run again.</div>`;
978
985
  showToast('Run cancelled', 'info');
979
986
  } else {
980
- // Expected, recoverable failure (host not attached, stale lock, …): show it
981
- // in the viewer overlay + a toast. No console.error — the server returns the
982
- // failure in-band, so a failed run isn't a console-worthy fault.
983
- $reportOverlay.innerHTML = `<div>Run failed: ${escapeHtml(msg)}. Check the host is attached and the lock is fresh.</div>`;
984
- showToast('Run failed: ' + msg, 'warn');
987
+ // Expected, recoverable failure. If it was a rejected token, prompt the
988
+ // user to reconnect that integration (not the misleading host hint).
989
+ const cred = e.body && e.body.credentialIssue;
990
+ if (cred) {
991
+ surfaceCredentialIssue(cred);
992
+ } else {
993
+ // Other recoverable failures (host not attached, stale lock, …): show it
994
+ // in the viewer overlay + a toast. No console.error — the server returns the
995
+ // failure in-band, so a failed run isn't a console-worthy fault.
996
+ $reportOverlay.innerHTML = `<div>Run failed: ${escapeHtml(msg)}. Check the host is attached and the lock is fresh.</div>`;
997
+ showToast('Run failed: ' + msg, 'warn');
998
+ }
985
999
  }
986
1000
  } finally {
987
1001
  reportRunning = false;
988
1002
  }
989
1003
  }
990
1004
 
1005
+ // ── Credential-reconnect card ──────────────────────────────────────────────
1006
+ // When a real run fails (or "succeeds" with a 401 body) because an integration's
1007
+ // OAuth token was rejected, the server returns `credentialIssue: {id,label}`. We
1008
+ // replace the misleading "check the host" message with a focused prompt to
1009
+ // reconnect that integration — generic across Trimble, M365, any OAuth provider.
1010
+ // Built with DOM nodes + textContent (no innerHTML) so the integration label can
1011
+ // never inject markup.
1012
+ let credReconnect = null; // { id, onSuccess, onFail } — latched while a card reconnect runs
1013
+
1014
+ function mkEl(tag, cls, text) {
1015
+ const n = document.createElement(tag);
1016
+ if (cls) n.className = cls;
1017
+ if (text != null) n.textContent = text;
1018
+ return n;
1019
+ }
1020
+
1021
+ const CRED_BODY_IDLE = 'Sign in again and then run the workflow — it takes about 30 seconds.';
1022
+
1023
+ function showCredentialReconnect(cred) {
1024
+ $reportFrame.srcdoc = '';
1025
+ $reportOverlay.hidden = false;
1026
+ $reportOverlay.replaceChildren();
1027
+
1028
+ const card = mkEl('div', 'cred-fail-card');
1029
+ card.setAttribute('role', 'status');
1030
+
1031
+ const status = mkEl('div', 'cred-fail-status');
1032
+ const icon = mkEl('span', 'cred-fail-icon', iconFor(cred.id));
1033
+ icon.setAttribute('aria-hidden', 'true');
1034
+ status.append(icon, document.createTextNode('SESSION EXPIRED'));
1035
+
1036
+ const headline = mkEl('div', 'cred-fail-headline', `Your ${cred.label} session expired`);
1037
+ const body = mkEl('div', 'cred-fail-body', CRED_BODY_IDLE);
1038
+
1039
+ const actions = mkEl('div', 'cred-fail-actions');
1040
+ const reconnect = mkEl('button', 'primary cred-reconnect', 'Reconnect');
1041
+ reconnect.type = 'button';
1042
+ const openInt = mkEl('button', 'cred-open-integrations', 'Open Integrations');
1043
+ openInt.type = 'button';
1044
+ actions.append(reconnect, openInt);
1045
+
1046
+ card.append(status, headline, body, actions);
1047
+ $reportOverlay.append(card);
1048
+ showModal($reportModal);
1049
+
1050
+ openInt.onclick = () => { hideModal($reportModal); openIntegrations(); };
1051
+ reconnect.onclick = () => credReconnectStart(cred, card, reconnect, body);
1052
+ reconnect.focus();
1053
+ }
1054
+
1055
+ function credReconnectStart(cred, card, btn, body) {
1056
+ const reset = (errMsg) => {
1057
+ credReconnect = null;
1058
+ btn.disabled = false; btn.textContent = 'Reconnect';
1059
+ body.textContent = CRED_BODY_IDLE;
1060
+ let e = card.querySelector('.cred-fail-err');
1061
+ if (!e) { e = mkEl('div', 'cred-fail-err'); body.after(e); }
1062
+ e.textContent = errMsg;
1063
+ };
1064
+ const oldErr = card.querySelector('.cred-fail-err'); if (oldErr) oldErr.remove();
1065
+ btn.disabled = true;
1066
+ btn.textContent = '';
1067
+ const sp = mkEl('span', 'cred-reconnect-spinner'); sp.setAttribute('aria-hidden', 'true');
1068
+ btn.append(sp, document.createTextNode('Signing in…'));
1069
+ body.textContent = 'Opening your browser to sign in — complete it there and this will update.';
1070
+
1071
+ credReconnect = {
1072
+ id: cred.id,
1073
+ onSuccess: () => {
1074
+ credReconnect = null;
1075
+ const actions = card.querySelector('.cred-fail-actions'); if (actions) actions.remove();
1076
+ body.replaceChildren(mkEl('span', 'cred-fail-ok', 'Connected — click ▶ Run workflow to continue.'));
1077
+ setTimeout(() => {
1078
+ if ($reportOverlay.querySelector('.cred-fail-card')) { $reportOverlay.hidden = true; $reportOverlay.replaceChildren(); }
1079
+ }, 2000);
1080
+ },
1081
+ onFail: () => reset('Sign-in failed — try again, or use the device code flow in Integrations.'),
1082
+ };
1083
+ api(`/api/connect/${encodeURIComponent(cred.id)}`, { method: 'POST', body: '{}' })
1084
+ .then((res) => { if (res && res.started === false) reset('Couldn’t start sign-in — open Integrations to retry.'); })
1085
+ .catch(() => reset('Sign-in failed — try again, or use the device code flow in Integrations.'));
1086
+ }
1087
+
1088
+ // Shared by both run paths: when the server flags a credential issue, show the
1089
+ // reconnect card + a quiet toast/narration instead of the generic failure copy.
1090
+ function surfaceCredentialIssue(cred) {
1091
+ showCredentialReconnect(cred);
1092
+ showToast(`Session expired — reconnect ${cred.label} to continue.`, 'warn');
1093
+ appendNarration(`Run stopped — your <strong>${escapeHtml(cred.label)}</strong> session expired. Reconnect and run again.`);
1094
+ }
1095
+
991
1096
  function showModal(el) { el.classList.add('show'); }
992
1097
  function hideModal(el) { el.classList.remove('show'); }
993
1098
 
@@ -1622,6 +1727,8 @@
1622
1727
  const res = await api('/api/run', { method: 'POST', body: JSON.stringify({ id: currentId, simulate, inputs: currentInputs() }) });
1623
1728
  // SSE usually fills liveTrace first; backfill from the response if it didn't.
1624
1729
  if (liveTrace.length === 0 && Array.isArray(res.events)) res.events.forEach(pushTrace);
1730
+ // A "succeeded" run carrying a rejected token → prompt reconnect, not a result.
1731
+ if (res.credentialIssue) { surfaceCredentialIssue(res.credentialIssue); return; }
1625
1732
  switchToExecution();
1626
1733
  const steps = liveTrace.filter((r) => r.kind === 'node-start').length;
1627
1734
  appendNarration(simulate
@@ -1634,12 +1741,18 @@
1634
1741
  showToast('Run cancelled', 'info');
1635
1742
  appendNarration('Run cancelled. Click <strong>▶ Run workflow</strong> to run again.');
1636
1743
  } else {
1637
- // A failed run is an expected, recoverable outcome (the server returns it
1638
- // in-band) — surface it as a toast + narration, not a console error.
1639
- showToast((simulate ? 'Simulate failed' : 'Run failed') + ': ' + msg, 'warn');
1640
- appendNarration(simulate
1641
- ? `Simulate couldn't validate <code>${escapeHtml(currentId)}</code> — it stubs each node from its output-schema, but exec nodes have none, so the cross-node templates render undefined. Use <strong>▶ Run workflow</strong> for a live run against the host.`
1642
- : `Run failed: ${escapeHtml(msg)}. Check the host is attached and the lock is fresh.`);
1744
+ const cred = e.body && e.body.credentialIssue;
1745
+ if (cred) {
1746
+ // A rejected token prompt reconnect for that integration, not the host hint.
1747
+ surfaceCredentialIssue(cred);
1748
+ } else {
1749
+ // A failed run is an expected, recoverable outcome (the server returns it
1750
+ // in-band) — surface it as a toast + narration, not a console error.
1751
+ showToast((simulate ? 'Simulate failed' : 'Run failed') + ': ' + msg, 'warn');
1752
+ appendNarration(simulate
1753
+ ? `Simulate couldn't validate <code>${escapeHtml(currentId)}</code> — it stubs each node from its output-schema, but exec nodes have none, so the cross-node templates render undefined. Use <strong>▶ Run workflow</strong> for a live run against the host.`
1754
+ : `Run failed: ${escapeHtml(msg)}. Check the host is attached and the lock is fresh.`);
1755
+ }
1643
1756
  }
1644
1757
  } finally {
1645
1758
  state.running = false;
@@ -2215,6 +2328,12 @@
2215
2328
  } else if (m.type === 'trigger-session-changed') {
2216
2329
  applyTriggerSnapshot(m.id, m.snapshot);
2217
2330
  } else if (m.type === 'connect-result') {
2331
+ // A run's credential-reconnect card may be waiting on this integration —
2332
+ // drive its success/fail state from the same SSE result.
2333
+ if (credReconnect && credReconnect.id === m.id) {
2334
+ if (m.status === 'connected') credReconnect.onSuccess();
2335
+ else credReconnect.onFail(m.error);
2336
+ }
2218
2337
  // The device-code sign-in resolved. On success, refresh so the card flips
2219
2338
  // to Connected; otherwise surface the outcome in the integration's slot.
2220
2339
  connecting.delete(m.id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floless/app",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
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": {