@songsid/agend 2.3.1 → 2.3.2-beta.2
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/authority/authority-state.d.ts +4 -0
- package/dist/authority/authority-state.js +16 -10
- package/dist/authority/authority-state.js.map +1 -1
- package/dist/authority/session-authorizations.d.ts +7 -1
- package/dist/authority/session-authorizations.js +6 -4
- package/dist/authority/session-authorizations.js.map +1 -1
- package/dist/authority/web-authority.d.ts +19 -6
- package/dist/authority/web-authority.js +22 -8
- package/dist/authority/web-authority.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +2 -0
- package/dist/channel/adapters/discord.js +33 -3
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/mcp-tools.js +5 -5
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/types.d.ts +11 -0
- package/dist/config-validator.js +40 -3
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.d.ts +34 -3
- package/dist/daemon.js +227 -46
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +12 -1
- package/dist/delivery-outbox.js +67 -15
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/doctor.d.ts +3 -0
- package/dist/doctor.js +9 -0
- package/dist/doctor.js.map +1 -1
- package/dist/event-log.d.ts +5 -0
- package/dist/event-log.js +13 -0
- package/dist/event-log.js.map +1 -1
- package/dist/fleet-manager.d.ts +140 -0
- package/dist/fleet-manager.js +414 -29
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-process-handlers.d.ts +1 -0
- package/dist/fleet-process-handlers.js +9 -1
- package/dist/fleet-process-handlers.js.map +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +4 -4
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +7 -7
- package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +113 -55
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +114 -56
- package/dist/guest-seen-people.d.ts +38 -0
- package/dist/guest-seen-people.js +78 -0
- package/dist/guest-seen-people.js.map +1 -0
- package/dist/instance-config-impact.js +4 -1
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +1 -0
- package/dist/instance-lifecycle.js +16 -1
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +38 -10
- package/dist/locale.js.map +1 -1
- package/dist/mac-auto-restore.d.ts +117 -0
- package/dist/mac-auto-restore.js +294 -0
- package/dist/mac-auto-restore.js.map +1 -0
- package/dist/outbound-schemas.d.ts +2 -0
- package/dist/outbound-schemas.js +2 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +9 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/settings-api.d.ts +26 -0
- package/dist/settings-api.js +107 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +37 -3
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +5 -2
- package/dist/settings-effect.js.map +1 -1
- package/dist/start-attempt.d.ts +9 -5
- package/dist/start-attempt.js +27 -8
- package/dist/start-attempt.js.map +1 -1
- package/dist/start-failure-chat.d.ts +4 -0
- package/dist/start-failure-chat.js +23 -6
- package/dist/start-failure-chat.js.map +1 -1
- package/dist/start-failure-notice.d.ts +4 -0
- package/dist/start-failure-notice.js +8 -0
- package/dist/start-failure-notice.js.map +1 -1
- package/dist/start-failure.d.ts +25 -0
- package/dist/start-failure.js +75 -2
- package/dist/start-failure.js.map +1 -1
- package/dist/turn-reply-guard.d.ts +25 -0
- package/dist/turn-reply-guard.js +111 -12
- package/dist/turn-reply-guard.js.map +1 -1
- package/dist/types.d.ts +18 -1
- package/dist/ui/backend-signin.js +5 -2
- package/dist/ui/chat-ambient.js +1 -1
- package/dist/ui/chat-effects.js +4 -4
- package/dist/ui/chat-render.js +5 -2
- package/dist/ui/chat-strings.js +2 -2
- package/dist/ui/chat-thread.js +2 -2
- package/dist/ui/first-run.js +2 -2
- package/dist/ui/fleet-canvas-view.js +114 -0
- package/dist/ui/fleet-canvas.js +15 -77
- package/dist/ui/fleet-office-editor.js +80 -5
- package/dist/ui/fleet-office-layout.js +197 -0
- package/dist/ui/fleet-office-model.js +6 -3
- package/dist/ui/fleet-office-overview.js +167 -0
- package/dist/ui/fleet-office-pack.d.ts +39 -0
- package/dist/ui/fleet-office-pack.js +240 -29
- package/dist/ui/fleet-office-skin.js +140 -29
- package/dist/ui/fleet-office-store.js +84 -5
- package/dist/ui/fleet-office-view.js +154 -0
- package/dist/ui/fleet-office-zones.js +95 -33
- package/dist/ui/fleet-office.js +779 -140
- package/dist/ui/fleet-usage.js +7 -6
- package/dist/ui/panel-chat.js +1 -1
- package/dist/ui/panel-details.js +2 -2
- package/dist/ui/panel-fleet.js +11 -77
- package/dist/ui/panel-log.js +2 -2
- package/dist/ui/panel-settings.js +578 -163
- package/dist/ui/search-dialog.js +26 -7
- package/dist/ui/settings-appearance.js +82 -29
- package/dist/ui/settings-apply.js +42 -7
- package/dist/ui/settings-dialogs.js +68 -22
- package/dist/ui/settings-draft.js +51 -0
- package/dist/ui/settings-guests.js +244 -78
- package/dist/ui/settings-learn.js +2 -2
- package/dist/ui/settings-model.js +2 -0
- package/dist/ui/settings-registry.js +142 -0
- package/dist/ui/settings-strings.js +102 -44
- package/dist/ui/settings-web-authority.js +34 -23
- package/dist/ui/shared/app-i18n.js +14 -0
- package/dist/ui/shared/app-nav.js +10 -5
- package/dist/ui/shared/app-prefs.js +80 -2
- package/dist/ui/shared/app-route.js +39 -5
- package/dist/ui/shared/app-shell.js +59 -4
- package/dist/ui/shared/app-stream.js +2 -1
- package/dist/ui/shared/app.css +254 -17
- package/dist/ui/shared/app.js +3 -0
- package/dist/ui/shared/effect-stock-trust.js +1 -1
- package/dist/ui/shared/identity-color.js +4 -4
- package/dist/ui/shared/signin.js +2 -2
- package/dist/ui/shared/ui-dialog.js +6 -1
- package/dist/ui/shared/ui-icons.js +2 -0
- package/dist/ui/shared/web-authorize.js +13 -12
- package/dist/web-api.d.ts +8 -2
- package/dist/web-api.js +160 -5
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.js +7 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-experimental.d.ts +1 -1
- package/dist/web-experimental.js +1 -1
- package/dist/web-experimental.js.map +1 -1
- package/dist/web-mod-agent.js +2 -2
- package/dist/web-mod-agent.js.map +1 -1
- package/dist/web-office-skins.d.ts +108 -0
- package/dist/web-office-skins.js +298 -0
- package/dist/web-office-skins.js.map +1 -0
- package/dist/web-office-store.d.ts +8 -2
- package/dist/web-office-store.js +37 -7
- package/dist/web-office-store.js.map +1 -1
- package/dist/web-settings-patch.d.ts +6 -0
- package/dist/web-settings-patch.js +61 -4
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-shell-routes.d.ts +4 -2
- package/dist/web-shell-routes.js +17 -7
- package/dist/web-shell-routes.js.map +1 -1
- package/dist/working-recovery.d.ts +104 -0
- package/dist/working-recovery.js +263 -0
- package/dist/working-recovery.js.map +1 -0
- package/dist/working-state-journal.d.ts +3 -1
- package/dist/working-state-journal.js +15 -2
- package/dist/working-state-journal.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// #2347: one save model for Settings. A section's edits are its draft from the first keystroke: the draft is kept while
|
|
2
|
+
// the person moves between sections, staged as soon as it differs from what is saved (the sticky bar counts it and offers
|
|
3
|
+
// Save), and dropped on Save or Discard. Nothing is written until Save.
|
|
4
|
+
import { useEffect, useMemo, useState } from "/assets/app-html.js";
|
|
5
|
+
import { sameValue } from "./settings-model.js";
|
|
6
|
+
|
|
7
|
+
/** The fields of `form` that differ from `saved` (a draft is only what was edited). */
|
|
8
|
+
export function editsOf(form, saved) {
|
|
9
|
+
const out = {};
|
|
10
|
+
for (const k of Object.keys(form)) if (!sameValue(form[k], saved[k])) out[k] = form[k];
|
|
11
|
+
return out;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A section's form, kept in the panel's drafts by `key` while it differs from what is saved: leaving the section and
|
|
16
|
+
* coming back shows the same edits (they are still staged). `saved` builds the form from the saved configuration;
|
|
17
|
+
* `init` (default: `saved`) is the form a section starts from when no draft is kept.
|
|
18
|
+
*
|
|
19
|
+
* #2367 review: the draft is the edited fields only, put back over what is saved when the section is shown again — a
|
|
20
|
+
* field never touched follows the configuration as it is now (another admin's change since), never a stale copy of it.
|
|
21
|
+
*/
|
|
22
|
+
export function useDraft(ctx, key, saved, init = saved) {
|
|
23
|
+
const base = useMemo(saved, []);
|
|
24
|
+
const [form, setForm] = useState(() => (ctx.drafts && ctx.drafts.has(key) ? { ...base, ...ctx.drafts.get(key) } : init()));
|
|
25
|
+
useEffect(() => { keepDraft(ctx, key, editsOf(form, base), form); }, [form]);
|
|
26
|
+
return [form, setForm];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Keep `edits` as the draft `key` (none: no draft). #2367 review: while a Save runs, a field that Save is writing and
|
|
31
|
+
* the form has since put back to what was saved before is an edit too — once the Save lands, what is saved is the new
|
|
32
|
+
* value, and the person's last word was the old one.
|
|
33
|
+
*/
|
|
34
|
+
export function keepDraft(ctx, key, edits, form) {
|
|
35
|
+
if (!ctx.drafts) return;
|
|
36
|
+
const writing = typeof ctx.savingDrafts === "function" ? ctx.savingDrafts() : null;
|
|
37
|
+
const sent = writing && writing.get(key);
|
|
38
|
+
if (sent) for (const k of Object.keys(sent)) if (!(k in edits)) edits[k] = form[k];
|
|
39
|
+
if (Object.keys(edits).length) ctx.drafts.set(key, edits);
|
|
40
|
+
else ctx.drafts.delete(key);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Bring the staged change `key` in line with a section's draft: staged (quietly — the bar says it) when there is one,
|
|
45
|
+
* unstaged when the draft equals what is saved. An invalid draft is staged as `{ label, invalid }` with no request: it
|
|
46
|
+
* counts as unsaved, and Save waits until it is fixed (its message is shown at the field).
|
|
47
|
+
*/
|
|
48
|
+
export function syncStage(ctx, key, change) {
|
|
49
|
+
if (change) ctx.stage(key, change, true);
|
|
50
|
+
else ctx.unstage(key);
|
|
51
|
+
}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
// #
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// fleet admin
|
|
1
|
+
// #2348 (on #1920): Settings → Access & security → Guests. An admin chooses who may sign in as a guest and which
|
|
2
|
+
// agents each of them sees — people AgEnD has seen in chat (GET /api/settings/guests/people) — and may send one of them
|
|
3
|
+
// a sign-in code by DM. The switches (guest sign-in, guests may chat) and the grants are one draft, staged on every edit
|
|
4
|
+
// as PUT /api/settings/fleet/web { guests } (the shell's sticky bar is this section's Save; the server judges what
|
|
5
|
+
// waits for a fleet admin: a new person or a new agent does, taking away applies at once). Send sign-in code and Sign
|
|
6
|
+
// out are actions: they run at once, outside Save. Before any grant is saved the fleet uses #1920's derived access;
|
|
7
|
+
// "Start from today's guests" stages each guest's current reach as a grant.
|
|
8
|
+
//
|
|
9
|
+
// The shell's contract (agreed with web-claude, #2347): ctx.stage(key, { label, impact, request, count, invalid?,
|
|
10
|
+
// onResult }) on every edit, ctx.unstage(key) when the draft is the saved state, ctx.draftEpoch (Discard / after a Save:
|
|
11
|
+
// reset from ctx.fleet), ctx.readOnly (the public link: controls disabled, actions hidden), `s-dirty` on edited rows.
|
|
6
12
|
import { html, useEffect, useMemo, useState } from "/assets/app-html.js";
|
|
7
13
|
import { t } from "/assets/app-i18n.js";
|
|
8
14
|
import { toast } from "/assets/ui-toast.js";
|
|
@@ -14,105 +20,265 @@ import "./settings-strings.js";
|
|
|
14
20
|
const tn = (k, ...v) => t(`settings.${k}`, ...v);
|
|
15
21
|
const PLATFORMS = { discord: "Discord", telegram: "Telegram" };
|
|
16
22
|
const own = (o, k) => !!o && Object.prototype.hasOwnProperty.call(o, k);
|
|
17
|
-
const copyLists = (m) => { const out = {}; for (const [k, v] of Object.entries(m || {})) if (Array.isArray(v)) out[k] = [...v]; return out; };
|
|
18
23
|
const sameList = (a, b) => (a || []).length === (b || []).length && (a || []).every((n) => (b || []).includes(n));
|
|
19
24
|
const accountKey = (g) => `${g.adapterId}:${g.userId}`;
|
|
25
|
+
const copyGrants = (m) => { const out = {}; for (const [k, v] of Object.entries(m || {})) if (v && Array.isArray(v.instances)) out[k] = { ...(v.name ? { name: v.name } : {}), instances: [...v.instances] }; return out; };
|
|
26
|
+
/** The saved state, from the fleet's config: `grants` null while the fleet still uses #1920's derived access. */
|
|
27
|
+
export function savedGuests(fleet) {
|
|
28
|
+
const g = (fleet && fleet.web && fleet.web.guests) || {};
|
|
29
|
+
return { enabled: g.enabled === true, chat: g.chat !== false, grants: g.grants ? copyGrants(g.grants) : null };
|
|
30
|
+
}
|
|
31
|
+
const sameGrant = (a, b) => !!a && !!b && (a.name || "") === (b.name || "") && sameList(a.instances, b.instances);
|
|
20
32
|
|
|
21
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* What changed against the saved state, as the PUT /api/settings/fleet/web body's `guests` (null: nothing), and how
|
|
35
|
+
* many things changed (the nav badge). A grant is sent whole; a removed one is null.
|
|
36
|
+
*/
|
|
22
37
|
export function guestsBody(original, f) {
|
|
23
38
|
const out = {};
|
|
24
|
-
|
|
25
|
-
if (f.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (a
|
|
32
|
-
|
|
33
|
-
|
|
39
|
+
let count = 0;
|
|
40
|
+
if (f.enabled !== original.enabled) { out.enabled = f.enabled ? true : null; count++; } // off is the default
|
|
41
|
+
if (f.chat !== original.chat) { out.chat = f.chat ? null : false; count++; } // on is the default
|
|
42
|
+
if (f.grants) {
|
|
43
|
+
const was = original.grants || {}, diff = {};
|
|
44
|
+
for (const k of new Set([...Object.keys(was), ...Object.keys(f.grants)])) {
|
|
45
|
+
const a = own(was, k) ? was[k] : undefined, b = own(f.grants, k) ? f.grants[k] : undefined;
|
|
46
|
+
if (a && b && sameGrant(a, b)) continue;
|
|
47
|
+
diff[k] = b ? { ...(b.name ? { name: b.name } : {}), instances: [...b.instances] } : null;
|
|
48
|
+
count++;
|
|
34
49
|
}
|
|
35
|
-
|
|
50
|
+
// #2365 review: turning grants on is a change by itself, even with no one in them yet (default deny): `grants: {}`.
|
|
51
|
+
if (Object.keys(diff).length || !original.grants) out.grants = diff;
|
|
52
|
+
if (!original.grants && !Object.keys(diff).length) count++;
|
|
36
53
|
}
|
|
37
|
-
return Object.keys(out).length ? out : null;
|
|
54
|
+
return Object.keys(out).length ? { body: out, count } : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** "Wrote to: a, b" with each agent's name kept whole on a narrow screen (a name breaks at its hyphens otherwise). */
|
|
58
|
+
function wroteTo(names) {
|
|
59
|
+
const [before, after = ""] = tn("grantsWroteTo", "\u0000").split("\u0000");
|
|
60
|
+
return html`${before}${names.map((n, i) => html`${i ? ", " : ""}<span key=${n} class="g-wrote">${n}</span>`)}${after}`;
|
|
38
61
|
}
|
|
39
62
|
|
|
40
|
-
|
|
41
|
-
function Names({ id, all, value, onChange, away = new Set() }) {
|
|
63
|
+
function Names({ id, all, value, wrote = [], onChange }) {
|
|
42
64
|
const chosen = new Set(value || []);
|
|
43
|
-
return html`<div class="guest-names" id=${id} role="group">${all.map((n) => html`<label key=${n} class
|
|
65
|
+
return html`<div class="guest-names" id=${id} role="group">${all.map((n) => html`<label key=${n} class="check guest-name">
|
|
44
66
|
<input type="checkbox" checked=${chosen.has(n)} onChange=${(e) => { const s = new Set(chosen); if (e.target.checked) s.add(n); else s.delete(n); onChange(all.filter((x) => s.has(x))); }} />
|
|
45
|
-
<span>${n}${
|
|
67
|
+
<span>${n}${wrote.includes(n) ? html` <span class="note">${tn("grantsWroteHere")}</span>` : null}</span></label>`)}</div>`;
|
|
46
68
|
}
|
|
47
69
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
70
|
+
const clock = (ms) => new Date(ms).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
71
|
+
const COOLDOWN_MS = 60_000;
|
|
72
|
+
|
|
73
|
+
/** A draft as staged (#2365 review: carried in the staged change, so leaving the section and coming back keeps it). */
|
|
74
|
+
const cloneDraft = (d) => ({ enabled: !!d.enabled, chat: d.chat !== false, grants: d.grants ? copyGrants(d.grants) : null });
|
|
75
|
+
|
|
76
|
+
export function GuestsSection({ ctx, staged }) {
|
|
77
|
+
const readOnly = !!ctx.readOnly;
|
|
78
|
+
const [original, setOriginal] = useState(() => savedGuests(ctx.fleet));
|
|
79
|
+
// A draft already staged (this section was left with unsaved changes, and is shown again): pick it up, never drop it.
|
|
80
|
+
const prior = (staged && typeof staged.get === "function" && staged.get("guests")) || (ctx.staged && typeof ctx.staged.get === "function" && ctx.staged.get("guests")) || null;
|
|
81
|
+
const [f, setF] = useState(() => (prior && prior.draft ? cloneDraft(prior.draft) : { ...original, grants: original.grants ? copyGrants(original.grants) : null }));
|
|
82
|
+
const [overview, setOverview] = useState(null), [failed, setFailed] = useState(false);
|
|
83
|
+
const [people, setPeople] = useState(null), [query, setQuery] = useState(""), [editing, setEditing] = useState(null);
|
|
84
|
+
const [sendState, setSendState] = useState({}), [now, setNow] = useState(() => Date.now());
|
|
85
|
+
const [status, setStatus] = useState(null); // { kind: "waiting" } | { kind: "failed", error }
|
|
53
86
|
const load = async () => {
|
|
54
87
|
let r = null;
|
|
55
88
|
try { r = await api("/api/settings/guests"); } catch { r = null; }
|
|
56
89
|
if (r && r.ok && r.body) { setOverview(r.body); setFailed(false); } else setFailed(true);
|
|
57
90
|
};
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
91
|
+
const loadPeople = async () => {
|
|
92
|
+
let r = null;
|
|
93
|
+
try { r = await api("/api/settings/guests/people"); } catch { r = null; }
|
|
94
|
+
setPeople(r && r.ok && r.body && Array.isArray(r.body.people) ? r.body.people : []);
|
|
95
|
+
};
|
|
96
|
+
useEffect(() => { load(); if (!readOnly) loadPeople(); }, []);
|
|
97
|
+
// Discard, or a Save the shell finished: start again from what fleet.yaml says now. Until the new shell's draftEpoch
|
|
98
|
+
// exists, the current page's Discard and Apply both reload ctx.fleet (a new object): that is the epoch then.
|
|
99
|
+
const epoch = ctx.draftEpoch !== undefined ? ctx.draftEpoch : ctx.fleet;
|
|
100
|
+
const first = useMemo(() => ({ seen: false }), []);
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (!first.seen) { first.seen = true; return; }
|
|
103
|
+
const o = savedGuests(ctx.fleet);
|
|
104
|
+
// #2367 review: an edit made while a Save ran is staged again and outlives that Save; Discard leaves none.
|
|
105
|
+
const since = ctx.staged && typeof ctx.staged.get === "function" ? ctx.staged.get("guests") : null;
|
|
106
|
+
setOriginal(o); setF(since && since.draft ? cloneDraft(since.draft) : { ...o, grants: o.grants ? copyGrants(o.grants) : null });
|
|
107
|
+
setEditing(null); setStatus(null);
|
|
108
|
+
load();
|
|
109
|
+
}, [epoch]);
|
|
110
|
+
// A cooldown on screen counts down.
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
if (!Object.values(sendState).some((s) => s && s.until > Date.now())) return undefined;
|
|
113
|
+
const id = setInterval(() => setNow(Date.now()), 1000);
|
|
114
|
+
return () => clearInterval(id);
|
|
115
|
+
}, [sendState]);
|
|
116
|
+
|
|
117
|
+
const empty = f.grants ? Object.entries(f.grants).find(([, g]) => !g.instances.length) : null;
|
|
118
|
+
// Stage on every edit: the shell's bar is this section's Save.
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
const change = (diff) => (diff ? {
|
|
121
|
+
label: tn("guestsTitle"), impact: impactOf(ctx.schema || DEFAULT_SCHEMA, "web.guests"), count: diff.count, draft: cloneDraft(f),
|
|
122
|
+
request: { method: "PUT", url: "/api/settings/fleet/web", body: { guests: diff.body } },
|
|
123
|
+
...(empty ? { invalid: tn("grantsNoAgents"), section: "access", target: "#guests" } : {}),
|
|
124
|
+
onResult: (outcome) => {
|
|
125
|
+
if (!outcome || outcome.status === "applied") { setStatus(null); load(); }
|
|
126
|
+
else if (outcome.status === "waiting") setStatus({ kind: "waiting" });
|
|
127
|
+
else setStatus({ kind: "failed", error: outcome.error || "" });
|
|
128
|
+
},
|
|
129
|
+
} : null);
|
|
130
|
+
// #2367 r3: an edit made while a Save of this section runs is weighed against both ends of it — what is saved if
|
|
131
|
+
// that Save lands (its draft) and if it fails (what is saved now). The shell keeps the one that came true, so the
|
|
132
|
+
// latest word stands either way (also one that puts a field back where it was before the Save).
|
|
133
|
+
const writing = typeof ctx.savingStaged === "function" ? ctx.savingStaged() : null;
|
|
134
|
+
const sent = writing && writing.get("guests");
|
|
135
|
+
if (sent && sent.draft) {
|
|
136
|
+
const applied = change(guestsBody(cloneDraft(sent.draft), f)), failed = change(guestsBody(original, f));
|
|
137
|
+
if (!applied && !failed) { ctx.unstage("guests"); return; }
|
|
138
|
+
ctx.stage("guests", { ...(applied || failed), afterSave: { applied, failed } }, true);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const now = change(guestsBody(original, f));
|
|
142
|
+
if (!now) { ctx.unstage("guests"); return; }
|
|
143
|
+
ctx.stage("guests", now, true);
|
|
144
|
+
}, [f, original]);
|
|
145
|
+
|
|
146
|
+
const set = (patch) => { setStatus(null); setF((x) => ({ ...x, ...patch })); };
|
|
147
|
+
const setGrant = (k, grant) => { setStatus(null); setF((x) => { const g = { ...(x.grants || {}) }; if (grant) g[k] = grant; else delete g[k]; return { ...x, grants: g }; }); };
|
|
148
|
+
const guests = (overview && overview.guests) || [];
|
|
149
|
+
const instances = (overview && overview.instances) || [];
|
|
150
|
+
const who = (k) => {
|
|
151
|
+
const g = guests.find((x) => accountKey(x) === k), p = (people || []).find((x) => accountKey(x) === k);
|
|
152
|
+
const grant = (f.grants && f.grants[k]) || (original.grants && original.grants[k]);
|
|
153
|
+
const at = k.indexOf(":");
|
|
154
|
+
return { key: k, adapterId: k.slice(0, at), userId: k.slice(at + 1), name: (p && p.name) || (g && g.name) || (grant && grant.name) || k.slice(at + 1),
|
|
155
|
+
platform: (g && g.platform) || (p && p.platform) || "", username: p && p.username, wrote: (p && p.instances) || [], guest: g };
|
|
66
156
|
};
|
|
67
|
-
|
|
68
|
-
|
|
157
|
+
// "Start from today's guests": each guest's current reach, as a grant (staged like any edit).
|
|
158
|
+
const convert = () => {
|
|
159
|
+
const grants = {};
|
|
160
|
+
for (const g of guests) if (g.scope && g.scope.length) grants[accountKey(g)] = { ...(g.name ? { name: g.name } : {}), instances: [...g.scope] };
|
|
161
|
+
set({ grants });
|
|
162
|
+
};
|
|
163
|
+
const signOut = async (person) => {
|
|
164
|
+
const yes = await confirmDialog({ title: tn("guestSignOutTitle"), message: tn("guestSignOutConfirm", person.name), confirmLabel: tn("guestSignOut"), danger: true });
|
|
69
165
|
if (!yes) return;
|
|
70
166
|
let r = null;
|
|
71
|
-
try { r = await api("/api/settings/guests/revoke", { method: "POST", body: JSON.stringify({ adapterId:
|
|
72
|
-
if (r && r.ok) { toast(tn(r.body && r.body.durable === false ? "guestSignedOutNotDurable" : "guestSignedOut",
|
|
167
|
+
try { r = await api("/api/settings/guests/revoke", { method: "POST", body: JSON.stringify({ adapterId: person.adapterId, userId: person.userId }) }); } catch { r = null; }
|
|
168
|
+
if (r && r.ok) { toast(tn(r.body && r.body.durable === false ? "guestSignedOutNotDurable" : "guestSignedOut", person.name)); load(); }
|
|
73
169
|
else toast(tn("guestSignOutFailed"), false);
|
|
74
170
|
};
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
171
|
+
const sendCode = async (person) => {
|
|
172
|
+
setSendState((s) => ({ ...s, [person.key]: { sending: true } }));
|
|
173
|
+
let r = null;
|
|
174
|
+
try { r = await api("/api/settings/guests/send-code", { method: "POST", body: JSON.stringify({ adapterId: person.adapterId, userId: person.userId }) }); } catch { r = null; }
|
|
175
|
+
const b = (r && r.body) || {};
|
|
176
|
+
const until = b.outcome === "sent" ? Date.now() + COOLDOWN_MS : b.outcome === "wait" ? Date.now() + (b.retryAfterMs || COOLDOWN_MS) : 0;
|
|
177
|
+
setSendState((s) => ({ ...s, [person.key]: { result: { ...b, at: Date.now() }, until } }));
|
|
178
|
+
setNow(Date.now());
|
|
179
|
+
if (b.outcome === "wait") toast(tn("grantsWait"), false);
|
|
180
|
+
else if (b.outcome === "admin_limit") toast(tn("grantsAdminLimit"), false);
|
|
181
|
+
else if (b.outcome !== "sent" && b.outcome !== "dm_failed") toast(tn("grantsCodeFailed", guestCodeReason(b)), false);
|
|
182
|
+
load();
|
|
183
|
+
};
|
|
184
|
+
/** The state pill: what happened last with this person's code, or whether they are signed in. */
|
|
185
|
+
const pill = (person) => {
|
|
186
|
+
const mine = sendState[person.key];
|
|
187
|
+
// #2365 review (P3): the time it was sent, captured once — never "now" on every render.
|
|
188
|
+
const code = (mine && mine.result && mine.result.outcome) ? mine.result : person.guest && person.guest.code;
|
|
189
|
+
if (code && code.outcome === "dm_failed") {
|
|
190
|
+
const text = code.platform === "telegram" && code.startUrl ? tn("grantsDmFailedTg", person.name, code.startUrl)
|
|
191
|
+
: code.platform === "discord" && /cannot send messages/i.test(code.reason || "") ? tn("grantsDmFailedDiscord", person.name)
|
|
192
|
+
: tn("grantsDmFailed", person.name, code.reason || "");
|
|
193
|
+
return html`<span class="g-state warn" role="status">${text}</span>`;
|
|
194
|
+
}
|
|
195
|
+
if (code && code.outcome === "sent" && code.expiresAt && code.expiresAt > now) return html`<span class="g-state sent">${tn("grantsSent", clock(code.at || now), clock(code.expiresAt))}</span>`;
|
|
196
|
+
const g = person.guest;
|
|
197
|
+
return g && (g.sessions || g.devices) ? html`<span class="g-state on">${tn("grantsSignedIn", g.devices || g.sessions)}</span>` : html`<span class="g-state">${tn("grantsNotSignedIn")}</span>`;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const rows = f.grants ? [...new Set([...Object.keys(f.grants), ...Object.keys(original.grants || {})])] : [];
|
|
201
|
+
const q = query.trim().toLowerCase();
|
|
202
|
+
const candidates = (people || []).filter((p) => !(f.grants && own(f.grants, accountKey(p))))
|
|
203
|
+
.filter((p) => !q || [p.name, p.username, p.userId].some((x) => typeof x === "string" && x.toLowerCase().includes(q))).slice(0, 8);
|
|
204
|
+
|
|
205
|
+
return html`<section class="card s-guests" id="guests"><h3>${tn("guestsTitle")}</h3>
|
|
206
|
+
<p class="note">${tn("grantsHint")}</p>
|
|
207
|
+
<fieldset class="s-guest-fields" disabled=${readOnly}>
|
|
208
|
+
<label class=${`check${f.enabled !== original.enabled ? " s-dirty" : ""}`}><input id="guestsEnabled" type="checkbox" checked=${f.enabled} onChange=${(e) => set({ enabled: e.target.checked })} /><span>${tn("guestsEnabled")}</span></label>
|
|
83
209
|
<p class="note">${tn("guestsEnabledHint")}</p>
|
|
84
|
-
<label class
|
|
210
|
+
<label class=${`check${f.chat !== original.chat ? " s-dirty" : ""}`}><input id="guestsChat" type="checkbox" checked=${f.chat} onChange=${(e) => set({ chat: e.target.checked })} /><span>${tn("guestsChat")}</span></label>
|
|
85
211
|
<p class="note">${tn("guestsChatHint")}</p>
|
|
86
|
-
${open.map((c) => html`<div key=${c.id} class="field guest-open"><span class="label">${tn("guestsOpenFor", c.id, PLATFORMS[c.type] || c.type)}</span>
|
|
87
|
-
<p class="note">${tn("guestsOpenHint")}</p>
|
|
88
|
-
<${Names} id=${`guestsOpen-${c.id}`} all=${instances} value=${own(f.open_instances, c.id) ? f.open_instances[c.id] : []} onChange=${(list) => setList("open_instances", c.id, list)} /></div>`)}
|
|
89
|
-
<h4>${tn("guestsList")}</h4>
|
|
90
212
|
${failed ? html`<p class="note" role="status">${tn("guestsLoadFailed")} <button type="button" class="btn btn-sm" onClick=${load}>${t("app.retry")}</button></p>`
|
|
91
|
-
: !overview ? html`<p class="note">${t("app.loading")}</p>`
|
|
92
|
-
|
|
93
|
-
: html`<
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
</div>
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
213
|
+
: !overview ? html`<p class="note">${t("app.loading")}</p>` : null}
|
|
214
|
+
${!f.grants && overview ? html`<div class="g-legacy"><p class="note">${tn("grantsLegacy")}</p>
|
|
215
|
+
${readOnly ? null : html`<button type="button" class="btn btn-sm" id="grantsConvert" onClick=${convert}>${tn("grantsConvert")}</button>`}</div>` : null}
|
|
216
|
+
<h4>${tn("grantsWithAccess")}</h4>
|
|
217
|
+
${!rows.length ? html`<p class="note" id="grantsNone">${tn("grantsNoneYet")}</p>` : html`<ul class="guest-list">${rows.map((k) => {
|
|
218
|
+
const person = who(k), draft = f.grants && f.grants[k], saved = original.grants && original.grants[k];
|
|
219
|
+
const removed = !draft, added = !saved, dirty = removed || added || !sameGrant(draft, saved);
|
|
220
|
+
const applied = !!saved && saved.instances.length > 0 && !dirty;
|
|
221
|
+
const ss = sendState[k] || {}, cooling = ss.until && ss.until > now;
|
|
222
|
+
return html`<li key=${k} class=${`guest-row${dirty ? " s-dirty" : ""}${removed ? " g-removed" : ""}`} data-account=${k}>
|
|
223
|
+
<div class="g-head"><span class="g-av" aria-hidden="true">${(person.name || "?").slice(0, 1).toUpperCase()}</span>
|
|
224
|
+
<div class="g-id"><strong>${person.name}</strong> <span class="note">${PLATFORMS[person.platform] || person.platform}${person.username ? ` · @${person.username}` : ""}</span></div>
|
|
225
|
+
${removed ? html`<span class="g-state">${tn("grantsWillBeRemoved")}</span>` : added ? html`<span class="g-state">${tn("grantsNew")}</span>` : pill(person)}</div>
|
|
226
|
+
<div class="g-chips">${((draft || saved).instances || []).map((n) => html`<span key=${n} class="g-chip">${n}</span>`)}</div>
|
|
227
|
+
${draft && !draft.instances.length ? html`<p class="note g-error" role="alert">${tn("grantsNoAgents")}</p>` : null}
|
|
228
|
+
${editing === k && draft ? html`<div class="g-edit"><p class="note">${tn("grantsMaySee", person.name)}</p>
|
|
229
|
+
<${Names} id=${`grant-${k}`} all=${instances} value=${draft.instances} wrote=${person.wrote} onChange=${(list) => setGrant(k, { ...draft, instances: list })} /></div>` : null}
|
|
230
|
+
${readOnly ? null : html`<div class="guest-actions">
|
|
231
|
+
${draft ? html`<button type="button" class="btn btn-sm" aria-expanded=${editing === k ? "true" : "false"} onClick=${() => setEditing(editing === k ? null : k)}>${tn("grantsEditAgents")}</button>` : null}
|
|
232
|
+
${draft ? html`<button type="button" class="btn btn-sm g-send" disabled=${!applied || ss.sending || cooling || !f.enabled || !original.enabled}
|
|
233
|
+
title=${applied ? (person.platform ? tn("grantsPublicLinkNote") : undefined) : tn("grantsSendSaveFirst")} onClick=${() => sendCode(person)}>
|
|
234
|
+
${ss.sending ? tn("grantsSending") : cooling ? tn("grantsSendAgain", `0:${String(Math.ceil((ss.until - now) / 1000)).padStart(2, "0")}`) : tn("grantsSend")}</button>` : null}
|
|
235
|
+
${draft ? html`<button type="button" class="btn btn-sm btn-danger" onClick=${() => setGrant(k, null)}>${tn("grantsRemove")}</button>`
|
|
236
|
+
: html`<button type="button" class="btn btn-sm" onClick=${() => setGrant(k, { ...saved })}>${tn("grantsUndoRemove")}</button>`}
|
|
237
|
+
${person.guest && (person.guest.sessions || person.guest.devices) ? html`<button type="button" class="btn btn-sm" onClick=${() => signOut(person)}>${tn("guestSignOut")}</button>` : null}
|
|
238
|
+
</div>`}
|
|
239
|
+
</li>`;
|
|
240
|
+
})}</ul>`}
|
|
241
|
+
${readOnly ? null : html`<h4>${tn("grantsAdd")}</h4>
|
|
242
|
+
<input type="search" class="input g-search" id="grantsSearch" value=${query} aria-label=${tn("grantsSearch")} placeholder=${tn("grantsSearch")} onInput=${(e) => setQuery(e.target.value)} />
|
|
243
|
+
${people === null ? html`<p class="note">${t("app.loading")}</p>` : !candidates.length ? html`<p class="note" id="grantsNoPeople">${tn("grantsNoPeople")}</p>`
|
|
244
|
+
: html`<ul class="g-pick">${candidates.map((p) => html`<li key=${accountKey(p)} class="g-cand" data-account=${accountKey(p)}>
|
|
245
|
+
<span class="g-av" aria-hidden="true">${(p.name || "?").slice(0, 1).toUpperCase()}</span>
|
|
246
|
+
<div class="g-id"><strong>${p.name}</strong> <span class="note">${PLATFORMS[p.platform] || p.platform}${p.username ? ` · @${p.username}` : ""} · <span class="g-when">${tn("grantsLastSeen", new Date(p.lastSeen).toLocaleString())}</span></span>
|
|
247
|
+
${p.instances && p.instances.length ? html`<div class="note">${wroteTo(p.instances)}</div>` : null}</div>
|
|
248
|
+
<button type="button" class="btn btn-sm" onClick=${() => { const k = accountKey(p); setGrant(k, { ...(p.name ? { name: p.name } : {}), instances: instances.filter((n) => (p.instances || []).includes(n)) }); setEditing(k); }}>${tn("grantsChoose")}</button></li>`)}</ul>`}`}
|
|
114
249
|
</fieldset>
|
|
115
|
-
|
|
116
|
-
|
|
250
|
+
${overview && overview.audit ? html`<h4>${tn("grantsRecent")}</h4>${!overview.audit.length ? html`<p class="note" id="grantsRecentNone">${tn("grantsRecentNone")}</p>`
|
|
251
|
+
: html`<ul class="g-audit" id="grantsRecentList">${overview.audit.map((e, i) => html`<li key=${`${e.at}-${i}`} class="g-audit-row" data-kind=${e.kind}>
|
|
252
|
+
<span class="g-when">${new Date(e.at).toLocaleString()}</span> <strong>${e.name}</strong>
|
|
253
|
+
${e.kind === "grant" ? html`${e.added.map((n) => html` <span key=${`+${n}`} class="g-wrote g-add">+${n}</span>`)}${e.removed.map((n) => html` <span key=${`-${n}`} class="g-wrote g-del">−${n}</span>`)}
|
|
254
|
+
<span class="note"> · <span class="g-wrote">${tn(e.via === "confirmed" ? "grantsAuditConfirmed" : "grantsAuditDirect")}</span>${e.endedSessions ? html` · <span class="g-wrote">${tn("grantsAuditEnded", e.endedSessions)}</span>` : null}</span>`
|
|
255
|
+
: html` <span class="note">${e.outcome === "sent" ? tn("grantsAuditCodeSent") : tn("grantsAuditCodeNot", auditOutcome(e.outcome))}</span>`}</li>`)}</ul>`}` : null}
|
|
256
|
+
${status && status.kind === "waiting" ? html`<p class="note" role="status">${tn("grantsWaiting")}</p>` : null}
|
|
257
|
+
${status && status.kind === "failed" ? html`<p class="note g-error" role="alert">${tn("grantsSaveFailed", status.error)}</p>` : null}
|
|
117
258
|
</section>`;
|
|
118
259
|
}
|
|
260
|
+
|
|
261
|
+
/** The current Settings page's name for it (panel-settings.js), until the new shell mounts GuestsSection directly. */
|
|
262
|
+
export const GuestsCard = GuestsSection;
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* #2347 search: this section's entries, in the registry's shape (web-claude's settings-registry.js merges them in when it
|
|
266
|
+
* lands): id, section, card, label, the words a search finds it by, and the config key.
|
|
267
|
+
*/
|
|
268
|
+
/** #2390: the send-code outcomes (fleet-manager sendGuestCodeByAdmin) and their words. */
|
|
269
|
+
export const CODE_REASONS = { guests_off: "codeGuestsOff", admin: "codeAdmin", nothing: "codeNothing", starting: "codeStarting", paused: "codePaused", busy: "codeBusy", revoked: "codeRevoked" };
|
|
270
|
+
/** Why a code was not sent, in this page's language (the server's `error` is English): by its outcome, else as sent. */
|
|
271
|
+
/** #2499 review (P3): a code's outcome in the audit, in words — the toasts' words, plus the outcomes only the audit shows. */
|
|
272
|
+
const AUDIT_OUTCOMES = { dm_failed: "grantsAuditDmFailed", wait: "grantsAuditWait", admin_limit: "grantsAuditAdminLimit" };
|
|
273
|
+
const auditOutcome = (outcome) => (Object.prototype.hasOwnProperty.call(AUDIT_OUTCOMES, outcome) ? tn(AUDIT_OUTCOMES[outcome]) : guestCodeReason({ outcome }));
|
|
274
|
+
export const guestCodeReason = (b) => (Object.prototype.hasOwnProperty.call(CODE_REASONS, b.outcome) ? tn(CODE_REASONS[b.outcome]) : b.error || b.outcome || "?");
|
|
275
|
+
export const GUESTS_SETTINGS = [
|
|
276
|
+
{ id: "guests.enabled", section: "access", card: "guests", key: "web.guests.enabled",
|
|
277
|
+
label: { en: "Guest sign-in", "zh-TW": "訪客登入" }, words: { en: ["guest", "sign in", "visitor", "share"], "zh-TW": ["訪客", "登入", "分享"] } },
|
|
278
|
+
{ id: "guests.chat", section: "access", card: "guests", key: "web.guests.chat",
|
|
279
|
+
label: { en: "Guests may chat", "zh-TW": "訪客可以聊天" }, words: { en: ["guest", "chat", "read-only"], "zh-TW": ["訪客", "聊天", "唯讀"] } },
|
|
280
|
+
{ id: "guests.grants", section: "access", card: "guests", key: "web.guests.grants",
|
|
281
|
+
label: { en: "People with access", "zh-TW": "有存取權的人" }, words: { en: ["guest", "grant", "access", "person", "user", "agents", "who can see"], "zh-TW": ["訪客", "授權", "存取", "使用者", "誰可以看"] } },
|
|
282
|
+
{ id: "guests.send-code", section: "access", card: "guests",
|
|
283
|
+
label: { en: "Send sign-in code", "zh-TW": "傳送登入碼" }, words: { en: ["code", "sign-in code", "invite", "token", "DM"], "zh-TW": ["登入碼", "邀請", "私訊"] } },
|
|
284
|
+
];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// #2056 B: a card title with a small "?" that opens the card's section of the docs, so Settings →
|
|
1
|
+
// #2056 B: a card title with a small "?" that opens the card's section of the docs, so Settings → Look
|
|
2
2
|
// keeps one short line per control and the long explanations live in docs/web-dashboard(.zh-TW).md.
|
|
3
3
|
import { html } from "/assets/app-html.js";
|
|
4
4
|
import { lang, t } from "/assets/app-i18n.js";
|
|
@@ -10,7 +10,7 @@ import "./settings-strings.js";
|
|
|
10
10
|
*/
|
|
11
11
|
const DOCS = { en: "https://github.com/songsid/AgEnD/blob/main/docs/web-dashboard.md", "zh-TW": "https://github.com/songsid/AgEnD/blob/main/docs/web-dashboard.zh-TW.md" };
|
|
12
12
|
export const LEARN_ANCHORS = {
|
|
13
|
-
colors: ["identity-colours", "識別色"], css: ["custom-css-settings--
|
|
13
|
+
colors: ["identity-colours", "識別色"], css: ["custom-css-settings--look", "自訂-css設定--外觀"], themes: ["agents-themes", "agent-的主題"],
|
|
14
14
|
cards: ["identity-cards", "身分卡"], limits: ["card-upload-limits", "身分卡上傳限制"], widgets: ["widgets", "小工具"], mods: ["mods", "mod"],
|
|
15
15
|
effects: ["effects", "效果"], restore: ["restore-defaults", "還原預設值"],
|
|
16
16
|
"app-icon": ["app-icon", "app-圖示"],
|
|
@@ -88,6 +88,8 @@ export const chLabel = (i) => (i === 0 ? "primary" : "persona");
|
|
|
88
88
|
// ── Impacts: the server's (/api/settings/schema, derived from HOT_INSTANCE_CONFIG_KEYS); this page never decides one ──
|
|
89
89
|
export const DEFAULT_SCHEMA = { impacts: {}, order: ["now", "instance", "fleet"] };
|
|
90
90
|
export const impactOf = (schema, field) => (schema.impacts && schema.impacts[field]) || "instance";
|
|
91
|
+
/** #2347: what a change costs that touches nothing in fleet.yaml (this person's or this device's own): the cheapest kind. */
|
|
92
|
+
export const ownChoiceImpact = (schema) => (schema && Array.isArray(schema.order) && schema.order.length ? schema : DEFAULT_SCHEMA).order[0];
|
|
91
93
|
/** #2346: a new connection starts at once while another one runs (the schema says so); the first one needs AgEnD restarted. */
|
|
92
94
|
export const newConnectionImpact = (schema) => (schema && schema.connections_start_live === true ? "now" : impactOf(schema || DEFAULT_SCHEMA, "fleet.channels"));
|
|
93
95
|
/** A batch costs as much as its worst member, in the order the server ships. */
|