@songsid/agend 2.3.2-beta.9 → 2.3.3-beta.1

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.
Files changed (285) hide show
  1. package/README.md +6 -0
  2. package/README.zh-TW.md +6 -0
  3. package/dist/agent-cli-instructions.md +2 -2
  4. package/dist/agent-cli.js +48 -0
  5. package/dist/agent-cli.js.map +1 -1
  6. package/dist/agent-endpoint.js +6 -0
  7. package/dist/agent-endpoint.js.map +1 -1
  8. package/dist/agent-whats-new.d.ts +21 -0
  9. package/dist/agent-whats-new.js +53 -21
  10. package/dist/agent-whats-new.js.map +1 -1
  11. package/dist/auth-api.js +2 -0
  12. package/dist/auth-api.js.map +1 -1
  13. package/dist/backend/claude-code.d.ts +13 -1
  14. package/dist/backend/claude-code.js +43 -2
  15. package/dist/backend/claude-code.js.map +1 -1
  16. package/dist/backend/codex-session-lookup.d.ts +15 -0
  17. package/dist/backend/codex-session-lookup.js +58 -0
  18. package/dist/backend/codex-session-lookup.js.map +1 -1
  19. package/dist/backend/codex.js +15 -4
  20. package/dist/backend/codex.js.map +1 -1
  21. package/dist/backend/kiro-classic-archive.d.ts +57 -0
  22. package/dist/backend/kiro-classic-archive.js +622 -0
  23. package/dist/backend/kiro-classic-archive.js.map +1 -0
  24. package/dist/backend/kiro-classic-links.d.ts +24 -0
  25. package/dist/backend/kiro-classic-links.js +135 -0
  26. package/dist/backend/kiro-classic-links.js.map +1 -0
  27. package/dist/backend/kiro-identity.d.ts +100 -1
  28. package/dist/backend/kiro-identity.js +262 -31
  29. package/dist/backend/kiro-identity.js.map +1 -1
  30. package/dist/backend/kiro-migration-verify.d.ts +10 -0
  31. package/dist/backend/kiro-migration-verify.js +52 -0
  32. package/dist/backend/kiro-migration-verify.js.map +1 -0
  33. package/dist/backend/kiro-reconcile-store.d.ts +27 -0
  34. package/dist/backend/kiro-reconcile-store.js +351 -0
  35. package/dist/backend/kiro-reconcile-store.js.map +1 -0
  36. package/dist/backend/kiro-v2-store-worker.js +35 -1
  37. package/dist/backend/kiro-v2-store-worker.js.map +1 -1
  38. package/dist/backend/kiro-v2-store.d.ts +31 -1
  39. package/dist/backend/kiro-v2-store.js +22 -12
  40. package/dist/backend/kiro-v2-store.js.map +1 -1
  41. package/dist/backend/kiro.d.ts +124 -6
  42. package/dist/backend/kiro.js +553 -39
  43. package/dist/backend/kiro.js.map +1 -1
  44. package/dist/backend/types.d.ts +109 -4
  45. package/dist/backend/types.js +9 -0
  46. package/dist/backend/types.js.map +1 -1
  47. package/dist/cache-service.d.ts +13 -1
  48. package/dist/cache-service.js +45 -2
  49. package/dist/cache-service.js.map +1 -1
  50. package/dist/channel/ipc-timeouts.js +3 -0
  51. package/dist/channel/ipc-timeouts.js.map +1 -1
  52. package/dist/channel/mcp-tools.js +40 -5
  53. package/dist/channel/mcp-tools.js.map +1 -1
  54. package/dist/channel/tool-router.js +2 -1
  55. package/dist/channel/tool-router.js.map +1 -1
  56. package/dist/classic-channel-manager.d.ts +6 -0
  57. package/dist/classic-channel-manager.js +25 -0
  58. package/dist/classic-channel-manager.js.map +1 -1
  59. package/dist/cli.js +52 -6
  60. package/dist/cli.js.map +1 -1
  61. package/dist/config-validator.d.ts +7 -1
  62. package/dist/config-validator.js +20 -9
  63. package/dist/config-validator.js.map +1 -1
  64. package/dist/config.js +2 -0
  65. package/dist/config.js.map +1 -1
  66. package/dist/cross-instance-envelope.d.ts +10 -5
  67. package/dist/cross-instance-envelope.js +20 -17
  68. package/dist/cross-instance-envelope.js.map +1 -1
  69. package/dist/daemon.d.ts +125 -63
  70. package/dist/daemon.js +694 -330
  71. package/dist/daemon.js.map +1 -1
  72. package/dist/dashboard-permissions.d.ts +15 -4
  73. package/dist/dashboard-permissions.js +49 -23
  74. package/dist/dashboard-permissions.js.map +1 -1
  75. package/dist/delivery-outbox.d.ts +27 -22
  76. package/dist/delivery-outbox.js +109 -161
  77. package/dist/delivery-outbox.js.map +1 -1
  78. package/dist/delivery-outbox.test.js +10 -25
  79. package/dist/delivery-outbox.test.js.map +1 -1
  80. package/dist/doctor.js +2 -1
  81. package/dist/doctor.js.map +1 -1
  82. package/dist/fleet-control-audit.d.ts +1 -1
  83. package/dist/fleet-control-audit.js +3 -0
  84. package/dist/fleet-control-audit.js.map +1 -1
  85. package/dist/fleet-manager.d.ts +94 -15
  86. package/dist/fleet-manager.js +597 -133
  87. package/dist/fleet-manager.js.map +1 -1
  88. package/dist/forged-envelope-scan.d.ts +9 -0
  89. package/dist/forged-envelope-scan.js +30 -5
  90. package/dist/forged-envelope-scan.js.map +1 -1
  91. package/dist/general-instance.d.ts +24 -0
  92. package/dist/general-instance.js +53 -0
  93. package/dist/general-instance.js.map +1 -1
  94. package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +1 -1
  95. package/dist/general-knowledge/skills/docker-install/SKILL.md +25 -0
  96. package/dist/general-knowledge/skills/fleet-config/SKILL.md +5 -3
  97. package/dist/general-knowledge/skills/fleet-restart/SKILL.md +1 -1
  98. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +34 -0
  99. package/dist/general-knowledge/skills/session-management/SKILL.md +2 -2
  100. package/dist/general-knowledge/skills/tui-effort/SKILL.md +1 -1
  101. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +32 -0
  102. package/dist/general-knowledge/skills/web-appearance/SKILL.md +32 -0
  103. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +57 -17
  104. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +57 -17
  105. package/dist/instance-config-impact.js +3 -2
  106. package/dist/instance-config-impact.js.map +1 -1
  107. package/dist/instance-lifecycle.d.ts +4 -0
  108. package/dist/instance-lifecycle.js +28 -5
  109. package/dist/instance-lifecycle.js.map +1 -1
  110. package/dist/instructions.js +6 -7
  111. package/dist/instructions.js.map +1 -1
  112. package/dist/kiro-migrate.d.ts +19 -2
  113. package/dist/kiro-migrate.js +133 -35
  114. package/dist/kiro-migrate.js.map +1 -1
  115. package/dist/kiro-reconcile.d.ts +21 -0
  116. package/dist/kiro-reconcile.js +88 -0
  117. package/dist/kiro-reconcile.js.map +1 -0
  118. package/dist/kiro-session-proof.d.ts +41 -0
  119. package/dist/kiro-session-proof.js +89 -0
  120. package/dist/kiro-session-proof.js.map +1 -0
  121. package/dist/kiro-tui-forensic.d.ts +19 -0
  122. package/dist/kiro-tui-forensic.js +124 -0
  123. package/dist/kiro-tui-forensic.js.map +1 -0
  124. package/dist/locale.js +202 -16
  125. package/dist/locale.js.map +1 -1
  126. package/dist/log-service.js +3 -3
  127. package/dist/log-service.js.map +1 -1
  128. package/dist/needs-you.d.ts +3 -1
  129. package/dist/needs-you.js +4 -2
  130. package/dist/needs-you.js.map +1 -1
  131. package/dist/office-levels.d.ts +64 -0
  132. package/dist/office-levels.js +66 -0
  133. package/dist/office-levels.js.map +1 -0
  134. package/dist/office-share-page.d.ts +3 -1
  135. package/dist/office-share-page.js +8 -3
  136. package/dist/office-share-page.js.map +1 -1
  137. package/dist/office-share-service.d.ts +6 -3
  138. package/dist/office-share-service.js +12 -8
  139. package/dist/office-share-service.js.map +1 -1
  140. package/dist/office-share.d.ts +45 -8
  141. package/dist/office-share.js +75 -15
  142. package/dist/office-share.js.map +1 -1
  143. package/dist/outbound-handlers.js +21 -2
  144. package/dist/outbound-handlers.js.map +1 -1
  145. package/dist/outbound-schemas.d.ts +47 -2
  146. package/dist/outbound-schemas.js +42 -3
  147. package/dist/outbound-schemas.js.map +1 -1
  148. package/dist/public-web-gateway.js +1 -1
  149. package/dist/public-web-gateway.js.map +1 -1
  150. package/dist/quickstart-api.d.ts +39 -1
  151. package/dist/quickstart-api.js +115 -17
  152. package/dist/quickstart-api.js.map +1 -1
  153. package/dist/remote-card-answer.d.ts +83 -0
  154. package/dist/remote-card-answer.js +208 -0
  155. package/dist/remote-card-answer.js.map +1 -0
  156. package/dist/remote-card-prompt.d.ts +38 -0
  157. package/dist/remote-card-prompt.js +86 -0
  158. package/dist/remote-card-prompt.js.map +1 -0
  159. package/dist/scheduler/task-board.d.ts +44 -17
  160. package/dist/scheduler/task-board.js +95 -36
  161. package/dist/scheduler/task-board.js.map +1 -1
  162. package/dist/sd-notify.d.ts +13 -14
  163. package/dist/sd-notify.js +62 -28
  164. package/dist/sd-notify.js.map +1 -1
  165. package/dist/service-installer.d.ts +13 -0
  166. package/dist/service-installer.js +15 -0
  167. package/dist/service-installer.js.map +1 -1
  168. package/dist/service-owner.d.ts +25 -0
  169. package/dist/service-owner.js +43 -2
  170. package/dist/service-owner.js.map +1 -1
  171. package/dist/settings-api.d.ts +2 -1
  172. package/dist/settings-api.js +19 -26
  173. package/dist/settings-api.js.map +1 -1
  174. package/dist/settings-change.js +10 -5
  175. package/dist/settings-change.js.map +1 -1
  176. package/dist/settings-effect.js +7 -6
  177. package/dist/settings-effect.js.map +1 -1
  178. package/dist/settings-instance-patch.js +1 -1
  179. package/dist/settings-instance-patch.js.map +1 -1
  180. package/dist/setup-form.d.ts +2 -2
  181. package/dist/setup-form.js +139 -93
  182. package/dist/setup-form.js.map +1 -1
  183. package/dist/setup-i18n.d.ts +25 -0
  184. package/dist/setup-i18n.js +226 -0
  185. package/dist/setup-i18n.js.map +1 -0
  186. package/dist/task-board-waker.d.ts +40 -1
  187. package/dist/task-board-waker.js +115 -9
  188. package/dist/task-board-waker.js.map +1 -1
  189. package/dist/tmux-control.d.ts +1 -1
  190. package/dist/tmux-control.js +3 -2
  191. package/dist/tmux-control.js.map +1 -1
  192. package/dist/tmux-manager.js +18 -16
  193. package/dist/tmux-manager.js.map +1 -1
  194. package/dist/tmux-read.d.ts +2 -0
  195. package/dist/tmux-read.js +7 -1
  196. package/dist/tmux-read.js.map +1 -1
  197. package/dist/tool-permissions.js +18 -0
  198. package/dist/tool-permissions.js.map +1 -1
  199. package/dist/topic-commands.d.ts +2 -0
  200. package/dist/topic-commands.js +2 -1
  201. package/dist/topic-commands.js.map +1 -1
  202. package/dist/transcript-sources.d.ts +1 -0
  203. package/dist/transcript-sources.js +20 -3
  204. package/dist/transcript-sources.js.map +1 -1
  205. package/dist/types.d.ts +11 -5
  206. package/dist/ui/backend-signin.js +4 -3
  207. package/dist/ui/chat-store.js +3 -2
  208. package/dist/ui/chat-strings.js +2 -2
  209. package/dist/ui/fleet-office-card.js +114 -0
  210. package/dist/ui/fleet-office-editor.js +81 -9
  211. package/dist/ui/fleet-office-layout.js +56 -11
  212. package/dist/ui/fleet-office-levels.js +105 -0
  213. package/dist/ui/fleet-office-model.js +9 -1
  214. package/dist/ui/fleet-office-pack.d.ts +15 -3
  215. package/dist/ui/fleet-office-pack.js +177 -23
  216. package/dist/ui/fleet-office-skin.js +39 -3
  217. package/dist/ui/fleet-office-store.js +74 -3
  218. package/dist/ui/fleet-office-view.js +8 -0
  219. package/dist/ui/fleet-office-zones.js +102 -19
  220. package/dist/ui/fleet-office.js +308 -44
  221. package/dist/ui/fleet-org.js +2 -0
  222. package/dist/ui/fleet-task-card.js +2 -2
  223. package/dist/ui/fleet-task-common.js +30 -7
  224. package/dist/ui/fleet-task-work.js +2 -2
  225. package/dist/ui/fleet-tasks.js +10 -11
  226. package/dist/ui/office-share-dialog.js +31 -7
  227. package/dist/ui/office-share-page.js +1 -0
  228. package/dist/ui/panel-chat.js +14 -2
  229. package/dist/ui/panel-log.js +6 -6
  230. package/dist/ui/panel-needs.js +1 -1
  231. package/dist/ui/panel-settings.js +16 -8
  232. package/dist/ui/settings-appearance.js +3 -35
  233. package/dist/ui/settings-discord.js +3 -2
  234. package/dist/ui/settings-guests.js +17 -7
  235. package/dist/ui/settings-model.js +17 -9
  236. package/dist/ui/settings-registry.js +1 -1
  237. package/dist/ui/settings-strings.js +18 -6
  238. package/dist/ui/settings-token.js +3 -2
  239. package/dist/ui/settings-wizard.js +25 -9
  240. package/dist/ui/shared/app-i18n.js +2 -2
  241. package/dist/ui/shared/app-stream.js +1 -1
  242. package/dist/ui/shared/app.css +70 -2
  243. package/dist/ui/shared/setup-host-text.js +107 -0
  244. package/dist/ui/structured-signin.js +89 -38
  245. package/dist/update-check.d.ts +13 -4
  246. package/dist/update-check.js +17 -4
  247. package/dist/update-check.js.map +1 -1
  248. package/dist/usage-rollup.d.ts +34 -3
  249. package/dist/usage-rollup.js +56 -3
  250. package/dist/usage-rollup.js.map +1 -1
  251. package/dist/web-api.d.ts +10 -1
  252. package/dist/web-api.js +207 -17
  253. package/dist/web-api.js.map +1 -1
  254. package/dist/web-auth.js +1 -1
  255. package/dist/web-auth.js.map +1 -1
  256. package/dist/web-experimental.d.ts +6 -2
  257. package/dist/web-experimental.js +8 -4
  258. package/dist/web-experimental.js.map +1 -1
  259. package/dist/web-office-acts.d.ts +5 -2
  260. package/dist/web-office-acts.js +9 -4
  261. package/dist/web-office-acts.js.map +1 -1
  262. package/dist/web-office-pack-read.d.ts +35 -0
  263. package/dist/web-office-pack-read.js +252 -0
  264. package/dist/web-office-pack-read.js.map +1 -0
  265. package/dist/web-office-pack-tools.d.ts +241 -0
  266. package/dist/web-office-pack-tools.js +785 -0
  267. package/dist/web-office-pack-tools.js.map +1 -0
  268. package/dist/web-office-skins.d.ts +68 -4
  269. package/dist/web-office-skins.js +324 -33
  270. package/dist/web-office-skins.js.map +1 -1
  271. package/dist/web-office-store.d.ts +47 -4
  272. package/dist/web-office-store.js +124 -23
  273. package/dist/web-office-store.js.map +1 -1
  274. package/dist/web-settings-patch.d.ts +3 -2
  275. package/dist/web-settings-patch.js +7 -5
  276. package/dist/web-settings-patch.js.map +1 -1
  277. package/dist/workflow-templates/default.md +3 -3
  278. package/dist/working-recovery.d.ts +0 -2
  279. package/dist/working-recovery.js +2 -23
  280. package/dist/working-recovery.js.map +1 -1
  281. package/package.json +2 -2
  282. package/templates/windows-shortcuts.ps1 +18 -17
  283. package/dist/upgrade-notices.d.ts +0 -17
  284. package/dist/upgrade-notices.js +0 -93
  285. package/dist/upgrade-notices.js.map +0 -1
@@ -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
- // "door": [640, 700],
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 office (#2665,
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, 8, at).map((s, i) => one(s, `${at}[${i}]`)) : [one(v, at)];
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, label: f.label === undefined ? null : f.label.trim(), night: nightOf(f.night, `${at}.night`, scale),
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"), waypoints, edges, straight: x.straight === true, furniture, characters, byInstance,
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((s) => [s.x, s.y]);
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 }} OfficeLook
17
- // (one occupant; `free`: standing in a zone, not behind a desk cell)
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, and its style (skin); #2540: the view (zoom and centre) and the
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
- /** { rev, offices } from the host; null when it could not be read (no offices shown but the auto one). */
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