@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.
- package/dist/floless-server.cjs +30 -4
- package/dist/web/app.css +36 -0
- package/dist/web/aware.js +131 -12
- package/package.json +1 -1
package/dist/floless-server.cjs
CHANGED
|
@@ -52308,7 +52308,7 @@ function appVersion() {
|
|
|
52308
52308
|
return resolveVersion({
|
|
52309
52309
|
isSea: isSea2(),
|
|
52310
52310
|
sqVersionXml: readSqVersionXml(),
|
|
52311
|
-
define: true ? "0.7.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
981
|
-
//
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
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
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
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);
|