@songsid/agend 2.3.2-beta.10 → 2.3.2-beta.12
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 +4 -0
- package/README.zh-TW.md +4 -0
- 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/docker-install/SKILL.md +25 -0
- package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +10 -0
- package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +28 -0
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +28 -0
- package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +20 -2
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +20 -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 +9 -3
- package/dist/ui/fleet-office-pack.js +117 -19
- package/dist/ui/fleet-office-store.js +43 -0
- package/dist/ui/fleet-office-zones.js +92 -17
- package/dist/ui/fleet-office.js +102 -16
- 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 +21 -2
- package/dist/web-office-store.js +78 -18
- package/dist/web-office-store.js.map +1 -1
- package/package.json +1 -1
- package/templates/windows-shortcuts.ps1 +16 -15
|
@@ -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,9 +3,12 @@
|
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
export interface
|
|
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[] }
|
|
9
|
+
/** `label` (#2574): its name tag, or null. `activeOn`/`hides` (#2698): when it turns active, and whether it hides its people then. */
|
|
10
|
+
export interface PackFurniture { id: string; sprite: PackStrip; x: number; y: number; depth: number | "y"; activeWhen: string[] | null; activeSprite: PackStrip | null;
|
|
11
|
+
activeFor?: string[] | null; loop?: boolean; activeOn?: "state" | "seated"; hides?: boolean; label: string | null }
|
|
9
12
|
export interface CheckedPack {
|
|
10
13
|
format: 1; id: string; scale: number; background?: string; foreground?: string; matte?: string;
|
|
11
14
|
areas: Record<string, PackArea>; stateZones: Record<string, string> | null; door: PackPoint;
|
|
@@ -20,6 +23,8 @@ export interface LayoutOverrides {
|
|
|
20
23
|
furniture?: Record<string, [number, number]>;
|
|
21
24
|
/** #2665: an item's night place (an item with a `night` only). */
|
|
22
25
|
furnitureNight?: Record<string, [number, number]>;
|
|
26
|
+
/** #2691: a seat's reservation by area and slot ("0", "1", …), in place of the pack's; `for: []` is none. */
|
|
27
|
+
seatFor?: Record<string, Record<string, { for: string[]; lend: boolean }>>;
|
|
23
28
|
door?: [number, number];
|
|
24
29
|
waypoints?: Array<{ id: string; x: number; y: number }>;
|
|
25
30
|
edges?: Array<[string, string]>;
|
|
@@ -38,6 +43,7 @@ export function packStrips(pack: CheckedPack): Array<{ at: string; strip: PackSt
|
|
|
38
43
|
export function checkLayoutOverrides(x: unknown): { ok: true; overrides: LayoutOverrides } | { ok: false; error: string };
|
|
39
44
|
export function applyOverrides(pack: CheckedPack, ov: LayoutOverrides | null | undefined): { ok: true; pack: CheckedPack } | { ok: false; error: string };
|
|
40
45
|
export function fitNight<T extends LayoutOverrides | null | undefined>(pack: CheckedPack, ov: T): T;
|
|
46
|
+
export function fitSeatFor<T extends LayoutOverrides | null | undefined>(pack: CheckedPack, ov: T): T;
|
|
41
47
|
export function overridesBetween(pack: CheckedPack, edited: CheckedPack): LayoutOverrides | null;
|
|
42
48
|
export function poseStrip(set: Record<string, PackStrip> | null | undefined, pose: string, heading?: { dx: number; dy: number } | null): { strip: PackStrip; mirror: boolean } | null;
|
|
43
49
|
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
|
|
@@ -35,7 +42,7 @@
|
|
|
35
42
|
// always fit (graph edges laid out across and up/down put the
|
|
36
43
|
// corners on the aisles); default false
|
|
37
44
|
// "furniture": [ { "id": "rack", "sprite": STRIP, "x": 1020, "y": 140, "depth": 2,
|
|
38
|
-
// "activeWhen": "crashed", "activeSprite": STRIP, "activeFor": "atlas", "loop": true, "label": "Rack",
|
|
45
|
+
// "activeWhen": "crashed", "activeSprite": STRIP, "activeFor": "atlas", "loop": true, "activeOn": "seated", "hides": true, "label": "Rack",
|
|
39
46
|
// "night": { "sprite": STRIP, "x": 300, "y": 420 },
|
|
40
47
|
// "walk": { "walk": STRIP, "walkDown": STRIP, "walkUp": STRIP, "walkSide": STRIP } } ],
|
|
41
48
|
// "label" (#2574): a name tag under it, like a person's
|
|
@@ -55,7 +62,14 @@
|
|
|
55
62
|
// strip (or `activeSprite`); otherwise it holds the first frame —
|
|
56
63
|
// or, with "loop": true (#2570), plays its strip all along.
|
|
57
64
|
// "activeFor" (an instance's name, or a list): only their state
|
|
58
|
-
// counts, not anyone's (a room several people take turns in)
|
|
65
|
+
// counts, not anyone's (a room several people take turns in).
|
|
66
|
+
// "activeOn" (#2698): "state" (the default) turns it active as the
|
|
67
|
+
// state changes; "seated" only once one of them in that state is
|
|
68
|
+
// at the seat zones gave them, not walking — back as they walk off.
|
|
69
|
+
// "hides": true (#2698, only with activeFor): while active, the
|
|
70
|
+
// people it is active for are not drawn (walking, they always
|
|
71
|
+
// are); their name tags and bubbles are drawn on it, in one row at
|
|
72
|
+
// its tag point, and hovering or clicking it hits them
|
|
59
73
|
// "characters": { "default": { "working": STRIP, "idle": STRIP, "paused": STRIP, "needs": STRIP, "walk": STRIP,
|
|
60
74
|
// "walkDown": STRIP, "walkUp": STRIP, "walkSide": STRIP },
|
|
61
75
|
// "fox": { … } }, "default" with at least "idle" is required; a missing pose is idle.
|
|
@@ -103,6 +117,9 @@
|
|
|
103
117
|
// "seats": { "desks": [[120, 220], …] }, an area's seats: as many as the pack's, each inside the area's rect
|
|
104
118
|
// "furniture": { "rack": [1000, 150] }, an item's (x, y)
|
|
105
119
|
// "furnitureNight": { "elder": [300, 420] }, an item's night place (#2665: an item with a "night" only)
|
|
120
|
+
// "seatFor": { "desks": { "0": { "for": ["atlas"], "lend": false } } },
|
|
121
|
+
// a seat's reservation by area and slot (#2691), in place of the
|
|
122
|
+
// pack's own for that seat; "for": [] is none (lends nothing)
|
|
106
123
|
// "door": [640, 700],
|
|
107
124
|
// "waypoints": [{ "id": "hall", "x": 640, "y": 600 }, …], "edges": [["hall", "east"], …] }
|
|
108
125
|
// the walkable graph, replaced whole (edges need waypoints)
|
|
@@ -112,8 +129,8 @@
|
|
|
112
129
|
// seat outside its area, an area or furniture the pack does not have, or a seat count that differs (the host checks
|
|
113
130
|
// that too, for a pack it keeps). A night place goes on the item's `night` (its x, y), so at bedtime (nightFurniture)
|
|
114
131
|
// 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).
|
|
132
|
+
// and `seatFor` (#2691) beside the offices on disk, never inside them: an older host's stricter check would drop the
|
|
133
|
+
// office (#2665, web-office-store.ts).
|
|
117
134
|
|
|
118
135
|
export const LOGICAL = { w: 1280, h: 720 };
|
|
119
136
|
/** The people's depth: furniture below it is drawn under them, the rest over them. */
|
|
@@ -133,7 +150,7 @@ const RESERVED = new Set(["cozy"]);
|
|
|
133
150
|
const ID = /^[A-Za-z0-9_-]{1,40}$/;
|
|
134
151
|
const SRC = /^[A-Za-z0-9._/-]{1,200}$/;
|
|
135
152
|
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 };
|
|
153
|
+
const LIMIT = { areas: 32, seats: 64, furniture: 128, waypoints: 512, edges: 2048, sets: 64, byInstance: 500, lines: 12, line: 40, seatFor: 16 };
|
|
137
154
|
|
|
138
155
|
class Refused extends Error { constructor(at, why) { super(`${at}: ${why}`); } }
|
|
139
156
|
/** `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 +201,33 @@ function walkOf(v, at, scale) {
|
|
|
184
201
|
if (!w.walk) throw new Refused(`${at}.walk`, "required");
|
|
185
202
|
return w;
|
|
186
203
|
}
|
|
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) {
|
|
204
|
+
/** Whose state an item's activeWhen watches (#2570): an instance's name, or a list of up to `max` (8). */
|
|
205
|
+
function names(v, at, max = 8) {
|
|
189
206
|
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,
|
|
207
|
+
return Array.isArray(v) ? list(v, max, at).map((s, i) => one(s, `${at}[${i}]`)) : [one(v, at)];
|
|
208
|
+
}
|
|
209
|
+
/** Whom a seat is reserved for (#2691): instances' names (as names()), up to LIMIT.seatFor, none twice; `empty` may be []. */
|
|
210
|
+
function owners(v, at, empty = false) {
|
|
211
|
+
const out = names(v, at, LIMIT.seatFor);
|
|
212
|
+
if (!out.length && !empty) throw new Refused(at, "at least one instance");
|
|
213
|
+
if (new Set(out).size !== out.length) throw new Refused(at, "a name twice");
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* A seat (#2691): [x, y], or { x, y, for, lend } — reserved for `for` (lent to anyone while they are all elsewhere when
|
|
218
|
+
* `lend`). Checked: { x, y }, with `for` and `lend` only when reserved, so a plain seat is what it always was.
|
|
219
|
+
*/
|
|
220
|
+
function seatOf(v, at, rect) {
|
|
221
|
+
let p = null, extra = {};
|
|
222
|
+
if (isObj(v)) {
|
|
223
|
+
keys(v, ["x", "y", "for", "lend"], at);
|
|
224
|
+
p = { x: num(v.x, 0, LOGICAL.w, `${at}.x`), y: num(v.y, 0, LOGICAL.h, `${at}.y`) };
|
|
225
|
+
if (v.lend !== undefined && typeof v.lend !== "boolean") throw new Refused(`${at}.lend`, "true or false");
|
|
226
|
+
if (v.lend !== undefined && v.for === undefined) throw new Refused(`${at}.lend`, "only with for");
|
|
227
|
+
if (v.for !== undefined) extra = { for: owners(v.for, `${at}.for`), lend: v.lend === true };
|
|
228
|
+
} else p = point(v, at);
|
|
229
|
+
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");
|
|
230
|
+
return { ...p, ...extra };
|
|
191
231
|
}
|
|
192
232
|
function strips(v, names, at, scale) {
|
|
193
233
|
obj(v, at);
|
|
@@ -223,11 +263,7 @@ export function checkSkinPack(x, { kept = false } = {}) {
|
|
|
223
263
|
if (!Array.isArray(a.rect) || a.rect.length !== 4) throw new Refused(`${at}.rect`, "[x, y, w, h]");
|
|
224
264
|
const [rx, ry] = [num(a.rect[0], 0, LOGICAL.w, `${at}.rect[0]`), num(a.rect[1], 0, LOGICAL.h, `${at}.rect[1]`)];
|
|
225
265
|
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
|
-
});
|
|
266
|
+
const seats = list(a.seats, LIMIT.seats, `${at}.seats`).map((s, i) => seatOf(s, `${at}.seats[${i}]`, rect));
|
|
231
267
|
if (!seats.length) throw new Refused(`${at}.seats`, "at least one seat");
|
|
232
268
|
areas[name] = { name, rect, seats };
|
|
233
269
|
}
|
|
@@ -259,17 +295,22 @@ export function checkSkinPack(x, { kept = false } = {}) {
|
|
|
259
295
|
const fids = new Set();
|
|
260
296
|
const furniture = list(x.furniture ?? [], LIMIT.furniture, "furniture").map((f, i) => {
|
|
261
297
|
const at = `furniture[${i}]`;
|
|
262
|
-
obj(f, at); keys(f, ["id", "sprite", "x", "y", "depth", "activeWhen", "activeSprite", "activeFor", "loop", "label", "night", "walk"], at);
|
|
298
|
+
obj(f, at); keys(f, ["id", "sprite", "x", "y", "depth", "activeWhen", "activeSprite", "activeFor", "loop", "activeOn", "hides", "label", "night", "walk"], at);
|
|
263
299
|
if (!nameOk(f.id) || fids.has(f.id)) throw new Refused(`${at}.id`, "a new name of letters, digits, _ or - (not an inherited one)");
|
|
264
300
|
fids.add(f.id);
|
|
265
301
|
if (f.activeSprite !== undefined && f.activeWhen === undefined) throw new Refused(`${at}.activeSprite`, "only with activeWhen");
|
|
266
302
|
if (f.activeFor !== undefined && f.activeWhen === undefined) throw new Refused(`${at}.activeFor`, "only with activeWhen");
|
|
267
303
|
if (f.loop !== undefined && typeof f.loop !== "boolean") throw new Refused(`${at}.loop`, "true or false");
|
|
304
|
+
if (f.activeOn !== undefined && f.activeOn !== "state" && f.activeOn !== "seated") throw new Refused(`${at}.activeOn`, '"state" or "seated"');
|
|
305
|
+
if (f.activeOn !== undefined && f.activeWhen === undefined) throw new Refused(`${at}.activeOn`, "only with activeWhen");
|
|
306
|
+
if (f.hides !== undefined && typeof f.hides !== "boolean") throw new Refused(`${at}.hides`, "true or false");
|
|
307
|
+
if (f.hides !== undefined && f.activeFor === undefined) throw new Refused(`${at}.hides`, "only with activeFor");
|
|
268
308
|
if (f.label !== undefined && (typeof f.label !== "string" || !f.label.trim() || [...f.label].length > LIMIT.line || /[\u0000-\u001f\u007f]/.test(f.label))) throw new Refused(`${at}.label`, `1–${LIMIT.line} characters`);
|
|
269
309
|
return { id: f.id, sprite: checkStrip(f.sprite, `${at}.sprite`, scale), x: num(f.x, 0, LOGICAL.w, `${at}.x`), y: num(f.y, 0, LOGICAL.h, `${at}.y`),
|
|
270
310
|
depth: f.depth === "y" ? "y" : depth(f.depth ?? 0, `${at}.depth`), activeWhen: f.activeWhen === undefined ? null : states(f.activeWhen, `${at}.activeWhen`),
|
|
271
311
|
activeSprite: f.activeSprite === undefined ? null : checkStrip(f.activeSprite, `${at}.activeSprite`, scale),
|
|
272
|
-
activeFor: f.activeFor === undefined ? null : names(f.activeFor, `${at}.activeFor`), loop: f.loop === true,
|
|
312
|
+
activeFor: f.activeFor === undefined ? null : names(f.activeFor, `${at}.activeFor`), loop: f.loop === true,
|
|
313
|
+
activeOn: f.activeOn === "seated" ? "seated" : "state", hides: f.hides === true, label: f.label === undefined ? null : f.label.trim(), night: nightOf(f.night, `${at}.night`, scale),
|
|
273
314
|
walk: walkOf(f.walk, `${at}.walk`, scale) };
|
|
274
315
|
}).sort((a, b) => order(a) - order(b));
|
|
275
316
|
const sets = obj(x.characters, "characters");
|
|
@@ -341,11 +382,17 @@ export function packStrips(pack) {
|
|
|
341
382
|
// ── Layout overrides ──
|
|
342
383
|
|
|
343
384
|
const OV_LIMIT = { areas: LIMIT.areas, seats: LIMIT.seats, furniture: LIMIT.furniture, waypoints: LIMIT.waypoints, edges: LIMIT.edges };
|
|
385
|
+
/** A seat's slot in `seatFor` (#2691): its index in the area's order, as a key ("0", "1", …). */
|
|
386
|
+
const SLOT = /^(0|[1-9][0-9]{0,2})$/;
|
|
387
|
+
/** A seat's reservation as an override keeps it: { for, lend }, or null (not reserved). */
|
|
388
|
+
const reservedOf = (s) => (s.for && s.for.length ? { for: [...s.for], lend: s.lend === true } : null);
|
|
389
|
+
/** A seat with reservation `r` ({ for, lend }; for [] or null: none) in place of its own. */
|
|
390
|
+
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
391
|
/** An office's layout overrides, strictly (their shape and ranges): { ok: true, overrides } or { ok: false, error }. */
|
|
345
392
|
export function checkLayoutOverrides(x) {
|
|
346
393
|
try {
|
|
347
394
|
obj(x, "overrides");
|
|
348
|
-
keys(x, ["pack", "seats", "furniture", "furnitureNight", "door", "waypoints", "edges"], "overrides");
|
|
395
|
+
keys(x, ["pack", "seats", "furniture", "furnitureNight", "seatFor", "door", "waypoints", "edges"], "overrides");
|
|
349
396
|
if (typeof x.pack !== "string" || !ID.test(x.pack)) throw new Refused("overrides.pack", "1–40 letters, digits, _ or -");
|
|
350
397
|
const out = { pack: x.pack };
|
|
351
398
|
if (x.seats !== undefined) {
|
|
@@ -380,6 +427,26 @@ export function checkLayoutOverrides(x) {
|
|
|
380
427
|
out.furnitureNight[id] = [q.x, q.y];
|
|
381
428
|
}
|
|
382
429
|
}
|
|
430
|
+
if (x.seatFor !== undefined) {
|
|
431
|
+
obj(x.seatFor, "overrides.seatFor");
|
|
432
|
+
if (Object.keys(x.seatFor).length > OV_LIMIT.areas) throw new Refused("overrides.seatFor", `at most ${OV_LIMIT.areas} areas`);
|
|
433
|
+
out.seatFor = {};
|
|
434
|
+
for (const [area, slots] of Object.entries(x.seatFor)) {
|
|
435
|
+
const at = `overrides.seatFor.${area}`;
|
|
436
|
+
if (!nameOk(area)) throw new Refused(at, "an area's name");
|
|
437
|
+
obj(slots, at);
|
|
438
|
+
if (Object.keys(slots).length > OV_LIMIT.seats) throw new Refused(at, `at most ${OV_LIMIT.seats} seats`);
|
|
439
|
+
out.seatFor[area] = {};
|
|
440
|
+
for (const [slot, r] of Object.entries(slots)) {
|
|
441
|
+
const here = `${at}.${slot}`;
|
|
442
|
+
if (!SLOT.test(slot) || Number(slot) >= OV_LIMIT.seats) throw new Refused(here, `a seat's number, 0 to ${OV_LIMIT.seats - 1}`);
|
|
443
|
+
obj(r, here); keys(r, ["for", "lend"], here);
|
|
444
|
+
if (!Array.isArray(r.for)) throw new Refused(`${here}.for`, "a list");
|
|
445
|
+
if (r.lend !== undefined && typeof r.lend !== "boolean") throw new Refused(`${here}.lend`, "true or false");
|
|
446
|
+
out.seatFor[area][slot] = { for: owners(r.for, `${here}.for`, true), lend: r.lend === true };
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
383
450
|
if (x.door !== undefined) { const q = point(x.door, "overrides.door"); out.door = [q.x, q.y]; }
|
|
384
451
|
if (x.edges !== undefined && x.waypoints === undefined) throw new Refused("overrides.edges", "only with waypoints");
|
|
385
452
|
if (x.waypoints !== undefined) {
|
|
@@ -422,9 +489,15 @@ export function applyOverrides(pack, ov) {
|
|
|
422
489
|
const r = a.rect;
|
|
423
490
|
areas[name] = { ...a, seats: pts.map(([x, y], i) => {
|
|
424
491
|
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 };
|
|
492
|
+
return { ...a.seats[i], x, y }; // its reservation kept (#2691)
|
|
426
493
|
}) };
|
|
427
494
|
}
|
|
495
|
+
for (const [name, slots] of Object.entries(ov.seatFor || {})) {
|
|
496
|
+
const a = own(areas, name), at = `overrides.seatFor.${name}`;
|
|
497
|
+
if (!a) throw new Refused(at, "an area the pack defines");
|
|
498
|
+
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})`);
|
|
499
|
+
areas[name] = { ...a, seats: a.seats.map((s, i) => { const v = own(slots, String(i)); return v ? reserve(s, v) : s; }) };
|
|
500
|
+
}
|
|
428
501
|
const fids = new Set(pack.furniture.map((f) => f.id));
|
|
429
502
|
for (const id of Object.keys(ov.furniture || {})) if (!fids.has(id)) throw new Refused(`overrides.furniture.${id}`, "an item the pack has");
|
|
430
503
|
const nids = new Set(pack.furniture.filter((f) => f.night).map((f) => f.id));
|
|
@@ -457,6 +530,26 @@ export function fitNight(pack, ov) {
|
|
|
457
530
|
return kept.length ? { ...rest, furnitureNight: Object.fromEntries(kept) } : rest;
|
|
458
531
|
}
|
|
459
532
|
|
|
533
|
+
/**
|
|
534
|
+
* #2691: overrides with only the reservations `pack` can take (an area it has, a seat that area has): a pack changed
|
|
535
|
+
* since drops that entry, never the rest of the layout (as fitNight; applyOverrides stays strict).
|
|
536
|
+
*/
|
|
537
|
+
export function fitSeatFor(pack, ov) {
|
|
538
|
+
if (!ov || !ov.seatFor || ov.pack !== pack.id) return ov;
|
|
539
|
+
let dropped = false;
|
|
540
|
+
const kept = {};
|
|
541
|
+
for (const [area, slots] of Object.entries(ov.seatFor)) {
|
|
542
|
+
const a = own(pack.areas, area);
|
|
543
|
+
const ok = Object.entries(slots).filter(([slot]) => a && !!a.seats[Number(slot)]);
|
|
544
|
+
// An area the pack no longer has is dropped even with no slots under it (`{}`: 0 kept of 0, yet applyOverrides refuses it).
|
|
545
|
+
if (!a || ok.length !== Object.keys(slots).length) dropped = true;
|
|
546
|
+
if (ok.length) kept[area] = Object.fromEntries(ok);
|
|
547
|
+
}
|
|
548
|
+
if (!dropped) return ov;
|
|
549
|
+
const { seatFor: _gone, ...rest } = ov;
|
|
550
|
+
return Object.keys(kept).length ? { ...rest, seatFor: kept } : rest;
|
|
551
|
+
}
|
|
552
|
+
|
|
460
553
|
/**
|
|
461
554
|
* What an edited layout keeps as overrides: only the parts that differ from the pack's own (`edited` is a pack with
|
|
462
555
|
* the same areas and furniture, positions moved); null when nothing does (the office goes back to the pack's).
|
|
@@ -465,8 +558,13 @@ export function overridesBetween(pack, edited) {
|
|
|
465
558
|
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
466
559
|
const ov = { pack: pack.id };
|
|
467
560
|
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(
|
|
561
|
+
const e = own(edited.areas, name), xy = (s) => [s.x, s.y];
|
|
562
|
+
if (e && !same(a.seats.map(xy), e.seats.map(xy))) (ov.seats ||= {})[name] = e.seats.map(xy);
|
|
563
|
+
// #2691: a seat whose reservation differs from the pack's (none where the pack has one: for []).
|
|
564
|
+
if (e) e.seats.forEach((s, i) => {
|
|
565
|
+
const was = a.seats[i] ? reservedOf(a.seats[i]) : null, now = reservedOf(s);
|
|
566
|
+
if (a.seats[i] && !same(was, now)) ((ov.seatFor ||= {})[name] ||= {})[String(i)] = now || { for: [], lend: false };
|
|
567
|
+
});
|
|
470
568
|
}
|
|
471
569
|
const at = new Map(pack.furniture.map((f) => [f.id, f]));
|
|
472
570
|
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).
|