@floless/app 0.6.3 → 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.
- package/dist/floless-server.cjs +357 -202
- package/dist/web/app.css +36 -0
- package/dist/web/aware.js +142 -14
- package/package.json +1 -1
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)
|
|
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
|
|
|
@@ -651,7 +655,12 @@
|
|
|
651
655
|
function reportNodeId() {
|
|
652
656
|
const app = currentId && apps.get(currentId);
|
|
653
657
|
if (!app || !app.nodes.length) return null;
|
|
654
|
-
//
|
|
658
|
+
// An html-report agent node renders report HTML — its `render` command returns
|
|
659
|
+
// an `html` field, which is exactly what extractReportHtml keys on. So it's a
|
|
660
|
+
// report producer regardless of exec code; prefer it directly as the viewer node.
|
|
661
|
+
const htmlReportNode = app.nodes.find((n) => n.agent === 'html-report');
|
|
662
|
+
if (htmlReportNode) return htmlReportNode.id;
|
|
663
|
+
// Otherwise, qualify the app only if some node actually PRODUCES report HTML — its exec
|
|
655
664
|
// code returns an `html` field (the `data.result.html` extractReportHtml keys
|
|
656
665
|
// on), or it forwards `{{ <node>.result.html }}` through its args (a
|
|
657
666
|
// pass-through viewer). "any node has exec code" is NOT enough: a plain
|
|
@@ -954,6 +963,9 @@
|
|
|
954
963
|
: await api('/api/run', { method: 'POST', body: JSON.stringify({ id: currentId, simulate: false, inputs }) });
|
|
955
964
|
// Reflect the run in the Execution tab too.
|
|
956
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; }
|
|
957
969
|
const html = res.report && res.report.html;
|
|
958
970
|
if (!html) {
|
|
959
971
|
$reportOverlay.innerHTML = `<div>Run completed but <code>${escapeHtml(nodeId)}</code> returned no <code>html</code>. Check the Execution tab.</div>`;
|
|
@@ -972,17 +984,115 @@
|
|
|
972
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>`;
|
|
973
985
|
showToast('Run cancelled', 'info');
|
|
974
986
|
} else {
|
|
975
|
-
// Expected, recoverable failure
|
|
976
|
-
//
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
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
|
+
}
|
|
980
999
|
}
|
|
981
1000
|
} finally {
|
|
982
1001
|
reportRunning = false;
|
|
983
1002
|
}
|
|
984
1003
|
}
|
|
985
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
|
+
|
|
986
1096
|
function showModal(el) { el.classList.add('show'); }
|
|
987
1097
|
function hideModal(el) { el.classList.remove('show'); }
|
|
988
1098
|
|
|
@@ -1617,6 +1727,8 @@
|
|
|
1617
1727
|
const res = await api('/api/run', { method: 'POST', body: JSON.stringify({ id: currentId, simulate, inputs: currentInputs() }) });
|
|
1618
1728
|
// SSE usually fills liveTrace first; backfill from the response if it didn't.
|
|
1619
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; }
|
|
1620
1732
|
switchToExecution();
|
|
1621
1733
|
const steps = liveTrace.filter((r) => r.kind === 'node-start').length;
|
|
1622
1734
|
appendNarration(simulate
|
|
@@ -1629,12 +1741,18 @@
|
|
|
1629
1741
|
showToast('Run cancelled', 'info');
|
|
1630
1742
|
appendNarration('Run cancelled. Click <strong>▶ Run workflow</strong> to run again.');
|
|
1631
1743
|
} else {
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
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
|
+
}
|
|
1638
1756
|
}
|
|
1639
1757
|
} finally {
|
|
1640
1758
|
state.running = false;
|
|
@@ -2210,6 +2328,12 @@
|
|
|
2210
2328
|
} else if (m.type === 'trigger-session-changed') {
|
|
2211
2329
|
applyTriggerSnapshot(m.id, m.snapshot);
|
|
2212
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
|
+
}
|
|
2213
2337
|
// The device-code sign-in resolved. On success, refresh so the card flips
|
|
2214
2338
|
// to Connected; otherwise surface the outcome in the integration's slot.
|
|
2215
2339
|
connecting.delete(m.id);
|
|
@@ -2585,7 +2709,7 @@
|
|
|
2585
2709
|
<div class="int-icon">${escapeHtml(VENDOR_ICON[i.vendor] || (i.name[0] || '?'))}</div>
|
|
2586
2710
|
<div class="int-info">
|
|
2587
2711
|
<div class="int-name">${escapeHtml(i.name)}</div>
|
|
2588
|
-
<div class="int-kind">${escapeHtml(i.vendor || 'integration')} · agent <code>${escapeHtml(i.agent)}</code
|
|
2712
|
+
<div class="int-kind">${i.agent === 'first-party' ? 'Built-in integration' : `${escapeHtml(i.vendor || 'integration')} · agent <code>${escapeHtml(i.agent)}</code>`}</div>
|
|
2589
2713
|
<div class="int-scopes">${escapeHtml((i.network || []).join(' · ') || '—')}</div>
|
|
2590
2714
|
<div class="int-meta">${escapeHtml(meta)}</div>
|
|
2591
2715
|
<div class="int-row">
|
|
@@ -2636,6 +2760,10 @@
|
|
|
2636
2760
|
if (slot) slot.innerHTML = 'Starting sign-in…';
|
|
2637
2761
|
try {
|
|
2638
2762
|
const res = await api(`/api/connect/${encodeURIComponent(id)}`, { method: 'POST', body });
|
|
2763
|
+
// A managed-preset pre-flight (e.g. port 80 already in use) refuses to start
|
|
2764
|
+
// and broadcasts the reason over SSE (connect-result). Clear the guard so the
|
|
2765
|
+
// user can retry after fixing it, and don't claim the browser opened.
|
|
2766
|
+
if (res && res.started === false) { connecting.delete(id); return; }
|
|
2639
2767
|
if (slot) {
|
|
2640
2768
|
if (res.flow === 'device-code' && res.prompt) {
|
|
2641
2769
|
// Fallback flow: show the device code to enter at the verification URL.
|