@songsid/agend 2.3.2-beta.8 → 2.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/README.zh-TW.md +6 -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/auth-api.js +2 -0
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/claude-code.js +23 -1
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex-session-lookup.d.ts +15 -0
- package/dist/backend/codex-session-lookup.js +58 -0
- package/dist/backend/codex-session-lookup.js.map +1 -1
- package/dist/backend/codex.js +15 -4
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/kiro-classic-archive.d.ts +57 -0
- package/dist/backend/kiro-classic-archive.js +622 -0
- package/dist/backend/kiro-classic-archive.js.map +1 -0
- package/dist/backend/kiro-classic-links.d.ts +24 -0
- package/dist/backend/kiro-classic-links.js +135 -0
- package/dist/backend/kiro-classic-links.js.map +1 -0
- package/dist/backend/kiro-identity.d.ts +100 -1
- package/dist/backend/kiro-identity.js +262 -31
- 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-reconcile-store.d.ts +27 -0
- package/dist/backend/kiro-reconcile-store.js +344 -0
- package/dist/backend/kiro-reconcile-store.js.map +1 -0
- package/dist/backend/kiro-v2-store-worker.js +35 -1
- package/dist/backend/kiro-v2-store-worker.js.map +1 -1
- package/dist/backend/kiro-v2-store.d.ts +31 -1
- package/dist/backend/kiro-v2-store.js +22 -12
- package/dist/backend/kiro-v2-store.js.map +1 -1
- package/dist/backend/kiro.d.ts +124 -1
- package/dist/backend/kiro.js +553 -29
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +102 -0
- package/dist/backend/types.js +9 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/cache-service.d.ts +13 -1
- package/dist/cache-service.js +45 -2
- package/dist/cache-service.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 +40 -5
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/tool-router.js +2 -1
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/classic-channel-manager.d.ts +6 -0
- package/dist/classic-channel-manager.js +25 -0
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +50 -4
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.d.ts +7 -1
- package/dist/config-validator.js +17 -8
- 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 +66 -0
- package/dist/daemon.js +421 -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/doctor.js +2 -1
- package/dist/doctor.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 +90 -3
- package/dist/fleet-manager.js +569 -41
- package/dist/fleet-manager.js.map +1 -1
- package/dist/forged-envelope-scan.d.ts +9 -0
- package/dist/forged-envelope-scan.js +30 -5
- package/dist/forged-envelope-scan.js.map +1 -1
- package/dist/general-instance.d.ts +24 -0
- package/dist/general-instance.js +53 -0
- package/dist/general-instance.js.map +1 -1
- package/dist/general-knowledge/skills/docker-install/SKILL.md +25 -0
- package/dist/general-knowledge/skills/fleet-config/SKILL.md +5 -3
- package/dist/general-knowledge/skills/fleet-restart/SKILL.md +1 -1
- package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +34 -0
- package/dist/general-knowledge/skills/session-management/SKILL.md +1 -1
- package/dist/general-knowledge/skills/tui-effort/SKILL.md +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +32 -0
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +32 -0
- package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +60 -18
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +60 -18
- package/dist/instance-config-impact.js +3 -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 +19 -2
- package/dist/kiro-migrate.js +133 -35
- package/dist/kiro-migrate.js.map +1 -1
- package/dist/kiro-reconcile.d.ts +21 -0
- package/dist/kiro-reconcile.js +88 -0
- package/dist/kiro-reconcile.js.map +1 -0
- 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 +19 -0
- package/dist/kiro-tui-forensic.js +124 -0
- package/dist/kiro-tui-forensic.js.map +1 -0
- package/dist/locale.js +198 -0
- package/dist/locale.js.map +1 -1
- package/dist/log-service.js +2 -2
- 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-levels.d.ts +64 -0
- package/dist/office-levels.js +66 -0
- package/dist/office-levels.js.map +1 -0
- package/dist/office-share-page.d.ts +3 -1
- package/dist/office-share-page.js +8 -3
- package/dist/office-share-page.js.map +1 -1
- package/dist/office-share-service.d.ts +6 -3
- package/dist/office-share-service.js +12 -8
- package/dist/office-share-service.js.map +1 -1
- package/dist/office-share.d.ts +80 -13
- package/dist/office-share.js +145 -25
- package/dist/office-share.js.map +1 -1
- package/dist/outbound-handlers.js +20 -1
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +47 -2
- package/dist/outbound-schemas.js +41 -2
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +1 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/quickstart-api.d.ts +39 -1
- package/dist/quickstart-api.js +115 -17
- package/dist/quickstart-api.js.map +1 -1
- package/dist/remote-card-answer.d.ts +83 -0
- package/dist/remote-card-answer.js +208 -0
- package/dist/remote-card-answer.js.map +1 -0
- 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/scheduler/task-board.d.ts +28 -6
- package/dist/scheduler/task-board.js +66 -11
- package/dist/scheduler/task-board.js.map +1 -1
- package/dist/service-installer.d.ts +13 -0
- package/dist/service-installer.js +15 -0
- package/dist/service-installer.js.map +1 -1
- package/dist/service-owner.d.ts +25 -0
- package/dist/service-owner.js +43 -2
- package/dist/service-owner.js.map +1 -1
- package/dist/settings-api.d.ts +2 -0
- package/dist/settings-api.js +18 -12
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +10 -5
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +7 -4
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-instance-patch.js +1 -1
- package/dist/settings-instance-patch.js.map +1 -1
- package/dist/setup-form.d.ts +2 -2
- package/dist/setup-form.js +139 -93
- package/dist/setup-form.js.map +1 -1
- package/dist/setup-i18n.d.ts +25 -0
- package/dist/setup-i18n.js +226 -0
- package/dist/setup-i18n.js.map +1 -0
- package/dist/task-board-waker.js +7 -2
- package/dist/task-board-waker.js.map +1 -1
- package/dist/tool-permissions.js +18 -0
- package/dist/tool-permissions.js.map +1 -1
- package/dist/transcript-sources.d.ts +1 -0
- package/dist/transcript-sources.js +20 -3
- package/dist/transcript-sources.js.map +1 -1
- package/dist/types.d.ts +10 -0
- package/dist/ui/backend-signin.js +4 -3
- package/dist/ui/chat-store.js +3 -2
- package/dist/ui/chat-strings.js +2 -2
- package/dist/ui/fleet-office-card.js +114 -0
- package/dist/ui/fleet-office-editor.js +81 -9
- package/dist/ui/fleet-office-layout.js +87 -10
- package/dist/ui/fleet-office-levels.js +105 -0
- package/dist/ui/fleet-office-model.js +9 -1
- package/dist/ui/fleet-office-pack.d.ts +18 -3
- package/dist/ui/fleet-office-pack.js +217 -23
- package/dist/ui/fleet-office-skin.js +39 -3
- package/dist/ui/fleet-office-store.js +74 -3
- package/dist/ui/fleet-office-view.js +8 -0
- package/dist/ui/fleet-office-zones.js +102 -19
- package/dist/ui/fleet-office.js +323 -50
- package/dist/ui/fleet-org.js +2 -0
- package/dist/ui/fleet-task-card.js +2 -2
- package/dist/ui/fleet-task-common.js +30 -7
- package/dist/ui/fleet-task-work.js +2 -2
- package/dist/ui/fleet-tasks.js +10 -11
- package/dist/ui/office-share-dialog.js +31 -7
- package/dist/ui/office-share-page.js +50 -1
- package/dist/ui/panel-chat.js +14 -2
- package/dist/ui/panel-log.js +4 -4
- package/dist/ui/panel-needs.js +1 -1
- package/dist/ui/panel-settings.js +16 -8
- package/dist/ui/settings-discord.js +3 -2
- package/dist/ui/settings-guests.js +17 -7
- package/dist/ui/settings-model.js +17 -9
- package/dist/ui/settings-registry.js +1 -0
- package/dist/ui/settings-strings.js +16 -4
- package/dist/ui/settings-token.js +3 -2
- package/dist/ui/settings-wizard.js +25 -9
- package/dist/ui/shared/app-i18n.js +2 -2
- package/dist/ui/shared/app-stream.js +1 -1
- package/dist/ui/shared/app.css +74 -2
- package/dist/ui/shared/setup-host-text.js +107 -0
- package/dist/ui/structured-signin.js +89 -38
- package/dist/usage-rollup.d.ts +34 -3
- package/dist/usage-rollup.js +56 -3
- package/dist/usage-rollup.js.map +1 -1
- package/dist/web-api.d.ts +10 -1
- package/dist/web-api.js +207 -17
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.js +1 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-office-acts.d.ts +5 -2
- package/dist/web-office-acts.js +9 -4
- 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 +241 -0
- package/dist/web-office-pack-tools.js +785 -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 +324 -33
- package/dist/web-office-skins.js.map +1 -1
- package/dist/web-office-store.d.ts +47 -4
- package/dist/web-office-store.js +149 -15
- package/dist/web-office-store.js.map +1 -1
- package/dist/web-settings-patch.d.ts +3 -1
- package/dist/web-settings-patch.js +4 -1
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/workflow-templates/default.md +1 -1
- package/package.json +2 -2
- package/templates/windows-shortcuts.ps1 +18 -17
|
@@ -24,10 +24,21 @@
|
|
|
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
|
|
30
37
|
// "door": [640, 700], optional: where someone new walks in from (default: bottom middle)
|
|
38
|
+
// "mailDrop": [640, 420], optional (#2736): where a letter from another office lands (more
|
|
39
|
+
// waiting step GAP px right of it, past any seat); default: the
|
|
40
|
+
// waypoint nearest the middle off every seat (fleet-office-zones.js
|
|
41
|
+
// dropPoint)
|
|
31
42
|
// "waypoints": [{ "id": "hall", "x": 640, "y": 600 }, …], "edges": [["hall", "east"], …],
|
|
32
43
|
// optional: the walkable graph (else people walk straight)
|
|
33
44
|
// "straight": true, optional (#2634): every walk goes across, then up or down — each
|
|
@@ -35,7 +46,7 @@
|
|
|
35
46
|
// always fit (graph edges laid out across and up/down put the
|
|
36
47
|
// corners on the aisles); default false
|
|
37
48
|
// "furniture": [ { "id": "rack", "sprite": STRIP, "x": 1020, "y": 140, "depth": 2,
|
|
38
|
-
// "activeWhen": "crashed", "activeSprite": STRIP, "activeFor": "atlas", "loop": true, "label": "Rack",
|
|
49
|
+
// "activeWhen": "crashed", "activeSprite": STRIP, "activeFor": "atlas", "loop": true, "activeOn": "seated", "hides": true, "label": "Rack",
|
|
39
50
|
// "night": { "sprite": STRIP, "x": 300, "y": 420 },
|
|
40
51
|
// "walk": { "walk": STRIP, "walkDown": STRIP, "walkUp": STRIP, "walkSide": STRIP } } ],
|
|
41
52
|
// "label" (#2574): a name tag under it, like a person's
|
|
@@ -55,7 +66,14 @@
|
|
|
55
66
|
// strip (or `activeSprite`); otherwise it holds the first frame —
|
|
56
67
|
// or, with "loop": true (#2570), plays its strip all along.
|
|
57
68
|
// "activeFor" (an instance's name, or a list): only their state
|
|
58
|
-
// counts, not anyone's (a room several people take turns in)
|
|
69
|
+
// counts, not anyone's (a room several people take turns in).
|
|
70
|
+
// "activeOn" (#2698): "state" (the default) turns it active as the
|
|
71
|
+
// state changes; "seated" only once one of them in that state is
|
|
72
|
+
// at the seat zones gave them, not walking — back as they walk off.
|
|
73
|
+
// "hides": true (#2698, only with activeFor): while active, the
|
|
74
|
+
// people it is active for are not drawn (walking, they always
|
|
75
|
+
// are); their name tags and bubbles are drawn on it, in one row at
|
|
76
|
+
// its tag point, and hovering or clicking it hits them
|
|
59
77
|
// "characters": { "default": { "working": STRIP, "idle": STRIP, "paused": STRIP, "needs": STRIP, "walk": STRIP,
|
|
60
78
|
// "walkDown": STRIP, "walkUp": STRIP, "walkSide": STRIP },
|
|
61
79
|
// "fox": { … } }, "default" with at least "idle" is required; a missing pose is idle.
|
|
@@ -73,6 +91,13 @@
|
|
|
73
91
|
// ≤ 40 characters each), in place of the page's own
|
|
74
92
|
// "room": { "floor": "floor.png", "wall": "wall.png" }, "desk": { "back": …, "front": …, "lit": … }
|
|
75
93
|
// optional: for the desks layout (a pack without stateZones)
|
|
94
|
+
// "levels": { optional (#2661): the Office levels' words
|
|
95
|
+
// "titles": [{ "from": 1, "name": "Intern" }, …], the title from each level up (≤ 32, `from` 0–100000
|
|
96
|
+
// rising, a name 1–24 characters), in place of the page's own
|
|
97
|
+
// "fixed": { "atlas": { "title": "Founder", "hideLevel": true } } }
|
|
98
|
+
// a title pinned on an instance (or an NPC: a furniture item's
|
|
99
|
+
// id) whatever its level (≤ 500); hideLevel: its badge is a mark,
|
|
100
|
+
// its card has no "Lv n" or XP bar
|
|
76
101
|
// }
|
|
77
102
|
//
|
|
78
103
|
// A STRIP is { "src": "idle.png", "frames": 4, "w": 32, "h": 48, "fps": 4, "ax": 16, "ay": 44 }: `frames` frames of
|
|
@@ -102,14 +127,21 @@
|
|
|
102
127
|
// { "pack": "cozy", the pack they were made on (on another pack they are ignored)
|
|
103
128
|
// "seats": { "desks": [[120, 220], …] }, an area's seats: as many as the pack's, each inside the area's rect
|
|
104
129
|
// "furniture": { "rack": [1000, 150] }, an item's (x, y)
|
|
105
|
-
// "
|
|
130
|
+
// "furnitureNight": { "elder": [300, 420] }, an item's night place (#2665: an item with a "night" only)
|
|
131
|
+
// "seatFor": { "desks": { "0": { "for": ["atlas"], "lend": false } } },
|
|
132
|
+
// a seat's reservation by area and slot (#2691), in place of the
|
|
133
|
+
// pack's own for that seat; "for": [] is none (lends nothing)
|
|
134
|
+
// "door": [640, 700], "mailDrop": [640, 420], (#2736: kept beside the offices on disk, as furnitureNight)
|
|
106
135
|
// "waypoints": [{ "id": "hall", "x": 640, "y": 600 }, …], "edges": [["hall", "east"], …] }
|
|
107
136
|
// the walkable graph, replaced whole (edges need waypoints)
|
|
108
137
|
//
|
|
109
138
|
// Every coordinate is on the same 1280×720 logical canvas, in the same ranges as the pack's. checkLayoutOverrides is
|
|
110
139
|
// the shape (the host checks it on every PUT /ui/offices); applyOverrides puts them on a checked pack, refusing a
|
|
111
140
|
// seat outside its area, an area or furniture the pack does not have, or a seat count that differs (the host checks
|
|
112
|
-
// that too, for a pack it keeps).
|
|
141
|
+
// that too, for a pack it keeps). A night place goes on the item's `night` (its x, y), so at bedtime (nightFurniture)
|
|
142
|
+
// it is the office's, else the pack's night x/y, else the item's (moved) day place. The host keeps `furnitureNight`
|
|
143
|
+
// and `seatFor` (#2691) beside the offices on disk, never inside them: an older host's stricter check would drop the
|
|
144
|
+
// office (#2665, web-office-store.ts).
|
|
113
145
|
|
|
114
146
|
export const LOGICAL = { w: 1280, h: 720 };
|
|
115
147
|
/** The people's depth: furniture below it is drawn under them, the rest over them. */
|
|
@@ -129,7 +161,7 @@ const RESERVED = new Set(["cozy"]);
|
|
|
129
161
|
const ID = /^[A-Za-z0-9_-]{1,40}$/;
|
|
130
162
|
const SRC = /^[A-Za-z0-9._/-]{1,200}$/;
|
|
131
163
|
const HEX = /^#[0-9a-f]{6}$/i;
|
|
132
|
-
const LIMIT = { areas: 32, seats: 64, furniture: 128, waypoints: 512, edges: 2048, sets: 64, byInstance: 500, lines: 12, line: 40 };
|
|
164
|
+
const LIMIT = { areas: 32, seats: 64, furniture: 128, waypoints: 512, edges: 2048, sets: 64, byInstance: 500, lines: 12, line: 40, seatFor: 16, titles: 32, title: 24, fixed: 500 };
|
|
133
165
|
|
|
134
166
|
class Refused extends Error { constructor(at, why) { super(`${at}: ${why}`); } }
|
|
135
167
|
/** `o[k]` when it is o's own property, else undefined: a name from a pack or a page never reaches Object.prototype. */
|
|
@@ -180,10 +212,33 @@ function walkOf(v, at, scale) {
|
|
|
180
212
|
if (!w.walk) throw new Refused(`${at}.walk`, "required");
|
|
181
213
|
return w;
|
|
182
214
|
}
|
|
183
|
-
/** Whose state an item's activeWhen watches (#2570): an instance's name, or a list of up to 8. */
|
|
184
|
-
function names(v, at) {
|
|
215
|
+
/** Whose state an item's activeWhen watches (#2570): an instance's name, or a list of up to `max` (8). */
|
|
216
|
+
function names(v, at, max = 8) {
|
|
185
217
|
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; };
|
|
186
|
-
return Array.isArray(v) ? list(v,
|
|
218
|
+
return Array.isArray(v) ? list(v, max, at).map((s, i) => one(s, `${at}[${i}]`)) : [one(v, at)];
|
|
219
|
+
}
|
|
220
|
+
/** Whom a seat is reserved for (#2691): instances' names (as names()), up to LIMIT.seatFor, none twice; `empty` may be []. */
|
|
221
|
+
function owners(v, at, empty = false) {
|
|
222
|
+
const out = names(v, at, LIMIT.seatFor);
|
|
223
|
+
if (!out.length && !empty) throw new Refused(at, "at least one instance");
|
|
224
|
+
if (new Set(out).size !== out.length) throw new Refused(at, "a name twice");
|
|
225
|
+
return out;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* A seat (#2691): [x, y], or { x, y, for, lend } — reserved for `for` (lent to anyone while they are all elsewhere when
|
|
229
|
+
* `lend`). Checked: { x, y }, with `for` and `lend` only when reserved, so a plain seat is what it always was.
|
|
230
|
+
*/
|
|
231
|
+
function seatOf(v, at, rect) {
|
|
232
|
+
let p = null, extra = {};
|
|
233
|
+
if (isObj(v)) {
|
|
234
|
+
keys(v, ["x", "y", "for", "lend"], at);
|
|
235
|
+
p = { x: num(v.x, 0, LOGICAL.w, `${at}.x`), y: num(v.y, 0, LOGICAL.h, `${at}.y`) };
|
|
236
|
+
if (v.lend !== undefined && typeof v.lend !== "boolean") throw new Refused(`${at}.lend`, "true or false");
|
|
237
|
+
if (v.lend !== undefined && v.for === undefined) throw new Refused(`${at}.lend`, "only with for");
|
|
238
|
+
if (v.for !== undefined) extra = { for: owners(v.for, `${at}.for`), lend: v.lend === true };
|
|
239
|
+
} else p = point(v, at);
|
|
240
|
+
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");
|
|
241
|
+
return { ...p, ...extra };
|
|
187
242
|
}
|
|
188
243
|
function strips(v, names, at, scale) {
|
|
189
244
|
obj(v, at);
|
|
@@ -195,6 +250,43 @@ function states(v, at) {
|
|
|
195
250
|
return Array.isArray(v) ? list(v, 8, at).map((s, i) => one(s, `${at}[${i}]`)) : [one(v, at)];
|
|
196
251
|
}
|
|
197
252
|
|
|
253
|
+
/** #2661: a title's words — 1–LIMIT.title characters on one line. */
|
|
254
|
+
function titleText(v, at) {
|
|
255
|
+
if (typeof v !== "string" || !v.trim() || [...v.trim()].length > LIMIT.title || /[\u0000-\u001f\u007f]/.test(v)) throw new Refused(at, `1–${LIMIT.title} characters`);
|
|
256
|
+
return v.trim();
|
|
257
|
+
}
|
|
258
|
+
/** #2661: a pack's `levels` (titles, fixed), checked; null when it has none. */
|
|
259
|
+
function levelsOf(v) {
|
|
260
|
+
if (v === undefined) return null;
|
|
261
|
+
obj(v, "levels");
|
|
262
|
+
keys(v, ["titles", "fixed"], "levels");
|
|
263
|
+
const out = {};
|
|
264
|
+
if (v.titles !== undefined) {
|
|
265
|
+
let last = -1;
|
|
266
|
+
out.titles = list(v.titles, LIMIT.titles, "levels.titles").map((t, i) => {
|
|
267
|
+
const at = `levels.titles[${i}]`;
|
|
268
|
+
obj(t, at); keys(t, ["from", "name"], at);
|
|
269
|
+
const from = int(t.from, 0, 100000, `${at}.from`);
|
|
270
|
+
if (from <= last) throw new Refused(`${at}.from`, "higher than the title before it");
|
|
271
|
+
last = from;
|
|
272
|
+
return { from, name: titleText(t.name, `${at}.name`) };
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
if (v.fixed !== undefined) {
|
|
276
|
+
obj(v.fixed, "levels.fixed");
|
|
277
|
+
if (Object.keys(v.fixed).length > LIMIT.fixed) throw new Refused("levels.fixed", `at most ${LIMIT.fixed}`);
|
|
278
|
+
out.fixed = {};
|
|
279
|
+
for (const [k, f] of Object.entries(v.fixed)) {
|
|
280
|
+
const at = `levels.fixed.${k}`;
|
|
281
|
+
if (inherited(k)) throw new Refused(at, "an instance's name (not an inherited one)");
|
|
282
|
+
obj(f, at); keys(f, ["title", "hideLevel"], at);
|
|
283
|
+
if (f.hideLevel !== undefined && typeof f.hideLevel !== "boolean") throw new Refused(`${at}.hideLevel`, "true or false");
|
|
284
|
+
out.fixed[k] = { title: titleText(f.title, `${at}.title`), hideLevel: f.hideLevel === true };
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return out;
|
|
288
|
+
}
|
|
289
|
+
|
|
198
290
|
/**
|
|
199
291
|
* A pack's manifest, strictly: { ok: true, pack } or { ok: false, error: "<where>: <why>" }. `kept`: a pack the host
|
|
200
292
|
* keeps already (read back, not uploaded), which may hold a RESERVED id.
|
|
@@ -202,7 +294,7 @@ function states(v, at) {
|
|
|
202
294
|
export function checkSkinPack(x, { kept = false } = {}) {
|
|
203
295
|
try {
|
|
204
296
|
obj(x, "pack");
|
|
205
|
-
keys(x, ["format", "id", "scale", "background", "foreground", "matte", "areas", "stateZones", "door", "waypoints", "edges", "straight", "furniture", "characters", "byInstance", "props", "bubbles", "room", "desk"], "pack");
|
|
297
|
+
keys(x, ["format", "id", "scale", "background", "foreground", "matte", "areas", "stateZones", "door", "mailDrop", "waypoints", "edges", "straight", "furniture", "characters", "byInstance", "props", "bubbles", "room", "desk", "levels"], "pack");
|
|
206
298
|
if (x.format !== 1) throw new Refused("format", "1");
|
|
207
299
|
if (typeof x.id !== "string" || !ID.test(x.id)) throw new Refused("id", "1–40 letters, digits, _ or -");
|
|
208
300
|
if (BUILT_IN.has(x.id) || (!kept && RESERVED.has(x.id))) throw new Refused("id", "a built-in skin's id");
|
|
@@ -219,11 +311,7 @@ export function checkSkinPack(x, { kept = false } = {}) {
|
|
|
219
311
|
if (!Array.isArray(a.rect) || a.rect.length !== 4) throw new Refused(`${at}.rect`, "[x, y, w, h]");
|
|
220
312
|
const [rx, ry] = [num(a.rect[0], 0, LOGICAL.w, `${at}.rect[0]`), num(a.rect[1], 0, LOGICAL.h, `${at}.rect[1]`)];
|
|
221
313
|
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]`) };
|
|
222
|
-
const seats = list(a.seats, LIMIT.seats, `${at}.seats`).map((s, i) => {
|
|
223
|
-
const p = point(s, `${at}.seats[${i}]`);
|
|
224
|
-
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");
|
|
225
|
-
return p;
|
|
226
|
-
});
|
|
314
|
+
const seats = list(a.seats, LIMIT.seats, `${at}.seats`).map((s, i) => seatOf(s, `${at}.seats[${i}]`, rect));
|
|
227
315
|
if (!seats.length) throw new Refused(`${at}.seats`, "at least one seat");
|
|
228
316
|
areas[name] = { name, rect, seats };
|
|
229
317
|
}
|
|
@@ -255,17 +343,22 @@ export function checkSkinPack(x, { kept = false } = {}) {
|
|
|
255
343
|
const fids = new Set();
|
|
256
344
|
const furniture = list(x.furniture ?? [], LIMIT.furniture, "furniture").map((f, i) => {
|
|
257
345
|
const at = `furniture[${i}]`;
|
|
258
|
-
obj(f, at); keys(f, ["id", "sprite", "x", "y", "depth", "activeWhen", "activeSprite", "activeFor", "loop", "label", "night", "walk"], at);
|
|
346
|
+
obj(f, at); keys(f, ["id", "sprite", "x", "y", "depth", "activeWhen", "activeSprite", "activeFor", "loop", "activeOn", "hides", "label", "night", "walk"], at);
|
|
259
347
|
if (!nameOk(f.id) || fids.has(f.id)) throw new Refused(`${at}.id`, "a new name of letters, digits, _ or - (not an inherited one)");
|
|
260
348
|
fids.add(f.id);
|
|
261
349
|
if (f.activeSprite !== undefined && f.activeWhen === undefined) throw new Refused(`${at}.activeSprite`, "only with activeWhen");
|
|
262
350
|
if (f.activeFor !== undefined && f.activeWhen === undefined) throw new Refused(`${at}.activeFor`, "only with activeWhen");
|
|
263
351
|
if (f.loop !== undefined && typeof f.loop !== "boolean") throw new Refused(`${at}.loop`, "true or false");
|
|
352
|
+
if (f.activeOn !== undefined && f.activeOn !== "state" && f.activeOn !== "seated") throw new Refused(`${at}.activeOn`, '"state" or "seated"');
|
|
353
|
+
if (f.activeOn !== undefined && f.activeWhen === undefined) throw new Refused(`${at}.activeOn`, "only with activeWhen");
|
|
354
|
+
if (f.hides !== undefined && typeof f.hides !== "boolean") throw new Refused(`${at}.hides`, "true or false");
|
|
355
|
+
if (f.hides !== undefined && f.activeFor === undefined) throw new Refused(`${at}.hides`, "only with activeFor");
|
|
264
356
|
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`);
|
|
265
357
|
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`),
|
|
266
358
|
depth: f.depth === "y" ? "y" : depth(f.depth ?? 0, `${at}.depth`), activeWhen: f.activeWhen === undefined ? null : states(f.activeWhen, `${at}.activeWhen`),
|
|
267
359
|
activeSprite: f.activeSprite === undefined ? null : checkStrip(f.activeSprite, `${at}.activeSprite`, scale),
|
|
268
|
-
activeFor: f.activeFor === undefined ? null : names(f.activeFor, `${at}.activeFor`), loop: f.loop === true,
|
|
360
|
+
activeFor: f.activeFor === undefined ? null : names(f.activeFor, `${at}.activeFor`), loop: f.loop === true,
|
|
361
|
+
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),
|
|
269
362
|
walk: walkOf(f.walk, `${at}.walk`, scale) };
|
|
270
363
|
}).sort((a, b) => order(a) - order(b));
|
|
271
364
|
const sets = obj(x.characters, "characters");
|
|
@@ -299,14 +392,17 @@ export function checkSkinPack(x, { kept = false } = {}) {
|
|
|
299
392
|
});
|
|
300
393
|
}
|
|
301
394
|
}
|
|
395
|
+
const levels = levelsOf(x.levels);
|
|
302
396
|
const room = x.room === undefined ? {} : obj(x.room, "room"), desk = x.desk === undefined ? {} : obj(x.desk, "desk");
|
|
303
397
|
keys(room, ["floor", "wall"], "room"); keys(desk, ["back", "front", "lit"], "desk");
|
|
304
398
|
const opt = (v, at) => (v === undefined ? undefined : src(v, at));
|
|
305
399
|
return { ok: true, pack: {
|
|
306
400
|
format: 1, id: x.id, scale, background: opt(x.background, "background"), foreground: opt(x.foreground, "foreground"), matte: x.matte === undefined ? undefined : x.matte.toLowerCase(), areas, stateZones,
|
|
307
|
-
door: x.door === undefined ? { x: LOGICAL.w / 2, y: LOGICAL.h - 10 } : point(x.door, "door"),
|
|
401
|
+
door: x.door === undefined ? { x: LOGICAL.w / 2, y: LOGICAL.h - 10 } : point(x.door, "door"),
|
|
402
|
+
mailDrop: x.mailDrop === undefined ? null : point(x.mailDrop, "mailDrop"), waypoints, edges, straight: x.straight === true, furniture, characters, byInstance,
|
|
308
403
|
props: x.props === undefined ? {} : strips(x.props, PROPS, "props", scale), bubbles,
|
|
309
404
|
room: { floor: opt(room.floor, "room.floor"), wall: opt(room.wall, "room.wall") }, desk: { back: opt(desk.back, "desk.back"), front: opt(desk.front, "desk.front"), lit: opt(desk.lit, "desk.lit") },
|
|
405
|
+
...(levels ? { levels } : {}),
|
|
310
406
|
} };
|
|
311
407
|
} catch (err) {
|
|
312
408
|
if (err instanceof Refused) return { ok: false, error: err.message };
|
|
@@ -337,11 +433,17 @@ export function packStrips(pack) {
|
|
|
337
433
|
// ── Layout overrides ──
|
|
338
434
|
|
|
339
435
|
const OV_LIMIT = { areas: LIMIT.areas, seats: LIMIT.seats, furniture: LIMIT.furniture, waypoints: LIMIT.waypoints, edges: LIMIT.edges };
|
|
436
|
+
/** A seat's slot in `seatFor` (#2691): its index in the area's order, as a key ("0", "1", …). */
|
|
437
|
+
const SLOT = /^(0|[1-9][0-9]{0,2})$/;
|
|
438
|
+
/** A seat's reservation as an override keeps it: { for, lend }, or null (not reserved). */
|
|
439
|
+
const reservedOf = (s) => (s.for && s.for.length ? { for: [...s.for], lend: s.lend === true } : null);
|
|
440
|
+
/** A seat with reservation `r` ({ for, lend }; for [] or null: none) in place of its own. */
|
|
441
|
+
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; };
|
|
340
442
|
/** An office's layout overrides, strictly (their shape and ranges): { ok: true, overrides } or { ok: false, error }. */
|
|
341
443
|
export function checkLayoutOverrides(x) {
|
|
342
444
|
try {
|
|
343
445
|
obj(x, "overrides");
|
|
344
|
-
keys(x, ["pack", "seats", "furniture", "door", "waypoints", "edges"], "overrides");
|
|
446
|
+
keys(x, ["pack", "seats", "furniture", "furnitureNight", "seatFor", "door", "mailDrop", "waypoints", "edges"], "overrides");
|
|
345
447
|
if (typeof x.pack !== "string" || !ID.test(x.pack)) throw new Refused("overrides.pack", "1–40 letters, digits, _ or -");
|
|
346
448
|
const out = { pack: x.pack };
|
|
347
449
|
if (x.seats !== undefined) {
|
|
@@ -366,7 +468,38 @@ export function checkLayoutOverrides(x) {
|
|
|
366
468
|
out.furniture[id] = [q.x, q.y];
|
|
367
469
|
}
|
|
368
470
|
}
|
|
471
|
+
if (x.furnitureNight !== undefined) {
|
|
472
|
+
obj(x.furnitureNight, "overrides.furnitureNight");
|
|
473
|
+
if (Object.keys(x.furnitureNight).length > OV_LIMIT.furniture) throw new Refused("overrides.furnitureNight", `at most ${OV_LIMIT.furniture}`);
|
|
474
|
+
out.furnitureNight = {};
|
|
475
|
+
for (const [id, p] of Object.entries(x.furnitureNight)) {
|
|
476
|
+
if (!nameOk(id)) throw new Refused(`overrides.furnitureNight.${id}`, "an item's id");
|
|
477
|
+
const q = point(p, `overrides.furnitureNight.${id}`);
|
|
478
|
+
out.furnitureNight[id] = [q.x, q.y];
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (x.seatFor !== undefined) {
|
|
482
|
+
obj(x.seatFor, "overrides.seatFor");
|
|
483
|
+
if (Object.keys(x.seatFor).length > OV_LIMIT.areas) throw new Refused("overrides.seatFor", `at most ${OV_LIMIT.areas} areas`);
|
|
484
|
+
out.seatFor = {};
|
|
485
|
+
for (const [area, slots] of Object.entries(x.seatFor)) {
|
|
486
|
+
const at = `overrides.seatFor.${area}`;
|
|
487
|
+
if (!nameOk(area)) throw new Refused(at, "an area's name");
|
|
488
|
+
obj(slots, at);
|
|
489
|
+
if (Object.keys(slots).length > OV_LIMIT.seats) throw new Refused(at, `at most ${OV_LIMIT.seats} seats`);
|
|
490
|
+
out.seatFor[area] = {};
|
|
491
|
+
for (const [slot, r] of Object.entries(slots)) {
|
|
492
|
+
const here = `${at}.${slot}`;
|
|
493
|
+
if (!SLOT.test(slot) || Number(slot) >= OV_LIMIT.seats) throw new Refused(here, `a seat's number, 0 to ${OV_LIMIT.seats - 1}`);
|
|
494
|
+
obj(r, here); keys(r, ["for", "lend"], here);
|
|
495
|
+
if (!Array.isArray(r.for)) throw new Refused(`${here}.for`, "a list");
|
|
496
|
+
if (r.lend !== undefined && typeof r.lend !== "boolean") throw new Refused(`${here}.lend`, "true or false");
|
|
497
|
+
out.seatFor[area][slot] = { for: owners(r.for, `${here}.for`, true), lend: r.lend === true };
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
369
501
|
if (x.door !== undefined) { const q = point(x.door, "overrides.door"); out.door = [q.x, q.y]; }
|
|
502
|
+
if (x.mailDrop !== undefined) { const q = point(x.mailDrop, "overrides.mailDrop"); out.mailDrop = [q.x, q.y]; }
|
|
370
503
|
if (x.edges !== undefined && x.waypoints === undefined) throw new Refused("overrides.edges", "only with waypoints");
|
|
371
504
|
if (x.waypoints !== undefined) {
|
|
372
505
|
const ids = new Set();
|
|
@@ -408,21 +541,68 @@ export function applyOverrides(pack, ov) {
|
|
|
408
541
|
const r = a.rect;
|
|
409
542
|
areas[name] = { ...a, seats: pts.map(([x, y], i) => {
|
|
410
543
|
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");
|
|
411
|
-
return { x, y };
|
|
544
|
+
return { ...a.seats[i], x, y }; // its reservation kept (#2691)
|
|
412
545
|
}) };
|
|
413
546
|
}
|
|
547
|
+
for (const [name, slots] of Object.entries(ov.seatFor || {})) {
|
|
548
|
+
const a = own(areas, name), at = `overrides.seatFor.${name}`;
|
|
549
|
+
if (!a) throw new Refused(at, "an area the pack defines");
|
|
550
|
+
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})`);
|
|
551
|
+
areas[name] = { ...a, seats: a.seats.map((s, i) => { const v = own(slots, String(i)); return v ? reserve(s, v) : s; }) };
|
|
552
|
+
}
|
|
414
553
|
const fids = new Set(pack.furniture.map((f) => f.id));
|
|
415
554
|
for (const id of Object.keys(ov.furniture || {})) if (!fids.has(id)) throw new Refused(`overrides.furniture.${id}`, "an item the pack has");
|
|
416
|
-
const
|
|
555
|
+
const nids = new Set(pack.furniture.filter((f) => f.night).map((f) => f.id));
|
|
556
|
+
for (const id of Object.keys(ov.furnitureNight || {})) if (!nids.has(id)) throw new Refused(`overrides.furnitureNight.${id}`, "an item with a night the pack has");
|
|
557
|
+
const furniture = pack.furniture.map((f) => {
|
|
558
|
+
const q = own(ov.furniture, f.id), n = own(ov.furnitureNight, f.id);
|
|
559
|
+
if (!q && !n) return f;
|
|
560
|
+
return { ...f, ...(q ? { x: q[0], y: q[1] } : {}), ...(n ? { night: { ...f.night, x: n[0], y: n[1] } } : {}) };
|
|
561
|
+
});
|
|
417
562
|
const door = ov.door ? { x: ov.door[0], y: ov.door[1] } : pack.door;
|
|
563
|
+
const mailDrop = ov.mailDrop ? { x: ov.mailDrop[0], y: ov.mailDrop[1] } : pack.mailDrop ?? null;
|
|
418
564
|
const graph = ov.waypoints ? { waypoints: ov.waypoints.map((w) => ({ ...w })), edges: (ov.edges || []).map((e) => [e[0], e[1]]) } : { waypoints: pack.waypoints, edges: pack.edges };
|
|
419
|
-
return { ok: true, pack: { ...pack, areas, furniture, door, ...graph } };
|
|
565
|
+
return { ok: true, pack: { ...pack, areas, furniture, door, mailDrop, ...graph } };
|
|
420
566
|
} catch (err) {
|
|
421
567
|
if (err instanceof Refused) return { ok: false, error: err.message };
|
|
422
568
|
throw err;
|
|
423
569
|
}
|
|
424
570
|
}
|
|
425
571
|
|
|
572
|
+
/**
|
|
573
|
+
* #2665: overrides with only the night places `pack` can take (an item it has, with a `night`): a pack changed since
|
|
574
|
+
* (an item dropped, or its night) drops that entry, never the rest of the layout. applyOverrides itself stays strict
|
|
575
|
+
* (a write naming an item the pack lacks is refused, as for `furniture`).
|
|
576
|
+
*/
|
|
577
|
+
export function fitNight(pack, ov) {
|
|
578
|
+
if (!ov || !ov.furnitureNight || ov.pack !== pack.id) return ov;
|
|
579
|
+
const ok = new Set(pack.furniture.filter((f) => f.night).map((f) => f.id));
|
|
580
|
+
const kept = Object.entries(ov.furnitureNight).filter(([id]) => ok.has(id));
|
|
581
|
+
if (kept.length === Object.keys(ov.furnitureNight).length) return ov;
|
|
582
|
+
const { furnitureNight: _gone, ...rest } = ov;
|
|
583
|
+
return kept.length ? { ...rest, furnitureNight: Object.fromEntries(kept) } : rest;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* #2691: overrides with only the reservations `pack` can take (an area it has, a seat that area has): a pack changed
|
|
588
|
+
* since drops that entry, never the rest of the layout (as fitNight; applyOverrides stays strict).
|
|
589
|
+
*/
|
|
590
|
+
export function fitSeatFor(pack, ov) {
|
|
591
|
+
if (!ov || !ov.seatFor || ov.pack !== pack.id) return ov;
|
|
592
|
+
let dropped = false;
|
|
593
|
+
const kept = {};
|
|
594
|
+
for (const [area, slots] of Object.entries(ov.seatFor)) {
|
|
595
|
+
const a = own(pack.areas, area);
|
|
596
|
+
const ok = Object.entries(slots).filter(([slot]) => a && !!a.seats[Number(slot)]);
|
|
597
|
+
// An area the pack no longer has is dropped even with no slots under it (`{}`: 0 kept of 0, yet applyOverrides refuses it).
|
|
598
|
+
if (!a || ok.length !== Object.keys(slots).length) dropped = true;
|
|
599
|
+
if (ok.length) kept[area] = Object.fromEntries(ok);
|
|
600
|
+
}
|
|
601
|
+
if (!dropped) return ov;
|
|
602
|
+
const { seatFor: _gone, ...rest } = ov;
|
|
603
|
+
return Object.keys(kept).length ? { ...rest, seatFor: kept } : rest;
|
|
604
|
+
}
|
|
605
|
+
|
|
426
606
|
/**
|
|
427
607
|
* What an edited layout keeps as overrides: only the parts that differ from the pack's own (`edited` is a pack with
|
|
428
608
|
* the same areas and furniture, positions moved); null when nothing does (the office goes back to the pack's).
|
|
@@ -431,12 +611,26 @@ export function overridesBetween(pack, edited) {
|
|
|
431
611
|
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
432
612
|
const ov = { pack: pack.id };
|
|
433
613
|
for (const [name, a] of Object.entries(pack.areas)) {
|
|
434
|
-
const e = own(edited.areas, name);
|
|
435
|
-
if (e && !same(a.seats, e.seats)) (ov.seats ||= {})[name] = e.seats.map(
|
|
614
|
+
const e = own(edited.areas, name), xy = (s) => [s.x, s.y];
|
|
615
|
+
if (e && !same(a.seats.map(xy), e.seats.map(xy))) (ov.seats ||= {})[name] = e.seats.map(xy);
|
|
616
|
+
// #2691: a seat whose reservation differs from the pack's (none where the pack has one: for []).
|
|
617
|
+
if (e) e.seats.forEach((s, i) => {
|
|
618
|
+
const was = a.seats[i] ? reservedOf(a.seats[i]) : null, now = reservedOf(s);
|
|
619
|
+
if (a.seats[i] && !same(was, now)) ((ov.seatFor ||= {})[name] ||= {})[String(i)] = now || { for: [], lend: false };
|
|
620
|
+
});
|
|
436
621
|
}
|
|
437
622
|
const at = new Map(pack.furniture.map((f) => [f.id, f]));
|
|
438
623
|
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]; }
|
|
624
|
+
// #2665: a night place that differs from the pack's (its own x/y, or in place: null); in place again is no entry.
|
|
625
|
+
for (const f of edited.furniture) {
|
|
626
|
+
const o = at.get(f.id);
|
|
627
|
+
if (!o || !o.night || !f.night || f.night.x === null || f.night.y === null) continue;
|
|
628
|
+
if (o.night.x !== f.night.x || o.night.y !== f.night.y) (ov.furnitureNight ||= {})[f.id] = [f.night.x, f.night.y];
|
|
629
|
+
}
|
|
439
630
|
if (pack.door.x !== edited.door.x || pack.door.y !== edited.door.y) ov.door = [edited.door.x, edited.door.y];
|
|
631
|
+
// #2736: a mail drop placed (the pack has none) or moved from the pack's.
|
|
632
|
+
const md = pack.mailDrop ?? null, ed = edited.mailDrop ?? null;
|
|
633
|
+
if (ed && (!md || md.x !== ed.x || md.y !== ed.y)) ov.mailDrop = [ed.x, ed.y];
|
|
440
634
|
if (!same(pack.waypoints, edited.waypoints) || !same(pack.edges, edited.edges)) {
|
|
441
635
|
ov.waypoints = edited.waypoints.map((w) => ({ id: w.id, x: w.x, y: w.y }));
|
|
442
636
|
ov.edges = edited.edges.map((e) => [e[0], e[1]]);
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
// @typedef {"working"|"idle"|"paused"|"needs"|"walk"} OfficePose (fleet-office-model.js POSES; "away" draws no one)
|
|
14
14
|
// @typedef {{ pal: OfficePalette, mode: "dark"|"light", labels: { lobby, closed, areas: { [area]: string } } }} OfficeEnv
|
|
15
15
|
// @typedef {{ name: string, label: string, state: string, pose: OfficePose|"away", light: "warn"|"danger"|null,
|
|
16
|
-
// lamp: boolean, sign: "closed"|null, coat: string, work: number, dim: boolean, match: boolean, free: boolean
|
|
17
|
-
//
|
|
16
|
+
// lamp: boolean, sign: "closed"|null, coat: string, work: number, dim: boolean, match: boolean, free: boolean,
|
|
17
|
+
// badge?: { text, tier } | { mark: true } }} OfficeLook
|
|
18
|
+
// (one occupant; `free`: standing in a zone, not behind a desk cell; `badge`: its level on its name tag, #2661)
|
|
18
19
|
// @typedef {object} OfficeSkin
|
|
19
20
|
// @property {string} id
|
|
20
21
|
// @property {object} [pack] a checked pack (fleet-office-pack.js); with `pack.stateZones` the zones layout is used
|
|
@@ -44,15 +45,18 @@
|
|
|
44
45
|
import { t } from "/assets/app-i18n.js";
|
|
45
46
|
import { frameAt, nameHash } from "./fleet-office-model.js";
|
|
46
47
|
import { CHAR_DEPTH, LOGICAL, checkSkinPack, own, packImages, poseStrip, stripBox, walkStrip } from "./fleet-office-pack.js";
|
|
48
|
+
import { tierColours } from "./fleet-office-levels.js";
|
|
47
49
|
|
|
48
50
|
export { nameHash };
|
|
49
51
|
|
|
50
52
|
// ── The palette: the dashboard's tokens, read by the renderer; furniture keeps its own wood and metal ──
|
|
51
53
|
|
|
52
|
-
/** @typedef {{ bg, surface, surface2, border, text, text2, text3, ok, warn, danger, focus, fx: { task, send, report, broadcast } }} OfficePalette */
|
|
54
|
+
/** @typedef {{ bg, surface, surface2, border, text, text2, text3, ok, warn, danger, focus, accent, accentDim, okBg, warnBg, dangerBg, fx: { task, send, report, broadcast } }} OfficePalette */
|
|
53
55
|
export const DEFAULT_PAL = {
|
|
54
56
|
bg: "#212121", surface: "#2f2f2f", surface2: "#3a3a3a", border: "#3d3d3d", text: "#ececec", text2: "#b4b4b4", text3: "#a3a3a3",
|
|
55
57
|
ok: "#4cc38a", warn: "#e3b341", danger: "#ff8a80", focus: "#7ab7ff", fx: { task: "#b794f6", send: "#2dd4bf", report: "#f472b6", broadcast: "#fb923c" },
|
|
58
|
+
// #2661: the level badges' tiers (the status tokens and their tints).
|
|
59
|
+
accent: "#7ab7ff", accentDim: "rgba(122, 183, 255, 0.14)", okBg: "rgba(76, 195, 138, 0.14)", warnBg: "rgba(227, 179, 65, 0.14)", dangerBg: "rgba(255, 138, 128, 0.14)",
|
|
56
60
|
};
|
|
57
61
|
/** Light or dark, from the page's own background token (whatever chose it: the person, the system, a theme). */
|
|
58
62
|
export function modeOf(pal) {
|
|
@@ -235,6 +239,7 @@ function drawDeskFront(ctx, d, look, env) {
|
|
|
235
239
|
ctx.fillText(fitText(ctx, look.label, d.w - 18), d.x + d.w / 2, d.y + 86.5);
|
|
236
240
|
}
|
|
237
241
|
ctx.restore();
|
|
242
|
+
if (!look.notag && look.badge) drawBadge(ctx, d.x + 6, d.y + 78, look.badge, pal);
|
|
238
243
|
if (look.match) { ctx.save(); ctx.strokeStyle = pal.focus; ctx.lineWidth = 2; ctx.strokeRect(d.x + 2, d.y + 2, d.w - 4, d.h - 6); ctx.restore(); }
|
|
239
244
|
}
|
|
240
245
|
|
|
@@ -296,6 +301,36 @@ export function tagBox(ctx, x, y, label) {
|
|
|
296
301
|
ctx.restore();
|
|
297
302
|
return { x: Math.round(x - w / 2), y: y + 3, w, h: 14, text: t };
|
|
298
303
|
}
|
|
304
|
+
/**
|
|
305
|
+
* #2661: a level badge on a name tag's top-left corner (x, y: the tag's corner), overlapping its edge like an app's
|
|
306
|
+
* badge — it takes none of the tag's width. `badge`: { text, tier } (fleet-office-levels.js badgeOf), or { mark } for a
|
|
307
|
+
* pinned title that hides the number: a small diamond.
|
|
308
|
+
*/
|
|
309
|
+
export function drawBadge(ctx, x, y, badge, pal) {
|
|
310
|
+
if (!badge) return;
|
|
311
|
+
const c = tierColours(badge.tier, pal);
|
|
312
|
+
ctx.save();
|
|
313
|
+
ctx.globalAlpha = 1;
|
|
314
|
+
if (badge.mark) {
|
|
315
|
+
ctx.translate(x, y); ctx.rotate(Math.PI / 4);
|
|
316
|
+
ctx.fillStyle = pal.surface; ctx.fillRect(-4, -4, 8, 8);
|
|
317
|
+
ctx.fillStyle = c.bg; ctx.fillRect(-4, -4, 8, 8);
|
|
318
|
+
ctx.strokeStyle = c.line; ctx.lineWidth = 1; ctx.strokeRect(-3.5, -3.5, 7, 7);
|
|
319
|
+
ctx.restore();
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
ctx.font = BADGE_FONT;
|
|
323
|
+
const w = Math.max(12, Math.ceil((ctx.measureText ? ctx.measureText(badge.text).width : badge.text.length * 5) + 6)), h = 11;
|
|
324
|
+
const bx = Math.round(x - w / 2 + 3), by = Math.round(y - h / 2 - 1);
|
|
325
|
+
// An opaque surface first, so a tint reads the same over any picture.
|
|
326
|
+
ctx.fillStyle = pal.surface; ctx.fillRect(bx, by, w, h);
|
|
327
|
+
ctx.fillStyle = c.bg; ctx.fillRect(bx, by, w, h);
|
|
328
|
+
ctx.strokeStyle = c.line; ctx.lineWidth = 1; ctx.strokeRect(bx + 0.5, by + 0.5, w - 1, h - 1);
|
|
329
|
+
ctx.fillStyle = c.fg; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText(badge.text, bx + w / 2, by + h / 2 + 0.5);
|
|
330
|
+
ctx.restore();
|
|
331
|
+
}
|
|
332
|
+
const BADGE_FONT = "700 8px system-ui, sans-serif";
|
|
333
|
+
|
|
299
334
|
/** Zones: a name tag under someone's feet at (x, y). */
|
|
300
335
|
function drawTag(ctx, x, y, look, env) {
|
|
301
336
|
const { pal } = env;
|
|
@@ -307,6 +342,7 @@ function drawTag(ctx, x, y, look, env) {
|
|
|
307
342
|
ctx.strokeStyle = look.match ? pal.focus : pal.border; ctx.lineWidth = look.match ? 2 : 1; ctx.strokeRect(Math.round(x - w / 2) + 0.5, y + 3.5, w - 1, 13);
|
|
308
343
|
ctx.fillStyle = pal.text; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText(t, x, y + 10.5);
|
|
309
344
|
ctx.restore();
|
|
345
|
+
if (look.badge) drawBadge(ctx, Math.round(x - w / 2), y + 3, look.badge, pal);
|
|
310
346
|
}
|
|
311
347
|
|
|
312
348
|
/**
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
// The Office view's data, behind one small seam (fleet-office.js uses only these calls):
|
|
2
2
|
// - the offices people made: the host's (GET / PUT /ui/offices, src/web-office-store.ts — one list for every viewer);
|
|
3
3
|
// what comes back is checked again here (fleet-office-model.js officesOk), never trusted;
|
|
4
|
+
// - #2716: the whole-fleet office's style, kept on the host for everyone (GET /ui/offices's autoSkin; PUT
|
|
5
|
+
// /ui/office-style, an admin's) — this browser's own pick (skin(): an admin's copy of it, anyone else's own choice);
|
|
4
6
|
// - this browser's own choices (localStorage; blocked storage: this page only, as the canvas's Canvas/List): which
|
|
5
|
-
// office is shown, the auto office's member filter
|
|
7
|
+
// office is shown, the auto office's member filter; #2540: the view (zoom and centre) and the
|
|
6
8
|
// fit mode (Fit or Fill) per office; #2554: whether All offices shows the lobby tile.
|
|
7
9
|
// - the skin packs an admin uploaded (#2513: GET /ui/office-skins, POST to upload one, DELETE to remove one;
|
|
8
10
|
// src/web-office-skins.ts). The office editor and the layout editor call saveOffices() with the whole list and the rev
|
|
9
11
|
// they read (a change in between answers 409).
|
|
12
|
+
// - #2661: everyone's level (GET /ui/office-levels, src/office-levels.ts; fleet-office-levels.js levelsOk checks it).
|
|
13
|
+
// - #2692: the agents' pack changes (GET / POST /ui/office-pack-changes, an admin's): which agents may change packs, the
|
|
14
|
+
// fleet's approval mode, the changes waiting for a yes, and the newest log entries.
|
|
10
15
|
import { AUTO_ID, filterOk, officesOk } from "./fleet-office-model.js";
|
|
11
16
|
import { fitModeOk, savedOk } from "./fleet-office-view.js";
|
|
17
|
+
import { levelsOk } from "./fleet-office-levels.js";
|
|
12
18
|
|
|
13
19
|
const CHOICE_KEY = "agend_org_office", FILTER_KEY = "agend_org_office_filter", SKIN_KEY = "agend_org_office_skin", ASLEEP_KEY = "agend_org_office_hide_asleep", NIGHT_KEY = "agend_org_office_night";
|
|
14
20
|
/** #2554: "0" when All offices leaves the lobby tile out (default: shown). */
|
|
@@ -18,6 +24,19 @@ const VIEW_KEY = "agend_org_office_view", VIEWS_MAX = 32;
|
|
|
18
24
|
/** #2540: { "<office>@<style>": "cover" } — the offices whose fit fills the panel (the rest: "contain"), the last VIEWS_MAX. */
|
|
19
25
|
const FIT_KEY = "agend_org_office_fit";
|
|
20
26
|
const PACK_ID = /^[A-Za-z0-9_-]{1,40}$/;
|
|
27
|
+
/** #2692: GET /ui/office-pack-changes, checked: { approval, agents: [names], pending: [views], log: [entries] } (anything odd dropped). */
|
|
28
|
+
export function packChangesOk(d) {
|
|
29
|
+
const str = (v, n = 200) => (typeof v === "string" ? v.slice(0, n) : "");
|
|
30
|
+
const approval = d && (d.approval === "apply" || d.approval === "approve") ? d.approval : "approve";
|
|
31
|
+
const agents = Array.isArray(d && d.agents) ? d.agents.filter((n) => typeof n === "string" && n).slice(0, 500).map((n) => n.slice(0, 200)) : [];
|
|
32
|
+
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")
|
|
33
|
+
.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,
|
|
34
|
+
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 })),
|
|
35
|
+
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 }));
|
|
36
|
+
const log = (Array.isArray(d && d.log) ? d.log.slice(0, 50) : []).filter((e) => e && typeof e.at === "string")
|
|
37
|
+
.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) }));
|
|
38
|
+
return { approval, agents, pending, log };
|
|
39
|
+
}
|
|
21
40
|
/** The host's list of skin packs, checked: [{ id, name, sha }] (anything odd dropped). */
|
|
22
41
|
export function skinListOk(d) {
|
|
23
42
|
const out = [], ids = new Set();
|
|
@@ -50,15 +69,26 @@ export function createOfficeStore({ fetch: get = globalThis.fetch, storage } = {
|
|
|
50
69
|
const write = (k, v) => { try { const s = st(); if (s) s.setItem(k, v); } catch { /* this page only */ } };
|
|
51
70
|
const headers = { "Content-Type": "application/json" };
|
|
52
71
|
return {
|
|
53
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* { rev, offices, autoSkin } from the host (#2716 autoSkin: the whole-fleet office's style, or null: none kept); null
|
|
74
|
+
* when it could not be read (no offices shown but the auto one).
|
|
75
|
+
*/
|
|
54
76
|
async offices() {
|
|
55
77
|
try {
|
|
56
78
|
const r = await get("/ui/offices", { method: "GET", headers });
|
|
57
79
|
if (!r || !r.ok) return null;
|
|
58
80
|
const d = await r.json();
|
|
59
|
-
return { rev: Number.isSafeInteger(d && d.rev) ? d.rev : 0, offices: officesOk(d && d.offices) };
|
|
81
|
+
return { rev: Number.isSafeInteger(d && d.rev) ? d.rev : 0, offices: officesOk(d && d.offices), autoSkin: d && typeof d.autoSkin === "string" && PACK_ID.test(d.autoSkin) ? d.autoSkin : null };
|
|
60
82
|
} catch { return null; }
|
|
61
83
|
},
|
|
84
|
+
/** #2716: the whole-fleet office's style for everyone (an admin session): { ok, autoSkin } or { ok: false, status, error }. */
|
|
85
|
+
async saveAutoSkin(skin) {
|
|
86
|
+
try {
|
|
87
|
+
const r = await get("/ui/office-style", { method: "PUT", headers, body: JSON.stringify({ skin }) });
|
|
88
|
+
const d = await r.json().catch(() => ({}));
|
|
89
|
+
return r.ok ? { ok: true, autoSkin: typeof d.autoSkin === "string" ? d.autoSkin : null } : { ok: false, status: r.status, error: d.error || "office_style_failed" };
|
|
90
|
+
} catch { return { ok: false, status: 0, error: "office_style_failed" }; }
|
|
91
|
+
},
|
|
62
92
|
/**
|
|
63
93
|
* Replace the host's list (an admin session; `rev`: the one read — required: the host refuses a save without it,
|
|
64
94
|
* so two editors never overwrite each other unseen). { ok, rev, offices } or { ok: false, status, error }.
|
|
@@ -80,6 +110,17 @@ export function createOfficeStore({ fetch: get = globalThis.fetch, storage } = {
|
|
|
80
110
|
return actsOk(d && d.acts, d && d.now);
|
|
81
111
|
} catch { return null; }
|
|
82
112
|
},
|
|
113
|
+
/**
|
|
114
|
+
* #2661: everyone's level (GET /ui/office-levels; a share link's own, through its lease), checked (levelsOk); null
|
|
115
|
+
* when it could not be read (the levels shown stay).
|
|
116
|
+
*/
|
|
117
|
+
async levels() {
|
|
118
|
+
try {
|
|
119
|
+
const r = await get("/ui/office-levels", { method: "GET", headers });
|
|
120
|
+
if (!r || !r.ok) return null;
|
|
121
|
+
return levelsOk(await r.json());
|
|
122
|
+
} catch { return null; }
|
|
123
|
+
},
|
|
83
124
|
/** The skin packs the host keeps: { rev, packs: [{ id, name, sha }] }; null when they could not be read. */
|
|
84
125
|
async skins() {
|
|
85
126
|
try {
|
|
@@ -105,6 +146,34 @@ export function createOfficeStore({ fetch: get = globalThis.fetch, storage } = {
|
|
|
105
146
|
return r.ok ? { ok: true } : { ok: false, status: r.status, error: d.error || "skin_failed" };
|
|
106
147
|
} catch { return { ok: false, status: 0, error: "skin_failed" }; }
|
|
107
148
|
},
|
|
149
|
+
/**
|
|
150
|
+
* #2692: the agents' skin-pack changes (an admin session): { approval, agents, pending, log }, or null (not offered,
|
|
151
|
+
* not an admin, not read).
|
|
152
|
+
*/
|
|
153
|
+
async packChanges() {
|
|
154
|
+
try {
|
|
155
|
+
const r = await get("/ui/office-pack-changes", { method: "GET", headers });
|
|
156
|
+
if (!r || !r.ok) return null;
|
|
157
|
+
return packChangesOk(await r.json());
|
|
158
|
+
} catch { return null; }
|
|
159
|
+
},
|
|
160
|
+
/** #2692: the fleet's approval mode for agents' pack changes ("apply" | "approve"). */
|
|
161
|
+
async setPackApproval(approval) {
|
|
162
|
+
try {
|
|
163
|
+
const r = await get("/ui/office-pack-changes", { method: "POST", headers, body: JSON.stringify({ approval }) });
|
|
164
|
+
const d = await r.json().catch(() => ({}));
|
|
165
|
+
return r.ok ? { ok: true, changes: packChangesOk(d) } : { ok: false, status: r.status, error: d.error || "pack_changes_failed" };
|
|
166
|
+
} catch { return { ok: false, status: 0, error: "pack_changes_failed" }; }
|
|
167
|
+
},
|
|
168
|
+
/** #2692: an admin's yes or no on a pending pack change: { ok, text } or { ok: false, status, error, detail }. */
|
|
169
|
+
async decidePackChange(id, decision) {
|
|
170
|
+
if (!/^[0-9a-f]{32}$/.test(String(id))) return { ok: false, status: 0, error: "change_unknown", detail: "" };
|
|
171
|
+
try {
|
|
172
|
+
const r = await get(`/ui/office-pack-changes/${id}`, { method: "POST", headers, body: JSON.stringify({ decision }) });
|
|
173
|
+
const d = await r.json().catch(() => ({}));
|
|
174
|
+
return r.ok ? { ok: true, text: String(d.text || "") } : { ok: false, status: r.status, error: d.error || "pack_changes_failed", detail: String(d.detail || "") };
|
|
175
|
+
} catch { return { ok: false, status: 0, error: "pack_changes_failed", detail: "" }; }
|
|
176
|
+
},
|
|
108
177
|
/**
|
|
109
178
|
* The office shown here: an id (one the host no longer has falls back to the auto office where it is used), or
|
|
110
179
|
* #2554's All offices (fleet-office-overview.js ALL_ID; with fewer than two offices stored: the auto office).
|
|
@@ -115,6 +184,8 @@ export function createOfficeStore({ fetch: get = globalThis.fetch, storage } = {
|
|
|
115
184
|
saveFilter: (f) => write(FILTER_KEY, JSON.stringify(filterOk(f))),
|
|
116
185
|
/** The whole fleet's style here: a skin id ("pixel": desks, "studio" and "cozy": zones); none chosen yet: cozy (#2631); an unknown one is the default where used. */
|
|
117
186
|
skin: () => { const v = read(SKIN_KEY); return typeof v === "string" && /^[A-Za-z0-9_-]{1,40}$/.test(v) ? v : "cozy"; },
|
|
187
|
+
/** #2716: this browser's own pick of it, or null (none made here). */
|
|
188
|
+
skinChosen: () => { const v = read(SKIN_KEY); return typeof v === "string" && /^[A-Za-z0-9_-]{1,40}$/.test(v) ? v : null; },
|
|
118
189
|
saveSkin: (id) => write(SKIN_KEY, String(id)),
|
|
119
190
|
/** Whether paused and stopped people are left out of the Office here (default: shown). */
|
|
120
191
|
hideAsleep: () => read(ASLEEP_KEY) === "1",
|