@songsid/agend 2.3.2-beta.10 → 2.3.2-beta.11
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/agent-cli.js +48 -0
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.js +6 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-whats-new.js +3 -0
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/backend/kiro-classic-archive.d.ts +52 -0
- package/dist/backend/kiro-classic-archive.js +444 -0
- package/dist/backend/kiro-classic-archive.js.map +1 -0
- package/dist/backend/kiro-identity.d.ts +19 -0
- package/dist/backend/kiro-identity.js +43 -0
- package/dist/backend/kiro-identity.js.map +1 -1
- package/dist/backend/kiro-migration-verify.d.ts +10 -0
- package/dist/backend/kiro-migration-verify.js +52 -0
- package/dist/backend/kiro-migration-verify.js.map +1 -0
- package/dist/backend/kiro-v2-store-worker.js +21 -1
- package/dist/backend/kiro-v2-store-worker.js.map +1 -1
- package/dist/backend/kiro-v2-store.d.ts +8 -0
- package/dist/backend/kiro-v2-store.js.map +1 -1
- package/dist/backend/kiro.d.ts +99 -1
- package/dist/backend/kiro.js +352 -18
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +49 -0
- package/dist/backend/types.js +9 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/channel/ipc-timeouts.js +3 -0
- package/dist/channel/ipc-timeouts.js.map +1 -1
- package/dist/channel/mcp-tools.js +12 -0
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/cli.js +33 -0
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +2 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/daemon.d.ts +45 -0
- package/dist/daemon.js +342 -19
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard-permissions.d.ts +15 -4
- package/dist/dashboard-permissions.js +49 -23
- package/dist/dashboard-permissions.js.map +1 -1
- package/dist/fleet-control-audit.d.ts +1 -1
- package/dist/fleet-control-audit.js +3 -0
- package/dist/fleet-control-audit.js.map +1 -1
- package/dist/fleet-manager.d.ts +61 -2
- package/dist/fleet-manager.js +421 -28
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +10 -0
- package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +25 -0
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +25 -0
- package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +12 -2
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +12 -2
- package/dist/instance-config-impact.js +2 -2
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +4 -0
- package/dist/instance-lifecycle.js +7 -0
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/kiro-migrate.d.ts +11 -0
- package/dist/kiro-migrate.js +86 -31
- package/dist/kiro-migrate.js.map +1 -1
- package/dist/kiro-session-proof.d.ts +41 -0
- package/dist/kiro-session-proof.js +89 -0
- package/dist/kiro-session-proof.js.map +1 -0
- package/dist/kiro-tui-forensic.d.ts +5 -0
- package/dist/kiro-tui-forensic.js +3 -3
- package/dist/kiro-tui-forensic.js.map +1 -1
- package/dist/locale.js +142 -0
- package/dist/locale.js.map +1 -1
- package/dist/log-service.js +1 -1
- package/dist/log-service.js.map +1 -1
- package/dist/needs-you.d.ts +3 -1
- package/dist/needs-you.js +4 -2
- package/dist/needs-you.js.map +1 -1
- package/dist/office-share.d.ts +12 -2
- package/dist/office-share.js +27 -2
- package/dist/office-share.js.map +1 -1
- package/dist/outbound-schemas.d.ts +39 -0
- package/dist/outbound-schemas.js +29 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/remote-card-prompt.d.ts +38 -0
- package/dist/remote-card-prompt.js +86 -0
- package/dist/remote-card-prompt.js.map +1 -0
- package/dist/settings-change.js +8 -5
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-instance-patch.js +1 -1
- package/dist/settings-instance-patch.js.map +1 -1
- package/dist/tool-permissions.js +17 -0
- package/dist/tool-permissions.js.map +1 -1
- package/dist/transcript-sources.d.ts +1 -0
- package/dist/transcript-sources.js +2 -1
- package/dist/transcript-sources.js.map +1 -1
- package/dist/types.d.ts +5 -0
- package/dist/ui/chat-store.js +3 -2
- package/dist/ui/chat-strings.js +2 -2
- package/dist/ui/fleet-office-editor.js +54 -2
- package/dist/ui/fleet-office-layout.js +44 -7
- package/dist/ui/fleet-office-pack.d.ts +6 -1
- package/dist/ui/fleet-office-pack.js +101 -15
- package/dist/ui/fleet-office-store.js +43 -0
- package/dist/ui/fleet-office-zones.js +64 -13
- package/dist/ui/fleet-office.js +5 -3
- package/dist/ui/panel-chat.js +14 -2
- package/dist/ui/panel-log.js +2 -2
- package/dist/ui/panel-needs.js +1 -1
- package/dist/ui/panel-settings.js +5 -5
- package/dist/ui/settings-model.js +17 -9
- package/dist/ui/settings-strings.js +2 -0
- package/dist/ui/shared/app-i18n.js +2 -2
- package/dist/ui/shared/app-stream.js +1 -1
- package/dist/ui/shared/app.css +18 -2
- package/dist/web-api.d.ts +5 -1
- package/dist/web-api.js +91 -3
- package/dist/web-api.js.map +1 -1
- package/dist/web-office-acts.js +2 -2
- package/dist/web-office-acts.js.map +1 -1
- package/dist/web-office-pack-read.d.ts +35 -0
- package/dist/web-office-pack-read.js +252 -0
- package/dist/web-office-pack-read.js.map +1 -0
- package/dist/web-office-pack-tools.d.ts +232 -0
- package/dist/web-office-pack-tools.js +754 -0
- package/dist/web-office-pack-tools.js.map +1 -0
- package/dist/web-office-skins.d.ts +68 -4
- package/dist/web-office-skins.js +318 -33
- package/dist/web-office-skins.js.map +1 -1
- package/dist/web-office-store.d.ts +5 -2
- package/dist/web-office-store.js +41 -15
- package/dist/web-office-store.js.map +1 -1
- package/package.json +1 -1
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
// (PUT /ui/offices; a change made meanwhile is a 409: read again, nothing overwritten). A stored office's layout (seats,
|
|
5
5
|
// paths) and its layout overrides (Edit layout, fleet-office-layout.js) are kept as they are. Skin packs (#2513): the
|
|
6
6
|
// packs the host keeps are styles to choose; an admin uploads one here (its manifest.json and its PNGs, picked together
|
|
7
|
-
// or as a folder — sent at once, checked by the host, which keeps it whole) or deletes one
|
|
7
|
+
// or as a folder — sent at once, checked by the host, which keeps it whole) or deletes one; #2692: which agents may change
|
|
8
|
+
// packs, whether their changes need the admin's yes, those waiting for it, and the newest log entries. Never offered to a guest,
|
|
8
9
|
// the public link or a read-only session (the host refuses them too).
|
|
9
|
-
import { html, useMemo, useState } from "/assets/app-html.js";
|
|
10
|
+
import { html, useEffect, useMemo, useState } from "/assets/app-html.js";
|
|
10
11
|
import { t, register } from "/assets/app-i18n.js";
|
|
11
12
|
import { SidePanel } from "/assets/side-panel.js";
|
|
12
13
|
import { toast } from "/assets/ui-toast.js";
|
|
@@ -29,6 +30,11 @@ register("orge", {
|
|
|
29
30
|
packsNone: "No skin packs yet.", upload: "Upload a skin pack", uploadFolder: "Upload a folder", uploading: "Uploading…", uploaded: "Skin pack “{0}” uploaded.",
|
|
30
31
|
uploadFailed: "The skin pack was not uploaded: {0}", noManifest: "no manifest.json among the files", badManifest: "manifest.json is not JSON",
|
|
31
32
|
removePack: "Delete {0}", removedPack: "Skin pack “{0}” deleted.", removeFailed: "The skin pack was not deleted ({0}).",
|
|
33
|
+
agentsTitle: "Agents changing packs", agentsLead: "Agents with Office skin packs switched on (Settings → agent) may check, upload, replace, delete and restore packs. Each change is logged and told to you in General.",
|
|
34
|
+
agentsNone: "No agent may change packs.", agentsAllowed: "May change packs: {0}", approval: "Their changes",
|
|
35
|
+
approvalApply: "Apply at once", approvalApprove: "Need my yes", pendingNone: "Nothing waits for your yes.",
|
|
36
|
+
approve: "Approve", reject: "Reject", decided: "Done: {0}", decideFailed: "Not done: {0}", recent: "Recent changes", refresh: "Refresh",
|
|
37
|
+
act_upload: "upload", act_delete: "delete", act_restore: "restore", act_style: "office style",
|
|
32
38
|
},
|
|
33
39
|
"zh-TW": {
|
|
34
40
|
edit: "編輯辦公室", title: "辦公室", lead: "這個 fleet 的每個人在辦公室檢視中都會看到這些辦公室。沒有選任何成員的辦公室會顯示全部。",
|
|
@@ -45,6 +51,11 @@ register("orge", {
|
|
|
45
51
|
packsNone: "還沒有外觀包。", upload: "上傳外觀包", uploadFolder: "上傳資料夾", uploading: "上傳中…", uploaded: "已上傳外觀包「{0}」。",
|
|
46
52
|
uploadFailed: "外觀包沒有上傳:{0}", noManifest: "檔案中沒有 manifest.json", badManifest: "manifest.json 不是 JSON",
|
|
47
53
|
removePack: "刪除 {0}", removedPack: "已刪除外觀包「{0}」。", removeFailed: "外觀包沒有刪除({0})。",
|
|
54
|
+
agentsTitle: "會改外觀包的 agent", agentsLead: "開啟「辦公室外觀包」的 agent(設定 → agent)可以檢查、上傳、取代、刪除與還原外觀包。每次變更都會記錄,並在 General 通知你。",
|
|
55
|
+
agentsNone: "沒有 agent 能改外觀包。", agentsAllowed: "可以改外觀包:{0}", approval: "它們的變更",
|
|
56
|
+
approvalApply: "直接套用", approvalApprove: "要我核准", pendingNone: "沒有等你核准的變更。",
|
|
57
|
+
approve: "核准", reject: "拒絕", decided: "完成:{0}", decideFailed: "沒有完成:{0}", recent: "最近的變更", refresh: "重新整理",
|
|
58
|
+
act_upload: "上傳", act_delete: "刪除", act_restore: "還原", act_style: "辦公室風格",
|
|
48
59
|
},
|
|
49
60
|
});
|
|
50
61
|
const te = (k, ...v) => t(`orge.${k}`, ...v);
|
|
@@ -114,6 +125,46 @@ function Packs({ packs, store }) {
|
|
|
114
125
|
</fieldset>`;
|
|
115
126
|
}
|
|
116
127
|
|
|
128
|
+
/**
|
|
129
|
+
* #2692: the agents that may change packs, the fleet's approval mode, the changes waiting for an admin's yes (approve /
|
|
130
|
+
* reject; judged again by the host on a yes) and the newest log entries. Read on opening, after each action, and when
|
|
131
|
+
* the packs change.
|
|
132
|
+
*/
|
|
133
|
+
function PackAgents({ store, packs }) {
|
|
134
|
+
const [d, setD] = useState(null);
|
|
135
|
+
const [busy, setBusy] = useState(false);
|
|
136
|
+
const [msg, setMsg] = useState("");
|
|
137
|
+
const load = async () => { const c = await store.packChanges(); if (c) setD(c); };
|
|
138
|
+
useEffect(() => { void load(); }, [packs.map((p) => `${p.id}:${p.sha}`).join(",")]);
|
|
139
|
+
if (!d) return null;
|
|
140
|
+
const mode = async (approval) => {
|
|
141
|
+
setBusy(true); setMsg("");
|
|
142
|
+
try { const r = await store.setPackApproval(approval); if (r.ok && r.changes) setD(r.changes); else setMsg(te("decideFailed", r.error)); } finally { setBusy(false); }
|
|
143
|
+
};
|
|
144
|
+
const decide = async (id, decision) => {
|
|
145
|
+
setBusy(true); setMsg("");
|
|
146
|
+
try { const r = await store.decidePackChange(id, decision); setMsg(r.ok ? te("decided", r.text) : te("decideFailed", r.detail || r.error)); await load(); } finally { setBusy(false); }
|
|
147
|
+
};
|
|
148
|
+
const what = (v) => `${te(`act_${v.action}`)} ${v.action === "style" ? `${v.office} → ${v.style}` : v.packs.map((p) => p.name !== p.id ? `${p.name} (${p.id})` : p.id).join(", ")}`;
|
|
149
|
+
return html`<fieldset class="oe-packs oe-form oe-pack-agents" disabled=${busy}>
|
|
150
|
+
<legend class="oe-label">${te("agentsTitle")}</legend>
|
|
151
|
+
<p class="note">${te("agentsLead")}</p>
|
|
152
|
+
<p class="note" id="oePackAgents">${d.agents.length ? te("agentsAllowed", d.agents.join(", ")) : te("agentsNone")}</p>
|
|
153
|
+
<label class="oe-field"><span class="oe-label">${te("approval")}</span>
|
|
154
|
+
<select class="tb-select oe-pack-approval" value=${d.approval} onChange=${(e) => mode(e.target.value)}>
|
|
155
|
+
<option value="approve">${te("approvalApprove")}</option><option value="apply">${te("approvalApply")}</option></select></label>
|
|
156
|
+
${d.pending.length ? html`<ul class="oe-pack-list">${d.pending.map((v) => html`<li key=${v.id} class="oe-pack oe-pack-pending" data-change=${v.id}>
|
|
157
|
+
<span>${v.instance}: ${what(v)}</span>
|
|
158
|
+
<button type="button" class="btn btn-sm btn-primary oe-pack-approve" onClick=${() => decide(v.id, "approve")}>${te("approve")}</button>
|
|
159
|
+
<button type="button" class="btn btn-sm oe-pack-reject" onClick=${() => decide(v.id, "reject")}>${te("reject")}</button></li>`)}</ul>`
|
|
160
|
+
: html`<p class="note">${te("pendingNone")}</p>`}
|
|
161
|
+
${d.log.length ? html`<details class="oe-pack-log"><summary>${te("recent")}</summary><ul class="oe-pack-list">${d.log.slice(0, 10).map((e, i) => html`<li key=${i} class="oe-pack muted">
|
|
162
|
+
<span class="mono">${e.at.slice(0, 16).replace("T", " ")}</span> <span>${e.by}: ${e.action} ${e.pack}${e.outcome ? ` — ${e.outcome}` : ""}${e.approved_by ? ` (${e.approved_by})` : ""}</span></li>`)}</ul></details>` : null}
|
|
163
|
+
<button type="button" class="btn btn-sm oe-pack-refresh" onClick=${() => { void load(); }}>${te("refresh")}</button>
|
|
164
|
+
${msg ? html`<p class="note oe-pack-msg" role="status">${msg}</p>` : null}
|
|
165
|
+
</fieldset>`;
|
|
166
|
+
}
|
|
167
|
+
|
|
117
168
|
/** A new office's id: unique among `taken`, the store's pattern ([A-Za-z0-9_-], never "auto"). */
|
|
118
169
|
export function newOfficeId(taken, rand = Math.random) {
|
|
119
170
|
for (;;) {
|
|
@@ -221,6 +272,7 @@ export function OfficeEditor({ offices, rev, view, store, packs = [], ask = () =
|
|
|
221
272
|
</fieldset>
|
|
222
273
|
${msg ? html`<p class="note oe-msg" role="status">${msg}</p>` : null}
|
|
223
274
|
${store.uploadSkin ? html`<${Packs} packs=${packs} store=${store} />` : null}
|
|
275
|
+
${store.packChanges ? html`<${PackAgents} packs=${packs} store=${store} />` : null}
|
|
224
276
|
${conflict ? html`<div class="oe-conflict">
|
|
225
277
|
<button type="button" class="btn btn-sm" id="oeKeepMine" onClick=${keepMine} disabled=${busy}>${te("keepMine")}</button>
|
|
226
278
|
<button type="button" class="btn btn-sm" id="oeLoadTheirs" onClick=${loadTheirs} disabled=${busy}>${te("loadTheirs")}</button>
|
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
// seats' figures, #2558), a moon, and a thin line to its day place. One that sleeps in place shows it on its day place
|
|
9
9
|
// (and it follows the item); dragged away, the item has its own night place; its Reset puts back the pack's.
|
|
10
10
|
//
|
|
11
|
+
// #2691: a selected seat lists the office's members under "Reserved for" (several may be picked) and "Lend when
|
|
12
|
+
// away": whoever it is for takes it first, and nobody else does unless it is lent while they are all elsewhere
|
|
13
|
+
// (fleet-office-zones.js assignSeats). Saved as the overrides' `seatFor` by area and slot; a reserved seat's handle
|
|
14
|
+
// is ringed.
|
|
15
|
+
//
|
|
11
16
|
// Every handle is a button for the keyboard too: Tab to it, the arrow keys move it (Shift: by 10), Delete removes a
|
|
12
17
|
// waypoint; the selected waypoint's corridors are listed with a way to join or unjoin each. Never offered to a guest,
|
|
13
18
|
// the public link or a read-only session (the host refuses their saves too).
|
|
@@ -25,6 +30,7 @@ register("orgl", {
|
|
|
25
30
|
reset: "Reset to the pack's layout", save: "Save layout", cancel: "Cancel", saved: "Layout saved.", hint: "Drag, or use the arrow keys (Shift: by 10).",
|
|
26
31
|
seat: "Seat {0} in {1}", furniture: "Item {0}", night: "Item {0} (night)", door: "Door", waypoint: "Waypoint {0}", at: "{0} at {1}, {2}",
|
|
27
32
|
nightReset: "Back to the pack's night place",
|
|
33
|
+
seatFor: "Seat {0} in {1}, reserved for {2}", reservedFor: "Reserved for", lend: "Lend when away", nobody: "No members to pick.",
|
|
28
34
|
changed: "Someone changed the offices meanwhile. Your layout is kept: Save again puts it on the current list.",
|
|
29
35
|
failed: "The layout was not saved ({0}).", rateLimited: "Too many saves. Try again in a minute.",
|
|
30
36
|
stale: "This office was removed or its style changed meanwhile: this layout can no longer be saved. Cancel closes it.",
|
|
@@ -35,6 +41,7 @@ register("orgl", {
|
|
|
35
41
|
reset: "還原成 pack 的配置", save: "儲存配置", cancel: "取消", saved: "配置已儲存。", hint: "拖曳,或用方向鍵移動(Shift:一次 10)。",
|
|
36
42
|
seat: "{1} 的座位 {0}", furniture: "家具 {0}", night: "家具 {0}(夜間)", door: "門", waypoint: "路徑點 {0}", at: "{0},位置 {1}, {2}",
|
|
37
43
|
nightReset: "回到 pack 的夜間位置",
|
|
44
|
+
seatFor: "{1} 的座位 {0},保留給 {2}", reservedFor: "保留給", lend: "不在時借給別人", nobody: "沒有可選的成員。",
|
|
38
45
|
changed: "有人在這期間修改了辦公室。你的配置已保留:再按一次儲存,會套用到目前的清單。",
|
|
39
46
|
failed: "配置沒有儲存({0})。", rateLimited: "儲存太頻繁,請一分鐘後再試。",
|
|
40
47
|
stale: "這個辦公室在這期間被移除,或它的風格已改變:這份配置無法再儲存。按取消關閉。",
|
|
@@ -48,12 +55,15 @@ const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, Math.round(v)));
|
|
|
48
55
|
/** An item's night place (#2665): its own, or (in place) its day place; `own` whether it has one. */
|
|
49
56
|
const nightAt = (f) => (f.night.x !== null && f.night.x !== undefined && f.night.y !== null && f.night.y !== undefined ? { x: f.night.x, y: f.night.y, own: true } : { x: f.x, y: f.y, own: false });
|
|
50
57
|
/**
|
|
51
|
-
* Every handle on a pack: { key, kind, label, x, y } — seats by area
|
|
52
|
-
* item with a `night`; `day` its day place, `inPlace` while it sleeps
|
|
58
|
+
* Every handle on a pack: { key, kind, label, x, y } — seats by area (#2691: `for` and `lend` when reserved),
|
|
59
|
+
* furniture, each item's night place (#2665: an item with a `night`; `day` its day place, `inPlace` while it sleeps
|
|
60
|
+
* there), the door, the waypoints.
|
|
53
61
|
*/
|
|
54
62
|
export function handlesOf(pack) {
|
|
55
63
|
const out = [];
|
|
56
|
-
for (const [name, a] of Object.entries(pack.areas)) a.seats.forEach((s, i) => out.push({ key: `seat:${name}:${i}`, kind: "seat",
|
|
64
|
+
for (const [name, a] of Object.entries(pack.areas)) a.seats.forEach((s, i) => out.push({ key: `seat:${name}:${i}`, kind: "seat",
|
|
65
|
+
label: s.for && s.for.length ? tl("seatFor", i + 1, name, s.for.join(", ")) : tl("seat", i + 1, name), x: s.x, y: s.y,
|
|
66
|
+
...(s.for && s.for.length ? { for: [...s.for], lend: s.lend === true } : {}) }));
|
|
57
67
|
for (const f of pack.furniture) out.push({ key: `furn:${f.id}`, kind: "furniture", label: tl("furniture", f.id), x: f.x, y: f.y });
|
|
58
68
|
for (const f of pack.furniture) {
|
|
59
69
|
if (!f.night) continue;
|
|
@@ -72,7 +82,7 @@ export function moveHandle(pack, key, x, y) {
|
|
|
72
82
|
const area = own(pack.areas, a), i = Number(b);
|
|
73
83
|
if (!area || !area.seats[i]) return pack;
|
|
74
84
|
const r = area.rect, p = { x: clamp(x, Math.ceil(r.x), Math.floor(r.x + r.w)), y: clamp(y, Math.ceil(r.y), Math.floor(r.y + r.h)) };
|
|
75
|
-
return { ...pack, areas: { ...pack.areas, [a]: { ...area, seats: area.seats.map((s, j) => (j === i ? p : s)) } } };
|
|
85
|
+
return { ...pack, areas: { ...pack.areas, [a]: { ...area, seats: area.seats.map((s, j) => (j === i ? { ...s, ...p } : s)) } } };
|
|
76
86
|
}
|
|
77
87
|
if (kind === "furn") return { ...pack, furniture: pack.furniture.map((f) => (f.id === a ? { ...f, ...inCanvas() } : f)) };
|
|
78
88
|
if (kind === "night") return { ...pack, furniture: pack.furniture.map((f) => (f.id === a && f.night ? { ...f, night: { ...f.night, ...inCanvas() } } : f)) };
|
|
@@ -104,6 +114,18 @@ export function resetNight(pack, base, id) {
|
|
|
104
114
|
if (!o || !o.night) return pack;
|
|
105
115
|
return { ...pack, furniture: pack.furniture.map((f) => (f.id === id && f.night ? { ...f, night: { ...f.night, x: o.night.x, y: o.night.y } } : f)) };
|
|
106
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* #2691: seat `key` ("seat:<area>:<i>") reserved for `names` (in that order, none twice), lent while they are all
|
|
119
|
+
* elsewhere when `lend`; no names: a plain seat again.
|
|
120
|
+
*/
|
|
121
|
+
export function reserveSeat(pack, key, names, lend = false) {
|
|
122
|
+
const [kind, a, b] = key.split(":");
|
|
123
|
+
const area = kind === "seat" ? own(pack.areas, a) : null, i = Number(b);
|
|
124
|
+
if (!area || !area.seats[i]) return pack;
|
|
125
|
+
const list = [...new Set(names)];
|
|
126
|
+
const seat = (s) => { const { for: _f, lend: _l, ...p } = s; return list.length ? { ...p, for: list, lend: lend === true } : p; };
|
|
127
|
+
return { ...pack, areas: { ...pack.areas, [a]: { ...area, seats: area.seats.map((s, j) => (j === i ? seat(s) : s)) } } };
|
|
128
|
+
}
|
|
107
129
|
/** What Save keeps: the overrides between the pack and the edited layout (null: the pack's own). */
|
|
108
130
|
export const layoutOverrides = (base, edited) => overridesBetween(base, edited);
|
|
109
131
|
|
|
@@ -123,9 +145,10 @@ const KEY_STEP = { ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, -1], Arr
|
|
|
123
145
|
* zoomed; the page sets its box through the CSSOM, never a style attribute: #1300) — a pointer is read back through
|
|
124
146
|
* the overlay's own box, so a drag lands on the logical point under it at any zoom; `onChange(edited)` as anything
|
|
125
147
|
* moves (the canvas redraws it); `onSave(overrides | null)` → { ok } or { ok: false, message }; `onClose()`. `stale` (a message): the
|
|
126
|
-
* office or its style changed underneath the session — no handles, nothing to save, only Cancel.
|
|
148
|
+
* office or its style changed underneath the session — no handles, nothing to save, only Cancel. `members` (#2691):
|
|
149
|
+
* the office's instances' names, offered under a selected seat's "Reserved for".
|
|
127
150
|
*/
|
|
128
|
-
export function LayoutEditor({ base, start, place = null, stale = "", dock = true, onChange, onSave, onClose }) {
|
|
151
|
+
export function LayoutEditor({ base, start, place = null, stale = "", dock = true, members = [], onChange, onSave, onClose }) {
|
|
129
152
|
const [edited, setEdited] = useState(start);
|
|
130
153
|
const [sel, setSel] = useState(null);
|
|
131
154
|
const [coords, setCoords] = useState(true);
|
|
@@ -160,6 +183,13 @@ export function LayoutEditor({ base, start, place = null, stale = "", dock = tru
|
|
|
160
183
|
const add = () => { const at = selected ? { x: selected.x + 40, y: selected.y } : { x: LOGICAL.w / 2, y: LOGICAL.h / 2 }; const r = addWaypoint(edited, at.x, at.y); change(r.pack); setSel(`wp:${r.id}`); };
|
|
161
184
|
const wpSel = selected && selected.kind === "waypoint" ? selected.key.slice(3) : null;
|
|
162
185
|
const nightSel = selected && selected.kind === "night" ? selected.key.slice(6) : null;
|
|
186
|
+
const seatSel = selected && selected.kind === "seat" ? selected : null;
|
|
187
|
+
// #2691: the members to pick from, and anyone the seat is already for who is not one now (so they can be unpicked).
|
|
188
|
+
const pickable = seatSel ? [...new Set([...members, ...(seatSel.for || [])])] : [];
|
|
189
|
+
const pickFor = (name) => {
|
|
190
|
+
const now = seatSel.for || [], next = now.includes(name) ? now.filter((n) => n !== name) : [...now, name];
|
|
191
|
+
change(reserveSeat(edited, seatSel.key, next, next.length ? seatSel.lend === true : false));
|
|
192
|
+
};
|
|
163
193
|
const save = async () => {
|
|
164
194
|
if (busy) return;
|
|
165
195
|
setBusy(true); setMsg("");
|
|
@@ -196,6 +226,12 @@ export function LayoutEditor({ base, start, place = null, stale = "", dock = tru
|
|
|
196
226
|
const controls = html`<label class="check"><input type="checkbox" class="ol-coords" checked=${coords} onChange=${() => setCoords((v) => !v)} /><span>${tl("coords")}</span></label>
|
|
197
227
|
<button type="button" class="btn btn-sm ol-add" onClick=${add} disabled=${busy}>${tl("addWaypoint")}</button>
|
|
198
228
|
<span class="muted ol-sel" aria-live="polite">${selected ? tl("at", selected.label, selected.x, selected.y) : tl("none")}</span>
|
|
229
|
+
${seatSel ? html`<fieldset class="ol-for"><legend class="oo-pick-label">${tl("reservedFor")}</legend>
|
|
230
|
+
${pickable.length ? pickable.map((n) => html`<label key=${n} class="check"><input type="checkbox" class="ol-for-pick" value=${n} checked=${(seatSel.for || []).includes(n)}
|
|
231
|
+
disabled=${busy} onChange=${() => pickFor(n)} /><span>${n}</span></label>`) : html`<span class="muted">${tl("nobody")}</span>`}
|
|
232
|
+
</fieldset>
|
|
233
|
+
<label class="check"><input type="checkbox" class="ol-lend" checked=${seatSel.lend === true} disabled=${busy || !(seatSel.for || []).length}
|
|
234
|
+
onChange=${() => change(reserveSeat(edited, seatSel.key, seatSel.for || [], !(seatSel.lend === true)))} /><span>${tl("lend")}</span></label>` : null}
|
|
199
235
|
${nightSel ? html`<button type="button" class="btn btn-sm ol-night-reset" onClick=${() => change(resetNight(edited, base, nightSel))} disabled=${busy}>${tl("nightReset")}</button>` : null}
|
|
200
236
|
${wpSel ? html`<button type="button" class="btn btn-sm btn-danger ol-remove" onClick=${() => { change(removeWaypoint(edited, wpSel)); setSel(null); }} disabled=${busy}>${tl("removeWaypoint")}</button>
|
|
201
237
|
<label class="oo-pick"><span class="oo-pick-label">${tl("joinTo")}</span>
|
|
@@ -219,12 +255,13 @@ export function LayoutEditor({ base, start, place = null, stale = "", dock = tru
|
|
|
219
255
|
${Object.values(edited.areas).map((a) => html`<rect key=${`a:${a.name}`} class="ol-area" x=${a.rect.x} y=${a.rect.y} width=${a.rect.w} height=${a.rect.h} />`)}
|
|
220
256
|
${edited.edges.map((e) => { const p = at.get(e[0]), q = at.get(e[1]); return p && q ? html`<line key=${`e:${edgeKey(e)}`} class="ol-edge" x1=${p.x} y1=${p.y} x2=${q.x} y2=${q.y} />` : null; })}
|
|
221
257
|
${handles.filter((h) => h.kind === "night" && !h.inPlace).map((h) => html`<line key=${`nl:${h.key}`} class="ol-night-line" x1=${h.day.x} y1=${h.day.y} x2=${h.x} y2=${h.y} />`)}
|
|
222
|
-
${[...handles.filter((h) => h.kind === "night"), ...handles.filter((h) => h.kind !== "night")].map((h) => html`<g key=${h.key} class=${`ol-handle ol-${h.kind}${sel === h.key ? " on" : ""}`} data-handle=${h.key} tabindex="0" role="button"
|
|
258
|
+
${[...handles.filter((h) => h.kind === "night"), ...handles.filter((h) => h.kind !== "night")].map((h) => html`<g key=${h.key} class=${`ol-handle ol-${h.kind}${h.for ? " reserved" : ""}${sel === h.key ? " on" : ""}`} data-handle=${h.key} tabindex="0" role="button"
|
|
223
259
|
aria-label=${tl("at", h.label, h.x, h.y)} aria-pressed=${sel === h.key ? "true" : "false"}
|
|
224
260
|
onPointerDown=${down(h.key)} onKeyDown=${keyOn(h)} onFocus=${() => setSel(h.key)}>
|
|
225
261
|
${h.kind === "door" ? html`<rect x=${h.x - 9} y=${h.y - 9} width="18" height="18" transform=${`rotate(45 ${h.x} ${h.y})`} />`
|
|
226
262
|
: h.kind === "furniture" ? html`<rect x=${h.x - 8} y=${h.y - 8} width="16" height="16" />`
|
|
227
263
|
: h.kind === "night" ? html`<circle cx=${h.x} cy=${h.y} r=${h.inPlace ? 15 : 8} /><path class="ol-moon" d=${h.inPlace ? moonPath(h.x + 10, h.y - 10) : moonPath(h.x, h.y)} />`
|
|
264
|
+
: h.kind === "seat" && h.for ? html`<circle class="ol-for-ring" cx=${h.x} cy=${h.y} r="13" /><circle cx=${h.x} cy=${h.y} r="9" />`
|
|
228
265
|
: html`<circle cx=${h.x} cy=${h.y} r=${h.kind === "seat" ? 9 : 7} />`}
|
|
229
266
|
${coords && sel === h.key ? html`<text class="ol-coord" x=${h.x + 12} y=${h.y - 10}>${h.x}, ${h.y}</text>` : null}
|
|
230
267
|
</g>`)}
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
/** `dw` × `dh`: its displayed size in logical units (w·scale × h·scale unless the manifest says otherwise). */
|
|
4
4
|
export interface PackStrip { src: string; frames: number; w: number; h: number; fps: number; ax: number; ay: number; dw: number; dh: number }
|
|
5
5
|
export interface PackPoint { x: number; y: number }
|
|
6
|
-
|
|
6
|
+
/** #2691: a seat reserved for `for` (lent while they are all elsewhere when `lend`); a plain seat has neither. */
|
|
7
|
+
export interface PackSeat extends PackPoint { for?: string[]; lend?: boolean }
|
|
8
|
+
export interface PackArea { name: string; rect: { x: number; y: number; w: number; h: number }; seats: PackSeat[] }
|
|
7
9
|
/** `label` (#2574): its name tag, or null. */
|
|
8
10
|
export interface PackFurniture { id: string; sprite: PackStrip; x: number; y: number; depth: number | "y"; activeWhen: string[] | null; activeSprite: PackStrip | null; label: string | null }
|
|
9
11
|
export interface CheckedPack {
|
|
@@ -20,6 +22,8 @@ export interface LayoutOverrides {
|
|
|
20
22
|
furniture?: Record<string, [number, number]>;
|
|
21
23
|
/** #2665: an item's night place (an item with a `night` only). */
|
|
22
24
|
furnitureNight?: Record<string, [number, number]>;
|
|
25
|
+
/** #2691: a seat's reservation by area and slot ("0", "1", …), in place of the pack's; `for: []` is none. */
|
|
26
|
+
seatFor?: Record<string, Record<string, { for: string[]; lend: boolean }>>;
|
|
23
27
|
door?: [number, number];
|
|
24
28
|
waypoints?: Array<{ id: string; x: number; y: number }>;
|
|
25
29
|
edges?: Array<[string, string]>;
|
|
@@ -38,6 +42,7 @@ export function packStrips(pack: CheckedPack): Array<{ at: string; strip: PackSt
|
|
|
38
42
|
export function checkLayoutOverrides(x: unknown): { ok: true; overrides: LayoutOverrides } | { ok: false; error: string };
|
|
39
43
|
export function applyOverrides(pack: CheckedPack, ov: LayoutOverrides | null | undefined): { ok: true; pack: CheckedPack } | { ok: false; error: string };
|
|
40
44
|
export function fitNight<T extends LayoutOverrides | null | undefined>(pack: CheckedPack, ov: T): T;
|
|
45
|
+
export function fitSeatFor<T extends LayoutOverrides | null | undefined>(pack: CheckedPack, ov: T): T;
|
|
41
46
|
export function overridesBetween(pack: CheckedPack, edited: CheckedPack): LayoutOverrides | null;
|
|
42
47
|
export function poseStrip(set: Record<string, PackStrip> | null | undefined, pose: string, heading?: { dx: number; dy: number } | null): { strip: PackStrip; mirror: boolean } | null;
|
|
43
48
|
export function walkStrip(f: { walk?: Record<string, PackStrip> | null; walking?: boolean; heading?: { dx: number; dy: number } | null } | null | undefined): { strip: PackStrip; mirror: boolean } | null;
|
|
@@ -24,6 +24,13 @@
|
|
|
24
24
|
// "areas": { named zones: a rect [x, y, w, h] and its seats, in the order
|
|
25
25
|
// "desks": { "rect": [40, 120, 620, 440], "seats": [[120, 220], [260, 220], …] }, they fill up
|
|
26
26
|
// "lounge": { "rect": [700, 120, 540, 260], "seats": [[760, 260], …] } },
|
|
27
|
+
// a seat is [x, y], or (#2691) { "x": 760, "y": 260, "for":
|
|
28
|
+
// ["atlas", …], "lend": false }: reserved for those instances
|
|
29
|
+
// (names checked like byInstance's, ≤ 16, none twice) — whoever
|
|
30
|
+
// of them comes to the area takes it first, and nobody else does
|
|
31
|
+
// unless "lend" (default false) lends it while they are all
|
|
32
|
+
// elsewhere (a borrower moves off when one comes). A checked
|
|
33
|
+
// seat is { x, y } — plus, reserved, `for` and `lend`
|
|
27
34
|
// "stateZones": { "working": "desks", "idle": "lounge", "needs": "alert", "default": "lounge" },
|
|
28
35
|
// the org chart's states (fleet-org.js ORG_STATES) → an area; a
|
|
29
36
|
// state it does not name goes to "default", else the first area
|
|
@@ -103,6 +110,9 @@
|
|
|
103
110
|
// "seats": { "desks": [[120, 220], …] }, an area's seats: as many as the pack's, each inside the area's rect
|
|
104
111
|
// "furniture": { "rack": [1000, 150] }, an item's (x, y)
|
|
105
112
|
// "furnitureNight": { "elder": [300, 420] }, an item's night place (#2665: an item with a "night" only)
|
|
113
|
+
// "seatFor": { "desks": { "0": { "for": ["atlas"], "lend": false } } },
|
|
114
|
+
// a seat's reservation by area and slot (#2691), in place of the
|
|
115
|
+
// pack's own for that seat; "for": [] is none (lends nothing)
|
|
106
116
|
// "door": [640, 700],
|
|
107
117
|
// "waypoints": [{ "id": "hall", "x": 640, "y": 600 }, …], "edges": [["hall", "east"], …] }
|
|
108
118
|
// the walkable graph, replaced whole (edges need waypoints)
|
|
@@ -112,8 +122,8 @@
|
|
|
112
122
|
// seat outside its area, an area or furniture the pack does not have, or a seat count that differs (the host checks
|
|
113
123
|
// that too, for a pack it keeps). A night place goes on the item's `night` (its x, y), so at bedtime (nightFurniture)
|
|
114
124
|
// it is the office's, else the pack's night x/y, else the item's (moved) day place. The host keeps `furnitureNight`
|
|
115
|
-
// beside the offices on disk, never inside them: an older host's stricter check would drop the
|
|
116
|
-
// web-office-store.ts).
|
|
125
|
+
// and `seatFor` (#2691) beside the offices on disk, never inside them: an older host's stricter check would drop the
|
|
126
|
+
// office (#2665, web-office-store.ts).
|
|
117
127
|
|
|
118
128
|
export const LOGICAL = { w: 1280, h: 720 };
|
|
119
129
|
/** The people's depth: furniture below it is drawn under them, the rest over them. */
|
|
@@ -133,7 +143,7 @@ const RESERVED = new Set(["cozy"]);
|
|
|
133
143
|
const ID = /^[A-Za-z0-9_-]{1,40}$/;
|
|
134
144
|
const SRC = /^[A-Za-z0-9._/-]{1,200}$/;
|
|
135
145
|
const HEX = /^#[0-9a-f]{6}$/i;
|
|
136
|
-
const LIMIT = { areas: 32, seats: 64, furniture: 128, waypoints: 512, edges: 2048, sets: 64, byInstance: 500, lines: 12, line: 40 };
|
|
146
|
+
const LIMIT = { areas: 32, seats: 64, furniture: 128, waypoints: 512, edges: 2048, sets: 64, byInstance: 500, lines: 12, line: 40, seatFor: 16 };
|
|
137
147
|
|
|
138
148
|
class Refused extends Error { constructor(at, why) { super(`${at}: ${why}`); } }
|
|
139
149
|
/** `o[k]` when it is o's own property, else undefined: a name from a pack or a page never reaches Object.prototype. */
|
|
@@ -184,10 +194,33 @@ function walkOf(v, at, scale) {
|
|
|
184
194
|
if (!w.walk) throw new Refused(`${at}.walk`, "required");
|
|
185
195
|
return w;
|
|
186
196
|
}
|
|
187
|
-
/** Whose state an item's activeWhen watches (#2570): an instance's name, or a list of up to 8. */
|
|
188
|
-
function names(v, at) {
|
|
197
|
+
/** Whose state an item's activeWhen watches (#2570): an instance's name, or a list of up to `max` (8). */
|
|
198
|
+
function names(v, at, max = 8) {
|
|
189
199
|
const one = (s, here) => { if (typeof s !== "string" || !s || s.length > 200 || inherited(s) || /[\u0000-\u001f\u007f]/.test(s)) throw new Refused(here, "an instance's name"); return s; };
|
|
190
|
-
return Array.isArray(v) ? list(v,
|
|
200
|
+
return Array.isArray(v) ? list(v, max, at).map((s, i) => one(s, `${at}[${i}]`)) : [one(v, at)];
|
|
201
|
+
}
|
|
202
|
+
/** Whom a seat is reserved for (#2691): instances' names (as names()), up to LIMIT.seatFor, none twice; `empty` may be []. */
|
|
203
|
+
function owners(v, at, empty = false) {
|
|
204
|
+
const out = names(v, at, LIMIT.seatFor);
|
|
205
|
+
if (!out.length && !empty) throw new Refused(at, "at least one instance");
|
|
206
|
+
if (new Set(out).size !== out.length) throw new Refused(at, "a name twice");
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* A seat (#2691): [x, y], or { x, y, for, lend } — reserved for `for` (lent to anyone while they are all elsewhere when
|
|
211
|
+
* `lend`). Checked: { x, y }, with `for` and `lend` only when reserved, so a plain seat is what it always was.
|
|
212
|
+
*/
|
|
213
|
+
function seatOf(v, at, rect) {
|
|
214
|
+
let p = null, extra = {};
|
|
215
|
+
if (isObj(v)) {
|
|
216
|
+
keys(v, ["x", "y", "for", "lend"], at);
|
|
217
|
+
p = { x: num(v.x, 0, LOGICAL.w, `${at}.x`), y: num(v.y, 0, LOGICAL.h, `${at}.y`) };
|
|
218
|
+
if (v.lend !== undefined && typeof v.lend !== "boolean") throw new Refused(`${at}.lend`, "true or false");
|
|
219
|
+
if (v.lend !== undefined && v.for === undefined) throw new Refused(`${at}.lend`, "only with for");
|
|
220
|
+
if (v.for !== undefined) extra = { for: owners(v.for, `${at}.for`), lend: v.lend === true };
|
|
221
|
+
} else p = point(v, at);
|
|
222
|
+
if (p.x < rect.x || p.x > rect.x + rect.w || p.y < rect.y || p.y > rect.y + rect.h) throw new Refused(at, "outside its area's rect");
|
|
223
|
+
return { ...p, ...extra };
|
|
191
224
|
}
|
|
192
225
|
function strips(v, names, at, scale) {
|
|
193
226
|
obj(v, at);
|
|
@@ -223,11 +256,7 @@ export function checkSkinPack(x, { kept = false } = {}) {
|
|
|
223
256
|
if (!Array.isArray(a.rect) || a.rect.length !== 4) throw new Refused(`${at}.rect`, "[x, y, w, h]");
|
|
224
257
|
const [rx, ry] = [num(a.rect[0], 0, LOGICAL.w, `${at}.rect[0]`), num(a.rect[1], 0, LOGICAL.h, `${at}.rect[1]`)];
|
|
225
258
|
const rect = { x: rx, y: ry, w: num(a.rect[2], 1, LOGICAL.w - rx, `${at}.rect[2]`), h: num(a.rect[3], 1, LOGICAL.h - ry, `${at}.rect[3]`) };
|
|
226
|
-
const seats = list(a.seats, LIMIT.seats, `${at}.seats`).map((s, i) => {
|
|
227
|
-
const p = point(s, `${at}.seats[${i}]`);
|
|
228
|
-
if (p.x < rect.x || p.x > rect.x + rect.w || p.y < rect.y || p.y > rect.y + rect.h) throw new Refused(`${at}.seats[${i}]`, "outside its area's rect");
|
|
229
|
-
return p;
|
|
230
|
-
});
|
|
259
|
+
const seats = list(a.seats, LIMIT.seats, `${at}.seats`).map((s, i) => seatOf(s, `${at}.seats[${i}]`, rect));
|
|
231
260
|
if (!seats.length) throw new Refused(`${at}.seats`, "at least one seat");
|
|
232
261
|
areas[name] = { name, rect, seats };
|
|
233
262
|
}
|
|
@@ -341,11 +370,17 @@ export function packStrips(pack) {
|
|
|
341
370
|
// ── Layout overrides ──
|
|
342
371
|
|
|
343
372
|
const OV_LIMIT = { areas: LIMIT.areas, seats: LIMIT.seats, furniture: LIMIT.furniture, waypoints: LIMIT.waypoints, edges: LIMIT.edges };
|
|
373
|
+
/** A seat's slot in `seatFor` (#2691): its index in the area's order, as a key ("0", "1", …). */
|
|
374
|
+
const SLOT = /^(0|[1-9][0-9]{0,2})$/;
|
|
375
|
+
/** A seat's reservation as an override keeps it: { for, lend }, or null (not reserved). */
|
|
376
|
+
const reservedOf = (s) => (s.for && s.for.length ? { for: [...s.for], lend: s.lend === true } : null);
|
|
377
|
+
/** A seat with reservation `r` ({ for, lend }; for [] or null: none) in place of its own. */
|
|
378
|
+
const reserve = (s, r) => { const { for: _f, lend: _l, ...p } = s; return r && r.for.length ? { ...p, for: [...r.for], lend: r.lend === true } : p; };
|
|
344
379
|
/** An office's layout overrides, strictly (their shape and ranges): { ok: true, overrides } or { ok: false, error }. */
|
|
345
380
|
export function checkLayoutOverrides(x) {
|
|
346
381
|
try {
|
|
347
382
|
obj(x, "overrides");
|
|
348
|
-
keys(x, ["pack", "seats", "furniture", "furnitureNight", "door", "waypoints", "edges"], "overrides");
|
|
383
|
+
keys(x, ["pack", "seats", "furniture", "furnitureNight", "seatFor", "door", "waypoints", "edges"], "overrides");
|
|
349
384
|
if (typeof x.pack !== "string" || !ID.test(x.pack)) throw new Refused("overrides.pack", "1–40 letters, digits, _ or -");
|
|
350
385
|
const out = { pack: x.pack };
|
|
351
386
|
if (x.seats !== undefined) {
|
|
@@ -380,6 +415,26 @@ export function checkLayoutOverrides(x) {
|
|
|
380
415
|
out.furnitureNight[id] = [q.x, q.y];
|
|
381
416
|
}
|
|
382
417
|
}
|
|
418
|
+
if (x.seatFor !== undefined) {
|
|
419
|
+
obj(x.seatFor, "overrides.seatFor");
|
|
420
|
+
if (Object.keys(x.seatFor).length > OV_LIMIT.areas) throw new Refused("overrides.seatFor", `at most ${OV_LIMIT.areas} areas`);
|
|
421
|
+
out.seatFor = {};
|
|
422
|
+
for (const [area, slots] of Object.entries(x.seatFor)) {
|
|
423
|
+
const at = `overrides.seatFor.${area}`;
|
|
424
|
+
if (!nameOk(area)) throw new Refused(at, "an area's name");
|
|
425
|
+
obj(slots, at);
|
|
426
|
+
if (Object.keys(slots).length > OV_LIMIT.seats) throw new Refused(at, `at most ${OV_LIMIT.seats} seats`);
|
|
427
|
+
out.seatFor[area] = {};
|
|
428
|
+
for (const [slot, r] of Object.entries(slots)) {
|
|
429
|
+
const here = `${at}.${slot}`;
|
|
430
|
+
if (!SLOT.test(slot) || Number(slot) >= OV_LIMIT.seats) throw new Refused(here, `a seat's number, 0 to ${OV_LIMIT.seats - 1}`);
|
|
431
|
+
obj(r, here); keys(r, ["for", "lend"], here);
|
|
432
|
+
if (!Array.isArray(r.for)) throw new Refused(`${here}.for`, "a list");
|
|
433
|
+
if (r.lend !== undefined && typeof r.lend !== "boolean") throw new Refused(`${here}.lend`, "true or false");
|
|
434
|
+
out.seatFor[area][slot] = { for: owners(r.for, `${here}.for`, true), lend: r.lend === true };
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
383
438
|
if (x.door !== undefined) { const q = point(x.door, "overrides.door"); out.door = [q.x, q.y]; }
|
|
384
439
|
if (x.edges !== undefined && x.waypoints === undefined) throw new Refused("overrides.edges", "only with waypoints");
|
|
385
440
|
if (x.waypoints !== undefined) {
|
|
@@ -422,9 +477,15 @@ export function applyOverrides(pack, ov) {
|
|
|
422
477
|
const r = a.rect;
|
|
423
478
|
areas[name] = { ...a, seats: pts.map(([x, y], i) => {
|
|
424
479
|
if (x < r.x || x > r.x + r.w || y < r.y || y > r.y + r.h) throw new Refused(`${at}[${i}]`, "outside its area's rect");
|
|
425
|
-
return { x, y };
|
|
480
|
+
return { ...a.seats[i], x, y }; // its reservation kept (#2691)
|
|
426
481
|
}) };
|
|
427
482
|
}
|
|
483
|
+
for (const [name, slots] of Object.entries(ov.seatFor || {})) {
|
|
484
|
+
const a = own(areas, name), at = `overrides.seatFor.${name}`;
|
|
485
|
+
if (!a) throw new Refused(at, "an area the pack defines");
|
|
486
|
+
for (const slot of Object.keys(slots)) if (!a.seats[Number(slot)]) throw new Refused(`${at}.${slot}`, `a seat the area has (it has ${a.seats.length})`);
|
|
487
|
+
areas[name] = { ...a, seats: a.seats.map((s, i) => { const v = own(slots, String(i)); return v ? reserve(s, v) : s; }) };
|
|
488
|
+
}
|
|
428
489
|
const fids = new Set(pack.furniture.map((f) => f.id));
|
|
429
490
|
for (const id of Object.keys(ov.furniture || {})) if (!fids.has(id)) throw new Refused(`overrides.furniture.${id}`, "an item the pack has");
|
|
430
491
|
const nids = new Set(pack.furniture.filter((f) => f.night).map((f) => f.id));
|
|
@@ -457,6 +518,26 @@ export function fitNight(pack, ov) {
|
|
|
457
518
|
return kept.length ? { ...rest, furnitureNight: Object.fromEntries(kept) } : rest;
|
|
458
519
|
}
|
|
459
520
|
|
|
521
|
+
/**
|
|
522
|
+
* #2691: overrides with only the reservations `pack` can take (an area it has, a seat that area has): a pack changed
|
|
523
|
+
* since drops that entry, never the rest of the layout (as fitNight; applyOverrides stays strict).
|
|
524
|
+
*/
|
|
525
|
+
export function fitSeatFor(pack, ov) {
|
|
526
|
+
if (!ov || !ov.seatFor || ov.pack !== pack.id) return ov;
|
|
527
|
+
let dropped = false;
|
|
528
|
+
const kept = {};
|
|
529
|
+
for (const [area, slots] of Object.entries(ov.seatFor)) {
|
|
530
|
+
const a = own(pack.areas, area);
|
|
531
|
+
const ok = Object.entries(slots).filter(([slot]) => a && !!a.seats[Number(slot)]);
|
|
532
|
+
// An area the pack no longer has is dropped even with no slots under it (`{}`: 0 kept of 0, yet applyOverrides refuses it).
|
|
533
|
+
if (!a || ok.length !== Object.keys(slots).length) dropped = true;
|
|
534
|
+
if (ok.length) kept[area] = Object.fromEntries(ok);
|
|
535
|
+
}
|
|
536
|
+
if (!dropped) return ov;
|
|
537
|
+
const { seatFor: _gone, ...rest } = ov;
|
|
538
|
+
return Object.keys(kept).length ? { ...rest, seatFor: kept } : rest;
|
|
539
|
+
}
|
|
540
|
+
|
|
460
541
|
/**
|
|
461
542
|
* What an edited layout keeps as overrides: only the parts that differ from the pack's own (`edited` is a pack with
|
|
462
543
|
* the same areas and furniture, positions moved); null when nothing does (the office goes back to the pack's).
|
|
@@ -465,8 +546,13 @@ export function overridesBetween(pack, edited) {
|
|
|
465
546
|
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
466
547
|
const ov = { pack: pack.id };
|
|
467
548
|
for (const [name, a] of Object.entries(pack.areas)) {
|
|
468
|
-
const e = own(edited.areas, name);
|
|
469
|
-
if (e && !same(a.seats, e.seats)) (ov.seats ||= {})[name] = e.seats.map(
|
|
549
|
+
const e = own(edited.areas, name), xy = (s) => [s.x, s.y];
|
|
550
|
+
if (e && !same(a.seats.map(xy), e.seats.map(xy))) (ov.seats ||= {})[name] = e.seats.map(xy);
|
|
551
|
+
// #2691: a seat whose reservation differs from the pack's (none where the pack has one: for []).
|
|
552
|
+
if (e) e.seats.forEach((s, i) => {
|
|
553
|
+
const was = a.seats[i] ? reservedOf(a.seats[i]) : null, now = reservedOf(s);
|
|
554
|
+
if (a.seats[i] && !same(was, now)) ((ov.seatFor ||= {})[name] ||= {})[String(i)] = now || { for: [], lend: false };
|
|
555
|
+
});
|
|
470
556
|
}
|
|
471
557
|
const at = new Map(pack.furniture.map((f) => [f.id, f]));
|
|
472
558
|
for (const f of edited.furniture) { const o = at.get(f.id); if (o && (o.x !== f.x || o.y !== f.y)) (ov.furniture ||= {})[f.id] = [f.x, f.y]; }
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
// - the skin packs an admin uploaded (#2513: GET /ui/office-skins, POST to upload one, DELETE to remove one;
|
|
8
8
|
// src/web-office-skins.ts). The office editor and the layout editor call saveOffices() with the whole list and the rev
|
|
9
9
|
// they read (a change in between answers 409).
|
|
10
|
+
// - #2692: the agents' pack changes (GET / POST /ui/office-pack-changes, an admin's): which agents may change packs, the
|
|
11
|
+
// fleet's approval mode, the changes waiting for a yes, and the newest log entries.
|
|
10
12
|
import { AUTO_ID, filterOk, officesOk } from "./fleet-office-model.js";
|
|
11
13
|
import { fitModeOk, savedOk } from "./fleet-office-view.js";
|
|
12
14
|
|
|
@@ -18,6 +20,19 @@ const VIEW_KEY = "agend_org_office_view", VIEWS_MAX = 32;
|
|
|
18
20
|
/** #2540: { "<office>@<style>": "cover" } — the offices whose fit fills the panel (the rest: "contain"), the last VIEWS_MAX. */
|
|
19
21
|
const FIT_KEY = "agend_org_office_fit";
|
|
20
22
|
const PACK_ID = /^[A-Za-z0-9_-]{1,40}$/;
|
|
23
|
+
/** #2692: GET /ui/office-pack-changes, checked: { approval, agents: [names], pending: [views], log: [entries] } (anything odd dropped). */
|
|
24
|
+
export function packChangesOk(d) {
|
|
25
|
+
const str = (v, n = 200) => (typeof v === "string" ? v.slice(0, n) : "");
|
|
26
|
+
const approval = d && (d.approval === "apply" || d.approval === "approve") ? d.approval : "approve";
|
|
27
|
+
const agents = Array.isArray(d && d.agents) ? d.agents.filter((n) => typeof n === "string" && n).slice(0, 500).map((n) => n.slice(0, 200)) : [];
|
|
28
|
+
const pending = (Array.isArray(d && d.pending) ? d.pending.slice(0, 32) : []).filter((v) => v && /^[0-9a-f]{32}$/.test(String(v.id)) && typeof v.instance === "string")
|
|
29
|
+
.map((v) => ({ id: v.id, instance: str(v.instance), action: str(v.action, 20), at: Number(v.at) || 0, expires_at: Number(v.expires_at) || 0,
|
|
30
|
+
packs: (Array.isArray(v.packs) ? v.packs.slice(0, 16) : []).filter((p) => p && typeof p.id === "string").map((p) => ({ id: str(p.id, 40), name: str(p.name, 60) || str(p.id, 40), files: Number(p.files) || 0, bytes: Number(p.bytes) || 0, replaces: typeof p.replaces === "string" ? p.replaces.slice(0, 32) : null })),
|
|
31
|
+
office: str(v.office, 40), style: str(v.style, 40), keep_previous: v.keep_previous === true, drops: Array.isArray(v.drops) ? v.drops.length : 0 }));
|
|
32
|
+
const log = (Array.isArray(d && d.log) ? d.log.slice(0, 50) : []).filter((e) => e && typeof e.at === "string")
|
|
33
|
+
.map((e) => ({ at: str(e.at, 40), by: str(e.by), action: str(e.action, 20), pack: str(e.pack, 200), outcome: str(e.outcome), files: Number(e.files) || 0, bytes: Number(e.bytes) || 0, approved_by: str(e.approved_by) }));
|
|
34
|
+
return { approval, agents, pending, log };
|
|
35
|
+
}
|
|
21
36
|
/** The host's list of skin packs, checked: [{ id, name, sha }] (anything odd dropped). */
|
|
22
37
|
export function skinListOk(d) {
|
|
23
38
|
const out = [], ids = new Set();
|
|
@@ -105,6 +120,34 @@ export function createOfficeStore({ fetch: get = globalThis.fetch, storage } = {
|
|
|
105
120
|
return r.ok ? { ok: true } : { ok: false, status: r.status, error: d.error || "skin_failed" };
|
|
106
121
|
} catch { return { ok: false, status: 0, error: "skin_failed" }; }
|
|
107
122
|
},
|
|
123
|
+
/**
|
|
124
|
+
* #2692: the agents' skin-pack changes (an admin session): { approval, agents, pending, log }, or null (not offered,
|
|
125
|
+
* not an admin, not read).
|
|
126
|
+
*/
|
|
127
|
+
async packChanges() {
|
|
128
|
+
try {
|
|
129
|
+
const r = await get("/ui/office-pack-changes", { method: "GET", headers });
|
|
130
|
+
if (!r || !r.ok) return null;
|
|
131
|
+
return packChangesOk(await r.json());
|
|
132
|
+
} catch { return null; }
|
|
133
|
+
},
|
|
134
|
+
/** #2692: the fleet's approval mode for agents' pack changes ("apply" | "approve"). */
|
|
135
|
+
async setPackApproval(approval) {
|
|
136
|
+
try {
|
|
137
|
+
const r = await get("/ui/office-pack-changes", { method: "POST", headers, body: JSON.stringify({ approval }) });
|
|
138
|
+
const d = await r.json().catch(() => ({}));
|
|
139
|
+
return r.ok ? { ok: true, changes: packChangesOk(d) } : { ok: false, status: r.status, error: d.error || "pack_changes_failed" };
|
|
140
|
+
} catch { return { ok: false, status: 0, error: "pack_changes_failed" }; }
|
|
141
|
+
},
|
|
142
|
+
/** #2692: an admin's yes or no on a pending pack change: { ok, text } or { ok: false, status, error, detail }. */
|
|
143
|
+
async decidePackChange(id, decision) {
|
|
144
|
+
if (!/^[0-9a-f]{32}$/.test(String(id))) return { ok: false, status: 0, error: "change_unknown", detail: "" };
|
|
145
|
+
try {
|
|
146
|
+
const r = await get(`/ui/office-pack-changes/${id}`, { method: "POST", headers, body: JSON.stringify({ decision }) });
|
|
147
|
+
const d = await r.json().catch(() => ({}));
|
|
148
|
+
return r.ok ? { ok: true, text: String(d.text || "") } : { ok: false, status: r.status, error: d.error || "pack_changes_failed", detail: String(d.detail || "") };
|
|
149
|
+
} catch { return { ok: false, status: 0, error: "pack_changes_failed", detail: "" }; }
|
|
150
|
+
},
|
|
108
151
|
/**
|
|
109
152
|
* The office shown here: an id (one the host no longer has falls back to the auto office where it is used), or
|
|
110
153
|
* #2554's All offices (fleet-office-overview.js ALL_ID; with fewer than two offices stored: the auto office).
|