@songsid/agend 2.1.12 → 2.2.0-alpha.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/README.md +2 -2
- package/README.zh-TW.md +2 -2
- package/dist/auth/one-time-code.d.ts +19 -0
- package/dist/auth/one-time-code.js +49 -0
- package/dist/auth/one-time-code.js.map +1 -0
- package/dist/auth-api.d.ts +52 -0
- package/dist/auth-api.js +346 -0
- package/dist/auth-api.js.map +1 -0
- package/dist/backend/claude-code.d.ts +4 -0
- package/dist/backend/claude-code.js +97 -0
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex.js +59 -30
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/kiro-agent.d.ts +66 -0
- package/dist/backend/kiro-agent.js +302 -0
- package/dist/backend/kiro-agent.js.map +1 -0
- package/dist/backend/kiro-identity.d.ts +80 -0
- package/dist/backend/kiro-identity.js +537 -0
- package/dist/backend/kiro-identity.js.map +1 -0
- package/dist/backend/kiro-v2-store-worker.d.ts +1 -0
- package/dist/backend/kiro-v2-store-worker.js +87 -0
- package/dist/backend/kiro-v2-store-worker.js.map +1 -0
- package/dist/backend/kiro-v2-store.d.ts +54 -0
- package/dist/backend/kiro-v2-store.js +131 -0
- package/dist/backend/kiro-v2-store.js.map +1 -0
- package/dist/backend/kiro-v3-identity.d.ts +5 -0
- package/dist/backend/kiro-v3-identity.js +22 -0
- package/dist/backend/kiro-v3-identity.js.map +1 -1
- package/dist/backend/kiro.d.ts +83 -7
- package/dist/backend/kiro.js +418 -91
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +45 -3
- package/dist/backend/types.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +1 -0
- package/dist/channel/adapters/discord.js +29 -10
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +1 -0
- package/dist/channel/adapters/telegram.js +13 -1
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/types.d.ts +9 -1
- package/dist/classic-channel-manager.d.ts +18 -10
- package/dist/classic-channel-manager.js +45 -18
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +225 -127
- package/dist/cli.js.map +1 -1
- package/dist/command-table.d.ts +5 -3
- package/dist/command-table.js +29 -27
- package/dist/command-table.js.map +1 -1
- package/dist/config-validator.js +46 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/config.js +12 -0
- package/dist/config.js.map +1 -1
- package/dist/connection-secrets.d.ts +6 -0
- package/dist/connection-secrets.js +4 -0
- package/dist/connection-secrets.js.map +1 -1
- package/dist/daemon.d.ts +89 -4
- package/dist/daemon.js +661 -77
- package/dist/daemon.js.map +1 -1
- package/dist/darwin-memory.d.ts +6 -1
- package/dist/darwin-memory.js +17 -6
- package/dist/darwin-memory.js.map +1 -1
- package/dist/delivery-consumed-watch.d.ts +85 -0
- package/dist/delivery-consumed-watch.js +244 -0
- package/dist/delivery-consumed-watch.js.map +1 -0
- package/dist/delivery-outbox.d.ts +61 -0
- package/dist/delivery-outbox.js +120 -2
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/delivery-reconciliation.d.ts +14 -5
- package/dist/delivery-reconciliation.js +27 -4
- package/dist/delivery-reconciliation.js.map +1 -1
- package/dist/delivery-reconciliation.test.js +35 -1
- package/dist/delivery-reconciliation.test.js.map +1 -1
- package/dist/event-loop-watch.d.ts +28 -0
- package/dist/event-loop-watch.js +75 -3
- package/dist/event-loop-watch.js.map +1 -1
- package/dist/fleet-context.d.ts +22 -2
- package/dist/fleet-level-config.d.ts +2 -2
- package/dist/fleet-level-config.js +6 -1
- package/dist/fleet-level-config.js.map +1 -1
- package/dist/fleet-manager.d.ts +293 -12
- package/dist/fleet-manager.js +2364 -449
- package/dist/fleet-manager.js.map +1 -1
- package/dist/host-memory.d.ts +4 -0
- package/dist/host-memory.js.map +1 -1
- package/dist/instance-config-impact.js +5 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +10 -5
- package/dist/instance-lifecycle.js +162 -53
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +150 -16
- package/dist/locale.js.map +1 -1
- package/dist/login-controller.d.ts +2 -0
- package/dist/login-controller.js +17 -5
- package/dist/login-controller.js.map +1 -1
- package/dist/memory-pressure.d.ts +7 -8
- package/dist/memory-pressure.js +76 -15
- package/dist/memory-pressure.js.map +1 -1
- package/dist/monotonic-budget.d.ts +2 -0
- package/dist/monotonic-budget.js +28 -0
- package/dist/monotonic-budget.js.map +1 -0
- package/dist/needs-you-hub.d.ts +114 -0
- package/dist/needs-you-hub.js +370 -0
- package/dist/needs-you-hub.js.map +1 -0
- package/dist/needs-you-live.d.ts +116 -0
- package/dist/needs-you-live.js +228 -0
- package/dist/needs-you-live.js.map +1 -0
- package/dist/needs-you.d.ts +132 -0
- package/dist/needs-you.js +164 -0
- package/dist/needs-you.js.map +1 -0
- package/dist/node-version-guard.d.ts +30 -0
- package/dist/node-version-guard.js +50 -0
- package/dist/node-version-guard.js.map +1 -0
- package/dist/operator-control-paths.d.ts +4 -0
- package/dist/operator-control-paths.js +24 -0
- package/dist/operator-control-paths.js.map +1 -0
- package/dist/outbound-handlers.d.ts +2 -0
- package/dist/outbound-handlers.js +9 -2
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +5 -5
- package/dist/profile-control.js +4 -20
- package/dist/profile-control.js.map +1 -1
- package/dist/public-web-gateway.d.ts +18 -0
- package/dist/public-web-gateway.js +141 -0
- package/dist/public-web-gateway.js.map +1 -0
- package/dist/public-web-link.d.ts +53 -0
- package/dist/public-web-link.js +175 -0
- package/dist/public-web-link.js.map +1 -0
- package/dist/quickstart-api.d.ts +8 -1
- package/dist/quickstart-api.js +86 -48
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.d.ts +1 -0
- package/dist/quickstart.js +6 -2
- package/dist/quickstart.js.map +1 -1
- package/dist/scheduler/db.d.ts +7 -1
- package/dist/scheduler/db.js +38 -0
- package/dist/scheduler/db.js.map +1 -1
- package/dist/scheduler/index.d.ts +1 -1
- package/dist/scheduler/retry-label.d.ts +11 -0
- package/dist/scheduler/retry-label.js +24 -0
- package/dist/scheduler/retry-label.js.map +1 -0
- package/dist/scheduler/scheduler.d.ts +55 -2
- package/dist/scheduler/scheduler.js +220 -8
- package/dist/scheduler/scheduler.js.map +1 -1
- package/dist/scheduler/types.d.ts +22 -0
- package/dist/scheduler/types.js.map +1 -1
- package/dist/secret-store.d.ts +5 -0
- package/dist/secret-store.js +20 -0
- package/dist/secret-store.js.map +1 -1
- package/dist/service-activation.d.ts +110 -0
- package/dist/service-activation.js +302 -0
- package/dist/service-activation.js.map +1 -0
- package/dist/service-installer.d.ts +7 -0
- package/dist/service-installer.js +7 -4
- package/dist/service-installer.js.map +1 -1
- package/dist/service-restart-selection.d.ts +6 -5
- package/dist/service-restart-selection.js +6 -5
- package/dist/service-restart-selection.js.map +1 -1
- package/dist/settings-api.d.ts +10 -2
- package/dist/settings-api.js +175 -72
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-baseline.d.ts +20 -0
- package/dist/settings-baseline.js +104 -0
- package/dist/settings-baseline.js.map +1 -0
- package/dist/settings-change.d.ts +21 -0
- package/dist/settings-change.js +133 -0
- package/dist/settings-change.js.map +1 -0
- package/dist/settings-confirmation.d.ts +105 -0
- package/dist/settings-confirmation.js +232 -0
- package/dist/settings-confirmation.js.map +1 -0
- package/dist/settings-control.d.ts +41 -0
- package/dist/settings-control.js +294 -0
- package/dist/settings-control.js.map +1 -0
- package/dist/settings-effect.d.ts +24 -0
- package/dist/settings-effect.js +169 -0
- package/dist/settings-effect.js.map +1 -0
- package/dist/settings-http-confirmation.d.ts +29 -0
- package/dist/settings-http-confirmation.js +123 -0
- package/dist/settings-http-confirmation.js.map +1 -0
- package/dist/settings-instance-patch.d.ts +4 -0
- package/dist/settings-instance-patch.js +26 -0
- package/dist/settings-instance-patch.js.map +1 -0
- package/dist/settings-request-capability.d.ts +16 -0
- package/dist/settings-request-capability.js +32 -0
- package/dist/settings-request-capability.js.map +1 -0
- package/dist/settings-transaction.d.ts +65 -0
- package/dist/settings-transaction.js +223 -0
- package/dist/settings-transaction.js.map +1 -0
- package/dist/setup-auth.d.ts +10 -18
- package/dist/setup-auth.js +14 -38
- package/dist/setup-auth.js.map +1 -1
- package/dist/setup-form.d.ts +1 -1
- package/dist/setup-form.js +75 -18
- package/dist/setup-form.js.map +1 -1
- package/dist/setup-guide.d.ts +13 -0
- package/dist/setup-guide.js +25 -0
- package/dist/setup-guide.js.map +1 -0
- package/dist/setup-host.d.ts +16 -0
- package/dist/setup-host.js +197 -32
- package/dist/setup-host.js.map +1 -1
- package/dist/setup-wizard.js +3 -0
- package/dist/setup-wizard.js.map +1 -1
- package/dist/slash-authz.d.ts +17 -3
- package/dist/slash-authz.js +5 -0
- package/dist/slash-authz.js.map +1 -1
- package/dist/spawn-gate.js +3 -3
- package/dist/spawn-gate.js.map +1 -1
- package/dist/statusline-watcher.d.ts +5 -0
- package/dist/statusline-watcher.js +2 -0
- package/dist/statusline-watcher.js.map +1 -1
- package/dist/steer-capability.d.ts +6 -0
- package/dist/steer-capability.js +8 -0
- package/dist/steer-capability.js.map +1 -1
- package/dist/sync-work-attribution.d.ts +14 -0
- package/dist/sync-work-attribution.js +43 -8
- package/dist/sync-work-attribution.js.map +1 -1
- package/dist/tips.js +10 -5
- package/dist/tips.js.map +1 -1
- package/dist/tmux-control.d.ts +35 -5
- package/dist/tmux-control.js +291 -57
- package/dist/tmux-control.js.map +1 -1
- package/dist/tmux-manager.d.ts +20 -6
- package/dist/tmux-manager.js +90 -47
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tmux-read.d.ts +59 -0
- package/dist/tmux-read.js +213 -0
- package/dist/tmux-read.js.map +1 -0
- package/dist/topic-commands.d.ts +21 -10
- package/dist/topic-commands.js +191 -58
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-delta-reader.js +2 -1
- package/dist/transcript-delta-reader.js.map +1 -1
- package/dist/tunnel/cloudflared-install.d.ts +2 -0
- package/dist/tunnel/cloudflared-install.js +1 -1
- package/dist/tunnel/cloudflared-install.js.map +1 -1
- package/dist/tunnel/purpose-lane.d.ts +14 -0
- package/dist/tunnel/purpose-lane.js +43 -0
- package/dist/tunnel/purpose-lane.js.map +1 -0
- package/dist/turn-reply-guard.d.ts +2 -0
- package/dist/turn-reply-guard.js +10 -1
- package/dist/turn-reply-guard.js.map +1 -1
- package/dist/types.d.ts +33 -0
- package/dist/ui/app.html +18 -0
- package/dist/ui/chat-render.js +478 -0
- package/dist/ui/chat-store.js +289 -0
- package/dist/ui/chat-strings.js +69 -0
- package/dist/ui/chat-thread.js +209 -0
- package/dist/ui/chat-tour.js +116 -0
- package/dist/ui/fleet-org.js +142 -0
- package/dist/ui/panel-chat.js +418 -0
- package/dist/ui/panel-fleet.js +413 -0
- package/dist/ui/panel-needs.js +202 -0
- package/dist/ui/panel-settings.js +750 -0
- package/dist/ui/preview.js +266 -0
- package/dist/ui/settings-apply.js +239 -0
- package/dist/ui/settings-confirm.js +186 -0
- package/dist/ui/settings-dialogs.js +546 -0
- package/dist/ui/settings-model.js +277 -0
- package/dist/ui/settings-strings.js +652 -0
- package/dist/ui/settings-wizard.js +140 -0
- package/dist/ui/shared/agend-auth.js +93 -0
- package/dist/ui/shared/app-ctx.js +80 -0
- package/dist/ui/shared/app-html.js +9 -0
- package/dist/ui/shared/app-i18n.js +95 -0
- package/dist/ui/shared/app-nav.js +73 -0
- package/dist/ui/shared/app-needs.js +114 -0
- package/dist/ui/shared/app-route.js +73 -0
- package/dist/ui/shared/app-session.js +66 -0
- package/dist/ui/shared/app-shell.js +344 -0
- package/dist/ui/shared/app-store.js +51 -0
- package/dist/ui/shared/app-stream.js +201 -0
- package/dist/ui/shared/app.css +676 -0
- package/dist/ui/shared/app.js +88 -0
- package/dist/ui/shared/fonts/OFL.txt +92 -0
- package/dist/ui/shared/fonts/README.md +21 -0
- package/dist/ui/shared/fonts/inter.woff2 +0 -0
- package/dist/ui/shared/htm.module.js +1 -0
- package/dist/ui/shared/panel-view.js +655 -0
- package/dist/ui/shared/preact-hooks.module.js +2 -0
- package/dist/ui/shared/preact.module.js +2 -0
- package/dist/ui/shared/signin.js +130 -0
- package/dist/ui/shared/theme.js +29 -0
- package/dist/ui/shared/tokens.css +153 -0
- package/dist/ui/shared/ui-confirm.js +43 -0
- package/dist/ui/shared/ui-dialog.js +45 -0
- package/dist/ui/shared/ui-icons.js +61 -0
- package/dist/ui/shared/ui-menu.js +39 -0
- package/dist/ui/shared/ui-states.js +20 -0
- package/dist/ui/shared/ui-toast.js +18 -0
- package/dist/ui/shared/vendor/htm.LICENSE +202 -0
- package/dist/ui/shared/vendor/preact.LICENSE +21 -0
- package/dist/ui/shared/vendor/vendor.json +39 -0
- package/dist/ui/shared/view-strings.js +309 -0
- package/dist/ui/signin.html +51 -0
- package/dist/update-check.d.ts +6 -2
- package/dist/update-check.js +12 -3
- package/dist/update-check.js.map +1 -1
- package/dist/update-install.d.ts +71 -0
- package/dist/update-install.js +162 -0
- package/dist/update-install.js.map +1 -0
- package/dist/upgrade-notices.d.ts +17 -0
- package/dist/upgrade-notices.js +93 -0
- package/dist/upgrade-notices.js.map +1 -0
- package/dist/usage/statusline-usage.d.ts +2 -0
- package/dist/usage/statusline-usage.js +14 -10
- package/dist/usage/statusline-usage.js.map +1 -1
- package/dist/view-api.d.ts +6 -1
- package/dist/view-api.js +62 -51
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +71 -14
- package/dist/web-api.js +492 -77
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.d.ts +71 -27
- package/dist/web-auth.js +169 -69
- package/dist/web-auth.js.map +1 -1
- package/dist/web-body.d.ts +3 -0
- package/dist/web-body.js +55 -0
- package/dist/web-body.js.map +1 -0
- package/dist/web-channel-echo.d.ts +9 -0
- package/dist/web-channel-echo.js +55 -0
- package/dist/web-channel-echo.js.map +1 -0
- package/dist/web-chat-history.d.ts +125 -0
- package/dist/web-chat-history.js +184 -0
- package/dist/web-chat-history.js.map +1 -0
- package/dist/web-continuation.d.ts +7 -0
- package/dist/web-continuation.js +22 -0
- package/dist/web-continuation.js.map +1 -0
- package/dist/web-host-guard.d.ts +34 -0
- package/dist/web-host-guard.js +57 -1
- package/dist/web-host-guard.js.map +1 -1
- package/dist/web-login.d.ts +72 -0
- package/dist/web-login.js +132 -0
- package/dist/web-login.js.map +1 -0
- package/dist/web-org.d.ts +55 -0
- package/dist/web-org.js +67 -0
- package/dist/web-org.js.map +1 -0
- package/dist/web-preview.d.ts +77 -0
- package/dist/web-preview.js +240 -0
- package/dist/web-preview.js.map +1 -0
- package/dist/web-request-context.d.ts +10 -0
- package/dist/web-request-context.js +13 -0
- package/dist/web-request-context.js.map +1 -0
- package/dist/web-session.d.ts +167 -0
- package/dist/web-session.js +389 -0
- package/dist/web-session.js.map +1 -0
- package/dist/web-shell-routes.d.ts +59 -0
- package/dist/web-shell-routes.js +96 -0
- package/dist/web-shell-routes.js.map +1 -0
- package/dist/web-upload.d.ts +183 -0
- package/dist/web-upload.js +468 -0
- package/dist/web-upload.js.map +1 -0
- package/dist/website-tips-renderer.d.ts +2 -2
- package/dist/website-tips-renderer.js +3 -3
- package/dist/website-tips-renderer.js.map +1 -1
- package/package.json +8 -5
- package/scripts/preinstall-guard.cjs +36 -0
- package/dist/ui/dashboard.html +0 -774
- package/dist/ui/settings.html +0 -2165
- package/dist/ui/view.html +0 -1139
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* #1306: running an agent's HTML in the web chat — the parent side. Design: docs/design/1306-inline-html-preview.md.
|
|
3
|
+
*
|
|
4
|
+
* The HTML runs in a frame from a SEPARATE listener (the preview origin), sandboxed to an opaque origin, so it cannot
|
|
5
|
+
* act as the signed-in person: it has no dashboard origin, no cookie it can use, no CSRF value. It reaches the frame
|
|
6
|
+
* only by postMessage, after the frame proved it is this fleet's listener (the boot id). What it is NOT promised: that
|
|
7
|
+
* it cannot send data out — so previews are off on every device until that device opts in, and every preview carries
|
|
8
|
+
* the banner saying so.
|
|
9
|
+
*
|
|
10
|
+
* This file knows nothing about the chat, the composer, the fleet's API or navigation: the message listener below can
|
|
11
|
+
* reach the frames it made and nothing else (a test asserts it). Imported by the chat panel (#1408: the import sets globalThis.AgendPreview); also
|
|
12
|
+
* exports itself for the tests.
|
|
13
|
+
*/
|
|
14
|
+
(function (root, factory) {
|
|
15
|
+
"use strict";
|
|
16
|
+
var api = factory(root);
|
|
17
|
+
if (typeof module === "object" && module && module.exports) module.exports = api;
|
|
18
|
+
// The global as well: the chat imports this file for its side effect (#1408), in the browser and under the tests.
|
|
19
|
+
if (root) root.AgendPreview = api;
|
|
20
|
+
})(typeof globalThis !== "undefined" ? globalThis : this, function (root) {
|
|
21
|
+
"use strict";
|
|
22
|
+
|
|
23
|
+
var LIMITS = { maxBytes: 1024 * 1024, minHeight: 40, maxHeight: 4000, readyMs: 3000, watchdogMs: 10000, resizePerSec: 10, minDelta: 2, growthFreeze: 5, growthWindowMs: 2000 };
|
|
24
|
+
var OPT_IN_KEY = "agend_html_preview"; // localStorage: "on" — this device allows previews
|
|
25
|
+
var NEVER_KEY = "agend_html_preview_never"; // sessionStorage: "1" — never preview on this device (this session)
|
|
26
|
+
var BANNER = "Previews run the agent's HTML in an isolated frame. It cannot use your login, but it may be able to send data out. Only preview content you trust. A preview can slow or freeze this tab.";
|
|
27
|
+
|
|
28
|
+
var cfg = { dashboardOrigin: "", previewOrigin: "", boot: "", reason: "" };
|
|
29
|
+
var changed = []; // the page's "this device's choice changed" callbacks (checkbox, cards)
|
|
30
|
+
var live = new Map(); // cardKey → { key, iframe, ch, holder, state, … } (at most one: one running preview per page)
|
|
31
|
+
var listening = false;
|
|
32
|
+
|
|
33
|
+
function now() { return root.performance && typeof root.performance.now === "function" ? root.performance.now() : Date.now(); }
|
|
34
|
+
function store(kind) { try { return root[kind] || null; } catch (e) { return null; } }
|
|
35
|
+
function utf8Bytes(s) { try { return new root.TextEncoder().encode(s).length; } catch (e) { return String(s).length * 3; } }
|
|
36
|
+
function randomCh() {
|
|
37
|
+
var b = new Uint8Array(16);
|
|
38
|
+
root.crypto.getRandomValues(b);
|
|
39
|
+
var s = "";
|
|
40
|
+
for (var i = 0; i < b.length; i++) s += (b[i] < 16 ? "0" : "") + b[i].toString(16);
|
|
41
|
+
return s;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** What the server said about this page load (the <body data-*> attributes /ui was served with). */
|
|
45
|
+
function init(data) {
|
|
46
|
+
cfg = {
|
|
47
|
+
dashboardOrigin: String((data && data.dashboardOrigin) || ""),
|
|
48
|
+
previewOrigin: String((data && data.previewOrigin) || ""),
|
|
49
|
+
boot: String((data && data.previewBoot) || ""),
|
|
50
|
+
reason: String((data && data.previewReason) || ""),
|
|
51
|
+
};
|
|
52
|
+
if (!listening && typeof root.addEventListener === "function") {
|
|
53
|
+
root.addEventListener("message", onMessage);
|
|
54
|
+
// The opt-in is per device: another tab of this dashboard turning it off (or clearing storage) stops the
|
|
55
|
+
// previews running here at once. Turning it on elsewhere starts nothing here — only a click on Preview does.
|
|
56
|
+
root.addEventListener("storage", onStorage);
|
|
57
|
+
listening = true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function onStorage(event) {
|
|
61
|
+
if (event && event.key !== OPT_IN_KEY && event.key !== null) return; // key null: storage was cleared
|
|
62
|
+
if (!optedIn()) stopAll("off");
|
|
63
|
+
notify();
|
|
64
|
+
}
|
|
65
|
+
/** Call `fn` whenever this device's choice may have changed (here or in another tab). Returns the unsubscribe: a
|
|
66
|
+
* chat that leaves takes its callback with it (#1408 §4), so 50 visits leave no callback behind. */
|
|
67
|
+
function onChange(fn) {
|
|
68
|
+
if (typeof fn !== "function") return function () {};
|
|
69
|
+
changed.push(fn);
|
|
70
|
+
return function () { var i = changed.indexOf(fn); if (i >= 0) changed.splice(i, 1); };
|
|
71
|
+
}
|
|
72
|
+
function listeners() { return changed.length; }
|
|
73
|
+
function notify() { for (var i = 0; i < changed.length; i++) { try { changed[i](); } catch (e) { /* one listener's error stops no other */ } } }
|
|
74
|
+
|
|
75
|
+
// ── This device's choice ────────────────────────────────
|
|
76
|
+
|
|
77
|
+
function optedIn() { var s = store("localStorage"); try { return !!s && s.getItem(OPT_IN_KEY) === "on"; } catch (e) { return false; } }
|
|
78
|
+
/** Allow (or stop allowing) previews on this device. Turning it off stops every running preview at once. */
|
|
79
|
+
function setOptIn(on) {
|
|
80
|
+
var s = store("localStorage");
|
|
81
|
+
try { if (s) { if (on) s.setItem(OPT_IN_KEY, "on"); else s.removeItem(OPT_IN_KEY); } } catch (e) { /* this page only */ }
|
|
82
|
+
if (!on) stopAll("off");
|
|
83
|
+
notify();
|
|
84
|
+
}
|
|
85
|
+
function never() { var s = store("sessionStorage"); try { return !!s && s.getItem(NEVER_KEY) === "1"; } catch (e) { return false; } }
|
|
86
|
+
function setNever(on) {
|
|
87
|
+
var s = store("sessionStorage");
|
|
88
|
+
try { if (s) { if (on) s.setItem(NEVER_KEY, "1"); else s.removeItem(NEVER_KEY); } } catch (e) { /* this page only */ }
|
|
89
|
+
if (on) stopAll("off");
|
|
90
|
+
notify();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Whether this page may run a preview at all, and why not: the server must have chosen a preview origin for this
|
|
95
|
+
* load, the browser must really be at the origin the server believed (a proxy that rewrites Host would otherwise get
|
|
96
|
+
* a frame from the wrong place), and the device must have opted in and not switched previews off.
|
|
97
|
+
*/
|
|
98
|
+
function availability() {
|
|
99
|
+
if (!cfg.previewOrigin || !cfg.boot) return { ok: false, why: "server", reason: cfg.reason || "Previews are not available here." };
|
|
100
|
+
var at = root.location && root.location.origin;
|
|
101
|
+
if (!cfg.dashboardOrigin || at !== cfg.dashboardOrigin) {
|
|
102
|
+
return { ok: false, why: "origin", reason: "Previews are off: this page is at " + at + ", but the fleet believes it is at " + (cfg.dashboardOrigin || "an unknown address") + " (a proxy that rewrites Host). Pass the external Host through, or set web.preview_origin." };
|
|
103
|
+
}
|
|
104
|
+
if (never()) return { ok: false, why: "never", reason: "Previews are switched off on this device for this session." };
|
|
105
|
+
if (!optedIn()) return { ok: false, why: "optin", reason: "Previews are off on this device. Allow them from the card's menu or the sidebar." };
|
|
106
|
+
return { ok: true, why: "", reason: "" };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── The frame ───────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The ONE place the dashboard makes an iframe or writes `sandbox` (a test fails if anything else does):
|
|
113
|
+
* opaque origin (allow-scripts only — never allow-same-origin, forms, popups, top navigation, modals, downloads),
|
|
114
|
+
* no permissions, no referrer, from <preview origin>/frame — the only URL the dashboard's frame-src allows.
|
|
115
|
+
*/
|
|
116
|
+
function mountPreview(doc) {
|
|
117
|
+
var f = doc.createElement("iframe");
|
|
118
|
+
f.setAttribute("sandbox", "allow-scripts");
|
|
119
|
+
f.setAttribute("allow", "");
|
|
120
|
+
f.setAttribute("referrerpolicy", "no-referrer");
|
|
121
|
+
f.setAttribute("loading", "eager");
|
|
122
|
+
f.setAttribute("title", "Untrusted HTML preview");
|
|
123
|
+
f.className = "preview-frame";
|
|
124
|
+
f.src = cfg.previewOrigin + "/frame";
|
|
125
|
+
return f;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Run `html` in `holder` (a node of the card) for `key`. `ui` is told what happened: ui.state(name, reason) with
|
|
130
|
+
* "starting" | "running" | "stopped" | "unavailable". One preview at a time: any other running one stops first.
|
|
131
|
+
*/
|
|
132
|
+
function start(key, holder, html, ui) {
|
|
133
|
+
var a = availability();
|
|
134
|
+
if (!a.ok) { ui.state("unavailable", a.reason); return false; }
|
|
135
|
+
if (typeof html !== "string" || utf8Bytes(html) > LIMITS.maxBytes) { ui.state("unavailable", "This HTML is over 1 MiB; it is not previewed."); return false; }
|
|
136
|
+
stopAll("another");
|
|
137
|
+
var doc = holder.ownerDocument;
|
|
138
|
+
var iframe = mountPreview(doc);
|
|
139
|
+
var card = {
|
|
140
|
+
key: key, iframe: iframe, ch: randomCh(), holder: holder, html: html, ui: ui, rendered: false,
|
|
141
|
+
height: LIMITS.minHeight, lastApplied: -Infinity, appliedInSecond: [], frozen: false, growth: [], pendingFrame: false,
|
|
142
|
+
readyTimer: null, watchdogTimer: null,
|
|
143
|
+
};
|
|
144
|
+
live.set(key, card);
|
|
145
|
+
card.readyTimer = root.setTimeout(function () {
|
|
146
|
+
if (live.get(key) === card && !card.rendered) stop(key, "unavailable", "Preview unavailable: the preview did not answer within 3 s. If an earlier preview froze, it may still be running — open the dashboard in a new tab. Over SSH, forward the preview port too.");
|
|
147
|
+
}, LIMITS.readyMs);
|
|
148
|
+
holder.appendChild(iframe);
|
|
149
|
+
setHeight(card, LIMITS.minHeight);
|
|
150
|
+
ui.state("starting", "");
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function setHeight(card, px) { card.height = px; card.iframe.style.height = px + "px"; } // CSSOM — never a style attribute
|
|
155
|
+
|
|
156
|
+
function armWatchdog(card) {
|
|
157
|
+
if (card.watchdogTimer) root.clearTimeout(card.watchdogTimer);
|
|
158
|
+
card.watchdogTimer = root.setTimeout(function () {
|
|
159
|
+
if (live.get(card.key) !== card) return;
|
|
160
|
+
// Best effort: a frame that froze this tab freezes this timer too. Only while the page is visible.
|
|
161
|
+
var hidden = root.document && root.document.visibilityState === "hidden";
|
|
162
|
+
if (hidden) { armWatchdog(card); return; }
|
|
163
|
+
stop(card.key, "stopped", "The preview stopped answering, so it was closed.");
|
|
164
|
+
}, LIMITS.watchdogMs);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Stop the preview for `key`: remove its frame and its timers. A preview is never moved or reloaded silently. */
|
|
168
|
+
function stop(key, state, reason) {
|
|
169
|
+
var card = live.get(key);
|
|
170
|
+
if (!card) return false;
|
|
171
|
+
live.delete(key);
|
|
172
|
+
if (card.readyTimer) root.clearTimeout(card.readyTimer);
|
|
173
|
+
if (card.watchdogTimer) root.clearTimeout(card.watchdogTimer);
|
|
174
|
+
if (card.iframe.parentNode) card.iframe.parentNode.removeChild(card.iframe);
|
|
175
|
+
card.ui.state(state || "stopped", reason || "");
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
function stopAll(why) {
|
|
179
|
+
var keys = [];
|
|
180
|
+
live.forEach(function (_c, k) { keys.push(k); });
|
|
181
|
+
for (var i = 0; i < keys.length; i++) stop(keys[i], "stopped", why === "off" ? "Previews were switched off on this device." : why === "another" ? "Another preview started." : "");
|
|
182
|
+
return keys;
|
|
183
|
+
}
|
|
184
|
+
/** Stop every preview inside `node` (before the chat replaces, moves or removes it); returns the stopped keys. */
|
|
185
|
+
function stopIn(node) {
|
|
186
|
+
var keys = [];
|
|
187
|
+
live.forEach(function (c, k) { if (node && (node === c.holder || (typeof node.contains === "function" && node.contains(c.holder)))) keys.push(k); });
|
|
188
|
+
for (var i = 0; i < keys.length; i++) stop(keys[i], "stopped", "");
|
|
189
|
+
return keys;
|
|
190
|
+
}
|
|
191
|
+
function running(key) { return live.has(key); }
|
|
192
|
+
function liveCount() { return live.size; }
|
|
193
|
+
function liveFrame(key) { var c = live.get(key); return c ? c.iframe : null; }
|
|
194
|
+
|
|
195
|
+
// ── Messages from frames (design §4.2) ──────────────────
|
|
196
|
+
|
|
197
|
+
var KEYS = { ready: "boot,ch,type,v", resize: "ch,height,type,v", heartbeat: "ch,type,v" };
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* The only window message listener. A message counts only from one of OUR frames (event.source is that frame's
|
|
201
|
+
* window, and its origin is the opaque "null"), with one of three exact shapes; each has one effect, on that frame
|
|
202
|
+
* only: ready → send the HTML once (boot checked, opt-in checked again), resize → its height, heartbeat → its
|
|
203
|
+
* watchdog. It has no reference to the chat, the composer, the fleet's API, navigation or storage.
|
|
204
|
+
*/
|
|
205
|
+
function onMessage(event) {
|
|
206
|
+
var card = null;
|
|
207
|
+
live.forEach(function (c) { if (event.source && event.source === c.iframe.contentWindow) card = c; });
|
|
208
|
+
if (!card || event.origin !== "null") return;
|
|
209
|
+
var d = event.data;
|
|
210
|
+
if (!d || typeof d !== "object" || Array.isArray(d) || d.v !== 1 || typeof d.type !== "string" || !Object.prototype.hasOwnProperty.call(KEYS, d.type)) return;
|
|
211
|
+
if (Object.keys(d).sort().join(",") !== KEYS[d.type]) return;
|
|
212
|
+
if (d.type === "ready") {
|
|
213
|
+
if (d.ch !== null || typeof d.boot !== "string" || card.rendered) return;
|
|
214
|
+
if (d.boot !== cfg.boot) { stop(card.key, "unavailable", "Preview unavailable: something else answered on the preview port."); return; }
|
|
215
|
+
if (!availability().ok) { stop(card.key, "unavailable", availability().reason); return; }
|
|
216
|
+
card.rendered = true;
|
|
217
|
+
root.clearTimeout(card.readyTimer); card.readyTimer = null;
|
|
218
|
+
card.iframe.contentWindow.postMessage({ v: 1, type: "render", ch: card.ch, html: card.html }, "*");
|
|
219
|
+
armWatchdog(card);
|
|
220
|
+
card.ui.state("running", "");
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (d.ch !== card.ch || !card.rendered) return;
|
|
224
|
+
if (d.type === "heartbeat") {
|
|
225
|
+
// A heartbeat never outlives the permission: if this device stopped allowing previews (and the storage event
|
|
226
|
+
// was missed), the next heartbeat ends it.
|
|
227
|
+
if (!availability().ok) { stop(card.key, "stopped", "Previews were switched off on this device."); return; }
|
|
228
|
+
armWatchdog(card);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (typeof d.height !== "number" || !isFinite(d.height) || Math.floor(d.height) !== d.height) return;
|
|
232
|
+
requestHeight(card, d.height);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Height rules (design §4.2): clamp to [40, 4000]; ≤ 1 per animation frame and ≤ 10 per second; Δ < 2 px ignored;
|
|
236
|
+
* after 5 consecutive increases within 2 s the height freezes and the frame scrolls. */
|
|
237
|
+
function requestHeight(card, h) {
|
|
238
|
+
var want = Math.max(LIMITS.minHeight, Math.min(LIMITS.maxHeight, h));
|
|
239
|
+
if (Math.abs(want - card.height) < LIMITS.minDelta) return;
|
|
240
|
+
var t = now();
|
|
241
|
+
if (want > card.height) {
|
|
242
|
+
card.growth = card.growth.filter(function (x) { return t - x < LIMITS.growthWindowMs; });
|
|
243
|
+
card.growth.push(t);
|
|
244
|
+
if (card.growth.length > LIMITS.growthFreeze) card.frozen = true;
|
|
245
|
+
} else card.growth = [];
|
|
246
|
+
if (card.frozen) return;
|
|
247
|
+
card.appliedInSecond = card.appliedInSecond.filter(function (x) { return t - x < 1000; });
|
|
248
|
+
if (card.appliedInSecond.length >= LIMITS.resizePerSec) return;
|
|
249
|
+
card.wanted = want;
|
|
250
|
+
if (card.pendingFrame) return;
|
|
251
|
+
card.pendingFrame = true;
|
|
252
|
+
var raf = typeof root.requestAnimationFrame === "function" ? root.requestAnimationFrame : function (f) { return root.setTimeout(f, 16); };
|
|
253
|
+
raf(function () {
|
|
254
|
+
card.pendingFrame = false;
|
|
255
|
+
if (live.get(card.key) !== card) return;
|
|
256
|
+
card.appliedInSecond.push(now());
|
|
257
|
+
setHeight(card, card.wanted);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
init: init, availability: availability, optedIn: optedIn, setOptIn: setOptIn, never: never, setNever: setNever, onChange: onChange, listeners: listeners, onStorage: onStorage,
|
|
263
|
+
start: start, stop: stop, stopAll: stopAll, stopIn: stopIn, running: running, liveCount: liveCount, liveFrame: liveFrame,
|
|
264
|
+
mountPreview: mountPreview, onMessage: onMessage, BANNER: BANNER, LIMITS: LIMITS,
|
|
265
|
+
};
|
|
266
|
+
});
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// #1408 §5: Settings' Apply belongs to the app from the moment it is confirmed. The panel hands over an operation —
|
|
2
|
+
// the staged changes as data (method, URL, body, label; never a DOM closure) — before the first write, and this
|
|
3
|
+
// runner performs the writes one by one, the POST /api/settings/apply (one key, made now, reused by every retry), and
|
|
4
|
+
// the one watcher of its job. It publishes progress to the app store (appStore.settingsOp) and never touches a panel's
|
|
5
|
+
// DOM: Settings renders from the store and re-attaches when it mounts again, the shell shows a line in any other panel.
|
|
6
|
+
//
|
|
7
|
+
// A write an admin must confirm (#1423) waits here, in the operation, until it is decided (settings-confirm.js).
|
|
8
|
+
// Secrets (a bot token being rotated) live only inside the operation and are dropped as their write finishes, or when
|
|
9
|
+
// the operation stops.
|
|
10
|
+
import { appStore } from "/assets/app-store.js";
|
|
11
|
+
import { t } from "/assets/app-i18n.js";
|
|
12
|
+
import { toast } from "/assets/ui-toast.js";
|
|
13
|
+
import "./settings-strings.js";
|
|
14
|
+
import { api, confirmedWrite, newKey } from "./settings-confirm.js";
|
|
15
|
+
|
|
16
|
+
const WATCH_LIMIT_MS = 10 * 60_000;
|
|
17
|
+
const POST_RETRIES = [1000, 2000, 4000];
|
|
18
|
+
const now = () => (typeof performance !== "undefined" ? performance.now() : Date.now());
|
|
19
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
20
|
+
const ACTIVE = new Set(["writing", "posting", "watching"]);
|
|
21
|
+
|
|
22
|
+
let op = null; // the operation in hand (the store holds its public view)
|
|
23
|
+
let seq = 0;
|
|
24
|
+
|
|
25
|
+
function view() {
|
|
26
|
+
if (!op) return null;
|
|
27
|
+
return {
|
|
28
|
+
id: op.id, phase: op.phase, error: op.error, job: op.job, lostJob: op.lostJob, restart: op.restart,
|
|
29
|
+
steps: op.steps.map(({ label, impact, status, error, pendingId }) => ({ label, impact, status, error: error || null, pendingId: pendingId || null })),
|
|
30
|
+
leftover: op.leftover,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function publish() { appStore.set({ settingsOp: view() }); }
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* An operation is in hand and not finished — its writes, its apply job, or a restart of AgEnD it asked for (from the
|
|
37
|
+
* press until that restart's watch ends, #1453 review): Apply stays off, and no other operation can take its place.
|
|
38
|
+
*/
|
|
39
|
+
export function operationActive() { return !!op && (ACTIVE.has(op.phase) || op.restart === "busy"); }
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Take over an Apply. `changes`: [{ label, impact, request: { method, url, body, sensitive? }, key? } | { label, impact,
|
|
43
|
+
* connectionSecret: { id, secret } }], in order. Returns false (and does nothing) while another operation is active.
|
|
44
|
+
* A change that brings its `key` is the retry of a write whose outcome was never learned: the same key rejoins the
|
|
45
|
+
* request the server may already hold. A `sensitive` request body (the wizard's token) is dropped when its write
|
|
46
|
+
* completes (answered, refused or failed), and such a change is never staged again.
|
|
47
|
+
*/
|
|
48
|
+
export function startOperation(changes) {
|
|
49
|
+
if (operationActive()) return false;
|
|
50
|
+
op = {
|
|
51
|
+
id: ++seq,
|
|
52
|
+
phase: "writing", error: null, job: null, lostJob: false, restart: "idle", leftover: [],
|
|
53
|
+
applyKey: newKey("apply"), // made now, before the first write; every POST retry reuses it
|
|
54
|
+
steps: changes.map((c) => ({ ...c, request: c.request ? { ...c.request } : undefined, key: c.key || newKey("write"), status: "queued" })),
|
|
55
|
+
};
|
|
56
|
+
publish();
|
|
57
|
+
run(op);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Forget a finished operation (its card's ×). */
|
|
62
|
+
export function dismissOperation() {
|
|
63
|
+
if (op && !ACTIVE.has(op.phase) && op.restart !== "busy") { op = null; publish(); }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Unrun changes of a failed operation, as data, for the panel to stage again (secrets excluded); taken once. */
|
|
67
|
+
export function takeLeftover() {
|
|
68
|
+
if (!op || !op.leftover.length) return [];
|
|
69
|
+
const out = op.leftover;
|
|
70
|
+
op.leftover = [];
|
|
71
|
+
publish();
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const mine = (o) => o === op;
|
|
76
|
+
|
|
77
|
+
async function run(o) {
|
|
78
|
+
for (const step of o.steps) {
|
|
79
|
+
if (!mine(o)) return;
|
|
80
|
+
step.status = "running"; publish();
|
|
81
|
+
let res;
|
|
82
|
+
// A transport failure leaves the outcome unknown: the server may have taken the write (or made it a pending
|
|
83
|
+
// request). That is not a refusal — the step keeps its key, so trying again rejoins it (#1453 review).
|
|
84
|
+
try { res = await perform(step, o); }
|
|
85
|
+
catch (err) { res = { ok: false, ambiguous: true, body: { error: err && err.message ? err.message : "network" } }; }
|
|
86
|
+
finally {
|
|
87
|
+
if (step.connectionSecret) step.connectionSecret.secret = "";
|
|
88
|
+
if (step.request && step.request.sensitive) step.request.body = null;
|
|
89
|
+
}
|
|
90
|
+
if (!mine(o)) return;
|
|
91
|
+
step.pendingId = null;
|
|
92
|
+
// The wizard's commit says when ~/.agend/.env is readable by others.
|
|
93
|
+
if (res && res.ok && res.body && res.body.secret_mode_ok === false) toast(t("settings.wizardEnvPerms"), false);
|
|
94
|
+
if (!res || !res.ok) {
|
|
95
|
+
step.status = "failed";
|
|
96
|
+
step.ambiguous = !!(res && res.ambiguous);
|
|
97
|
+
step.error = (res && res.body && (res.body.error || res.body.result)) || "failed";
|
|
98
|
+
stop(o, step);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
step.status = "done"; publish();
|
|
102
|
+
}
|
|
103
|
+
if (!mine(o)) return;
|
|
104
|
+
o.phase = "posting"; publish();
|
|
105
|
+
const started = await postApply(o);
|
|
106
|
+
if (!mine(o)) return;
|
|
107
|
+
if (!started) return;
|
|
108
|
+
o.job = started; o.phase = "watching"; publish();
|
|
109
|
+
await watch(o, started.id);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The step's write; a connection's token rotation is verify → apply → its own short job. */
|
|
113
|
+
async function perform(step, o) {
|
|
114
|
+
if (step.request) {
|
|
115
|
+
const { method, url, body } = step.request;
|
|
116
|
+
return confirmedWrite(url, { method, body, key: step.key, label: step.label,
|
|
117
|
+
onPending: (id) => { if (mine(o)) { step.pendingId = id; step.status = "waiting"; publish(); } } });
|
|
118
|
+
}
|
|
119
|
+
if (step.connectionSecret) return rotateConnectionSecret(step, o);
|
|
120
|
+
return { ok: false, body: { error: "unknown change" } };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function rotateConnectionSecret(step, o) {
|
|
124
|
+
const { id } = step.connectionSecret;
|
|
125
|
+
const base = `/api/settings/connections/${encodeURIComponent(id)}/secret`;
|
|
126
|
+
const verified = await api(`${base}/verify`, { method: "POST", headers: { "Idempotency-Key": step.key },
|
|
127
|
+
body: JSON.stringify({ secret: step.connectionSecret.secret, idempotency_key: step.key }) });
|
|
128
|
+
step.connectionSecret.secret = ""; // the server holds it from here (by verification id)
|
|
129
|
+
if (!verified.ok) return verified;
|
|
130
|
+
const applied = await confirmedWrite(`${base}/apply`, { method: "POST", key: step.key, label: step.label,
|
|
131
|
+
body: { verification_id: verified.body && verified.body.verification_id, idempotency_key: step.key },
|
|
132
|
+
onPending: (pid) => { if (mine(o)) { step.pendingId = pid; step.status = "waiting"; publish(); } } });
|
|
133
|
+
if (!applied.ok && applied.status !== 409) return applied;
|
|
134
|
+
let result = applied.body || {};
|
|
135
|
+
for (let n = 0; result.job_id && result.result === "applying" && n < 60; n++) {
|
|
136
|
+
await sleep(250);
|
|
137
|
+
const status = await api(`${base}/apply/${encodeURIComponent(result.job_id)}`).catch(() => null);
|
|
138
|
+
if (status && status.ok) result = status.body || result;
|
|
139
|
+
}
|
|
140
|
+
const ok = result.result === "applied" || result.result === "restart_required";
|
|
141
|
+
return { ok, status: ok ? 200 : 409, body: ok ? result : { error: result.error || result.result || "apply failed" } };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function stop(o, failed) {
|
|
145
|
+
o.phase = "failed";
|
|
146
|
+
o.error = failed ? failed.error : o.error;
|
|
147
|
+
let after = false;
|
|
148
|
+
for (const step of o.steps) {
|
|
149
|
+
if (step === failed) after = true;
|
|
150
|
+
else if (after && step.status === "queued") step.status = "skipped";
|
|
151
|
+
if (step.connectionSecret) step.connectionSecret.secret = "";
|
|
152
|
+
}
|
|
153
|
+
// What did not land can be staged again, except a secret, which has to be entered again. A write whose outcome is
|
|
154
|
+
// unknown goes back with its key; one the server refused, or that never ran, gets a new key when it is applied.
|
|
155
|
+
o.leftover = o.steps.filter((s) => (s.status === "failed" || s.status === "skipped") && s.request && !s.request.sensitive)
|
|
156
|
+
.map(({ label, impact, request, stageKey, key, ambiguous }) => ({ label, impact, request, stageKey, ...(ambiguous ? { key } : {}) }));
|
|
157
|
+
publish();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function postApply(o) {
|
|
161
|
+
for (let attempt = 0; ; attempt++) {
|
|
162
|
+
let res;
|
|
163
|
+
try {
|
|
164
|
+
res = await api("/api/settings/apply", { method: "POST", headers: { "Idempotency-Key": o.applyKey },
|
|
165
|
+
body: JSON.stringify({ idempotency_key: o.applyKey }) });
|
|
166
|
+
} catch {
|
|
167
|
+
// The answer was lost (a phone between networks): the same key rejoins the same job, never a second one.
|
|
168
|
+
if (!mine(o)) return null;
|
|
169
|
+
if (attempt < POST_RETRIES.length) { await sleep(POST_RETRIES[attempt]); if (!mine(o)) return null; continue; }
|
|
170
|
+
res = { ok: false, status: 0, body: { error: "network" } };
|
|
171
|
+
}
|
|
172
|
+
if (!mine(o)) return null;
|
|
173
|
+
if (res.status === 409) { o.phase = "failed"; o.error = "busy"; publish(); return null; }
|
|
174
|
+
if (!res.ok || !res.body || !res.body.id) { o.phase = "failed"; o.error = (res.body && res.body.error) || "failed"; publish(); return null; }
|
|
175
|
+
return res.body;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** GET is the authority; a restart takes the server away under the request, which is the middle of it, not the end. */
|
|
180
|
+
async function watch(o, id) {
|
|
181
|
+
const deadline = now() + WATCH_LIMIT_MS;
|
|
182
|
+
let current = o.job;
|
|
183
|
+
while (current && current.status === "running" && now() < deadline) {
|
|
184
|
+
await sleep(1000);
|
|
185
|
+
if (!mine(o)) return;
|
|
186
|
+
let res;
|
|
187
|
+
try { res = await api(`/api/settings/apply/${encodeURIComponent(id)}`); } catch { continue; }
|
|
188
|
+
if (!mine(o)) return;
|
|
189
|
+
// The job outlives the process that made it, so a 404 means it aged out of the store, not that it never ran.
|
|
190
|
+
if (res.status === 404) { o.lostJob = true; o.phase = "done"; publish(); return; }
|
|
191
|
+
if (!res.ok || !res.body || !res.body.id) continue;
|
|
192
|
+
current = res.body; o.job = current; publish();
|
|
193
|
+
}
|
|
194
|
+
if (!mine(o)) return;
|
|
195
|
+
o.phase = "done"; publish();
|
|
196
|
+
// Said once, wherever the person is now: a restart reminder and a partial result are errors, a clean apply is not.
|
|
197
|
+
const rows = (current && current.targets) || [];
|
|
198
|
+
const failed = rows.some((r) => r.status === "failed"), restart = rows.some((r) => r.status === "restart-required");
|
|
199
|
+
if (!o.lostJob) toast(t(failed ? "settings.applyPartial" : restart ? "settings.applyRestartNeeded" : "settings.changesApplied"), !(failed || restart));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* A full AgEnD restart for a job that needs one: its own confirmation (the caller's), its own key, one at a time. The
|
|
204
|
+
* job's fleet row moves to "running" on the server; that job is watched again.
|
|
205
|
+
*/
|
|
206
|
+
export async function restartFleet(forJob) {
|
|
207
|
+
const o = op;
|
|
208
|
+
if (!o || !o.job || o.restart === "busy" || ACTIVE.has(o.phase)) return { ok: false };
|
|
209
|
+
// A confirmation given for one job never restarts for another (the operation was replaced meanwhile).
|
|
210
|
+
if (forJob && (forJob.opId !== o.id || forJob.jobId !== o.job.id)) return { ok: false, stale: true };
|
|
211
|
+
o.restart = "busy"; publish();
|
|
212
|
+
const key = newKey("restart");
|
|
213
|
+
let res;
|
|
214
|
+
try {
|
|
215
|
+
res = await api("/api/settings/restart-fleet", { method: "POST", headers: { "Idempotency-Key": key },
|
|
216
|
+
body: JSON.stringify({ job_id: o.job.id, confirm: "restart-agend", idempotency_key: key }) });
|
|
217
|
+
} catch { res = { ok: false, status: 0, body: null }; }
|
|
218
|
+
if (!mine(o)) return res;
|
|
219
|
+
if (!res.ok) { o.restart = "idle"; publish(); return res; }
|
|
220
|
+
toast(t("settings.restartFleetStarted")); // when it is accepted; how it ends is said when it settles
|
|
221
|
+
const fresh = await api(`/api/settings/apply/${encodeURIComponent(o.job.id)}`).catch(() => null);
|
|
222
|
+
if (!mine(o)) return res;
|
|
223
|
+
o.job = fresh && fresh.ok && fresh.body && fresh.body.id ? fresh.body
|
|
224
|
+
: { ...o.job, status: "running", targets: o.job.targets.map((row) => (row.target === "fleet" ? { ...row, status: "running" } : row)) };
|
|
225
|
+
o.phase = "watching"; publish();
|
|
226
|
+
try { await watch(o, o.job.id); } finally { if (mine(o)) { o.restart = "idle"; publish(); } }
|
|
227
|
+
return res;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Leaving while writes or the POST are still to come would lose them: the browser asks first. Once the job exists,
|
|
231
|
+
// a reload re-attaches nothing but loses nothing either (the job runs on the server).
|
|
232
|
+
if (typeof window !== "undefined") {
|
|
233
|
+
window.addEventListener("beforeunload", (e) => {
|
|
234
|
+
if (op && (op.phase === "writing" || op.phase === "posting")) { e.preventDefault(); e.returnValue = ""; }
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** For tests. */
|
|
239
|
+
export function resetOperation() { op = null; publish(); }
|