@songsid/agend 2.3.2-beta.9 → 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 +57 -17
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +57 -17
- 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 +45 -8
- package/dist/office-share.js +75 -15
- 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 +56 -11
- 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 +15 -3
- package/dist/ui/fleet-office-pack.js +177 -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 +308 -44
- 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 +1 -0
- 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 +70 -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 +124 -23
- 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
|
|
@@ -103,7 +128,10 @@
|
|
|
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)
|
|
106
|
-
// "
|
|
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)
|
|
107
135
|
// "waypoints": [{ "id": "hall", "x": 640, "y": 600 }, …], "edges": [["hall", "east"], …] }
|
|
108
136
|
// the walkable graph, replaced whole (edges need waypoints)
|
|
109
137
|
//
|
|
@@ -112,8 +140,8 @@
|
|
|
112
140
|
// seat outside its area, an area or furniture the pack does not have, or a seat count that differs (the host checks
|
|
113
141
|
// that too, for a pack it keeps). A night place goes on the item's `night` (its x, y), so at bedtime (nightFurniture)
|
|
114
142
|
// 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).
|
|
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).
|
|
117
145
|
|
|
118
146
|
export const LOGICAL = { w: 1280, h: 720 };
|
|
119
147
|
/** The people's depth: furniture below it is drawn under them, the rest over them. */
|
|
@@ -133,7 +161,7 @@ const RESERVED = new Set(["cozy"]);
|
|
|
133
161
|
const ID = /^[A-Za-z0-9_-]{1,40}$/;
|
|
134
162
|
const SRC = /^[A-Za-z0-9._/-]{1,200}$/;
|
|
135
163
|
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 };
|
|
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 };
|
|
137
165
|
|
|
138
166
|
class Refused extends Error { constructor(at, why) { super(`${at}: ${why}`); } }
|
|
139
167
|
/** `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 +212,33 @@ function walkOf(v, at, scale) {
|
|
|
184
212
|
if (!w.walk) throw new Refused(`${at}.walk`, "required");
|
|
185
213
|
return w;
|
|
186
214
|
}
|
|
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) {
|
|
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) {
|
|
189
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; };
|
|
190
|
-
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 };
|
|
191
242
|
}
|
|
192
243
|
function strips(v, names, at, scale) {
|
|
193
244
|
obj(v, at);
|
|
@@ -199,6 +250,43 @@ function states(v, at) {
|
|
|
199
250
|
return Array.isArray(v) ? list(v, 8, at).map((s, i) => one(s, `${at}[${i}]`)) : [one(v, at)];
|
|
200
251
|
}
|
|
201
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
|
+
|
|
202
290
|
/**
|
|
203
291
|
* A pack's manifest, strictly: { ok: true, pack } or { ok: false, error: "<where>: <why>" }. `kept`: a pack the host
|
|
204
292
|
* keeps already (read back, not uploaded), which may hold a RESERVED id.
|
|
@@ -206,7 +294,7 @@ function states(v, at) {
|
|
|
206
294
|
export function checkSkinPack(x, { kept = false } = {}) {
|
|
207
295
|
try {
|
|
208
296
|
obj(x, "pack");
|
|
209
|
-
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");
|
|
210
298
|
if (x.format !== 1) throw new Refused("format", "1");
|
|
211
299
|
if (typeof x.id !== "string" || !ID.test(x.id)) throw new Refused("id", "1–40 letters, digits, _ or -");
|
|
212
300
|
if (BUILT_IN.has(x.id) || (!kept && RESERVED.has(x.id))) throw new Refused("id", "a built-in skin's id");
|
|
@@ -223,11 +311,7 @@ export function checkSkinPack(x, { kept = false } = {}) {
|
|
|
223
311
|
if (!Array.isArray(a.rect) || a.rect.length !== 4) throw new Refused(`${at}.rect`, "[x, y, w, h]");
|
|
224
312
|
const [rx, ry] = [num(a.rect[0], 0, LOGICAL.w, `${at}.rect[0]`), num(a.rect[1], 0, LOGICAL.h, `${at}.rect[1]`)];
|
|
225
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]`) };
|
|
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
|
-
});
|
|
314
|
+
const seats = list(a.seats, LIMIT.seats, `${at}.seats`).map((s, i) => seatOf(s, `${at}.seats[${i}]`, rect));
|
|
231
315
|
if (!seats.length) throw new Refused(`${at}.seats`, "at least one seat");
|
|
232
316
|
areas[name] = { name, rect, seats };
|
|
233
317
|
}
|
|
@@ -259,17 +343,22 @@ export function checkSkinPack(x, { kept = false } = {}) {
|
|
|
259
343
|
const fids = new Set();
|
|
260
344
|
const furniture = list(x.furniture ?? [], LIMIT.furniture, "furniture").map((f, i) => {
|
|
261
345
|
const at = `furniture[${i}]`;
|
|
262
|
-
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);
|
|
263
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)");
|
|
264
348
|
fids.add(f.id);
|
|
265
349
|
if (f.activeSprite !== undefined && f.activeWhen === undefined) throw new Refused(`${at}.activeSprite`, "only with activeWhen");
|
|
266
350
|
if (f.activeFor !== undefined && f.activeWhen === undefined) throw new Refused(`${at}.activeFor`, "only with activeWhen");
|
|
267
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");
|
|
268
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`);
|
|
269
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`),
|
|
270
358
|
depth: f.depth === "y" ? "y" : depth(f.depth ?? 0, `${at}.depth`), activeWhen: f.activeWhen === undefined ? null : states(f.activeWhen, `${at}.activeWhen`),
|
|
271
359
|
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,
|
|
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),
|
|
273
362
|
walk: walkOf(f.walk, `${at}.walk`, scale) };
|
|
274
363
|
}).sort((a, b) => order(a) - order(b));
|
|
275
364
|
const sets = obj(x.characters, "characters");
|
|
@@ -303,14 +392,17 @@ export function checkSkinPack(x, { kept = false } = {}) {
|
|
|
303
392
|
});
|
|
304
393
|
}
|
|
305
394
|
}
|
|
395
|
+
const levels = levelsOf(x.levels);
|
|
306
396
|
const room = x.room === undefined ? {} : obj(x.room, "room"), desk = x.desk === undefined ? {} : obj(x.desk, "desk");
|
|
307
397
|
keys(room, ["floor", "wall"], "room"); keys(desk, ["back", "front", "lit"], "desk");
|
|
308
398
|
const opt = (v, at) => (v === undefined ? undefined : src(v, at));
|
|
309
399
|
return { ok: true, pack: {
|
|
310
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,
|
|
311
|
-
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,
|
|
312
403
|
props: x.props === undefined ? {} : strips(x.props, PROPS, "props", scale), bubbles,
|
|
313
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 } : {}),
|
|
314
406
|
} };
|
|
315
407
|
} catch (err) {
|
|
316
408
|
if (err instanceof Refused) return { ok: false, error: err.message };
|
|
@@ -341,11 +433,17 @@ export function packStrips(pack) {
|
|
|
341
433
|
// ── Layout overrides ──
|
|
342
434
|
|
|
343
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; };
|
|
344
442
|
/** An office's layout overrides, strictly (their shape and ranges): { ok: true, overrides } or { ok: false, error }. */
|
|
345
443
|
export function checkLayoutOverrides(x) {
|
|
346
444
|
try {
|
|
347
445
|
obj(x, "overrides");
|
|
348
|
-
keys(x, ["pack", "seats", "furniture", "furnitureNight", "door", "waypoints", "edges"], "overrides");
|
|
446
|
+
keys(x, ["pack", "seats", "furniture", "furnitureNight", "seatFor", "door", "mailDrop", "waypoints", "edges"], "overrides");
|
|
349
447
|
if (typeof x.pack !== "string" || !ID.test(x.pack)) throw new Refused("overrides.pack", "1–40 letters, digits, _ or -");
|
|
350
448
|
const out = { pack: x.pack };
|
|
351
449
|
if (x.seats !== undefined) {
|
|
@@ -380,7 +478,28 @@ export function checkLayoutOverrides(x) {
|
|
|
380
478
|
out.furnitureNight[id] = [q.x, q.y];
|
|
381
479
|
}
|
|
382
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
|
+
}
|
|
383
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]; }
|
|
384
503
|
if (x.edges !== undefined && x.waypoints === undefined) throw new Refused("overrides.edges", "only with waypoints");
|
|
385
504
|
if (x.waypoints !== undefined) {
|
|
386
505
|
const ids = new Set();
|
|
@@ -422,9 +541,15 @@ export function applyOverrides(pack, ov) {
|
|
|
422
541
|
const r = a.rect;
|
|
423
542
|
areas[name] = { ...a, seats: pts.map(([x, y], i) => {
|
|
424
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");
|
|
425
|
-
return { x, y };
|
|
544
|
+
return { ...a.seats[i], x, y }; // its reservation kept (#2691)
|
|
426
545
|
}) };
|
|
427
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
|
+
}
|
|
428
553
|
const fids = new Set(pack.furniture.map((f) => f.id));
|
|
429
554
|
for (const id of Object.keys(ov.furniture || {})) if (!fids.has(id)) throw new Refused(`overrides.furniture.${id}`, "an item the pack has");
|
|
430
555
|
const nids = new Set(pack.furniture.filter((f) => f.night).map((f) => f.id));
|
|
@@ -435,8 +560,9 @@ export function applyOverrides(pack, ov) {
|
|
|
435
560
|
return { ...f, ...(q ? { x: q[0], y: q[1] } : {}), ...(n ? { night: { ...f.night, x: n[0], y: n[1] } } : {}) };
|
|
436
561
|
});
|
|
437
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;
|
|
438
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 };
|
|
439
|
-
return { ok: true, pack: { ...pack, areas, furniture, door, ...graph } };
|
|
565
|
+
return { ok: true, pack: { ...pack, areas, furniture, door, mailDrop, ...graph } };
|
|
440
566
|
} catch (err) {
|
|
441
567
|
if (err instanceof Refused) return { ok: false, error: err.message };
|
|
442
568
|
throw err;
|
|
@@ -457,6 +583,26 @@ export function fitNight(pack, ov) {
|
|
|
457
583
|
return kept.length ? { ...rest, furnitureNight: Object.fromEntries(kept) } : rest;
|
|
458
584
|
}
|
|
459
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
|
+
|
|
460
606
|
/**
|
|
461
607
|
* What an edited layout keeps as overrides: only the parts that differ from the pack's own (`edited` is a pack with
|
|
462
608
|
* the same areas and furniture, positions moved); null when nothing does (the office goes back to the pack's).
|
|
@@ -465,8 +611,13 @@ export function overridesBetween(pack, edited) {
|
|
|
465
611
|
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
466
612
|
const ov = { pack: pack.id };
|
|
467
613
|
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(
|
|
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
|
+
});
|
|
470
621
|
}
|
|
471
622
|
const at = new Map(pack.furniture.map((f) => [f.id, f]));
|
|
472
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]; }
|
|
@@ -477,6 +628,9 @@ export function overridesBetween(pack, edited) {
|
|
|
477
628
|
if (o.night.x !== f.night.x || o.night.y !== f.night.y) (ov.furnitureNight ||= {})[f.id] = [f.night.x, f.night.y];
|
|
478
629
|
}
|
|
479
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];
|
|
480
634
|
if (!same(pack.waypoints, edited.waypoints) || !same(pack.edges, edited.edges)) {
|
|
481
635
|
ov.waypoints = edited.waypoints.map((w) => ({ id: w.id, x: w.x, y: w.y }));
|
|
482
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",
|
|
@@ -97,6 +97,14 @@ export function miniFrame(v, size, w, h) {
|
|
|
97
97
|
const x1 = Math.min(w, (size.w - v.x) / v.k), y1 = Math.min(h, (size.h - v.y) / v.k);
|
|
98
98
|
return { x: x0, y: y0, width: Math.max(0, x1 - x0), height: Math.max(0, y1 - y0) };
|
|
99
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* #2737: the whole scene is in view (the overview's frame covers all of it, to half a unit), so the overview would only
|
|
102
|
+
* repeat the panel — and sit over the one corner of it a person cannot otherwise see.
|
|
103
|
+
*/
|
|
104
|
+
export function wholeInView(v, size, w, h) {
|
|
105
|
+
const f = miniFrame(v, size, w, h);
|
|
106
|
+
return f.x <= 0.5 && f.y <= 0.5 && f.x + f.width >= w - 0.5 && f.y + f.height >= h - 0.5;
|
|
107
|
+
}
|
|
100
108
|
|
|
101
109
|
// ── Kept per device, per office: the zoom and the logical point in the middle (the viewport's size may differ) ──
|
|
102
110
|
|