@songsid/agend 2.3.2-beta.8 → 2.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (243) hide show
  1. package/README.md +6 -0
  2. package/README.zh-TW.md +6 -0
  3. package/dist/agent-cli.js +48 -0
  4. package/dist/agent-cli.js.map +1 -1
  5. package/dist/agent-endpoint.js +6 -0
  6. package/dist/agent-endpoint.js.map +1 -1
  7. package/dist/agent-whats-new.js +3 -0
  8. package/dist/agent-whats-new.js.map +1 -1
  9. package/dist/auth-api.js +2 -0
  10. package/dist/auth-api.js.map +1 -1
  11. package/dist/backend/claude-code.js +23 -1
  12. package/dist/backend/claude-code.js.map +1 -1
  13. package/dist/backend/codex-session-lookup.d.ts +15 -0
  14. package/dist/backend/codex-session-lookup.js +58 -0
  15. package/dist/backend/codex-session-lookup.js.map +1 -1
  16. package/dist/backend/codex.js +15 -4
  17. package/dist/backend/codex.js.map +1 -1
  18. package/dist/backend/kiro-classic-archive.d.ts +57 -0
  19. package/dist/backend/kiro-classic-archive.js +622 -0
  20. package/dist/backend/kiro-classic-archive.js.map +1 -0
  21. package/dist/backend/kiro-classic-links.d.ts +24 -0
  22. package/dist/backend/kiro-classic-links.js +135 -0
  23. package/dist/backend/kiro-classic-links.js.map +1 -0
  24. package/dist/backend/kiro-identity.d.ts +100 -1
  25. package/dist/backend/kiro-identity.js +262 -31
  26. package/dist/backend/kiro-identity.js.map +1 -1
  27. package/dist/backend/kiro-migration-verify.d.ts +10 -0
  28. package/dist/backend/kiro-migration-verify.js +52 -0
  29. package/dist/backend/kiro-migration-verify.js.map +1 -0
  30. package/dist/backend/kiro-reconcile-store.d.ts +27 -0
  31. package/dist/backend/kiro-reconcile-store.js +344 -0
  32. package/dist/backend/kiro-reconcile-store.js.map +1 -0
  33. package/dist/backend/kiro-v2-store-worker.js +35 -1
  34. package/dist/backend/kiro-v2-store-worker.js.map +1 -1
  35. package/dist/backend/kiro-v2-store.d.ts +31 -1
  36. package/dist/backend/kiro-v2-store.js +22 -12
  37. package/dist/backend/kiro-v2-store.js.map +1 -1
  38. package/dist/backend/kiro.d.ts +124 -1
  39. package/dist/backend/kiro.js +553 -29
  40. package/dist/backend/kiro.js.map +1 -1
  41. package/dist/backend/types.d.ts +102 -0
  42. package/dist/backend/types.js +9 -0
  43. package/dist/backend/types.js.map +1 -1
  44. package/dist/cache-service.d.ts +13 -1
  45. package/dist/cache-service.js +45 -2
  46. package/dist/cache-service.js.map +1 -1
  47. package/dist/channel/ipc-timeouts.js +3 -0
  48. package/dist/channel/ipc-timeouts.js.map +1 -1
  49. package/dist/channel/mcp-tools.js +40 -5
  50. package/dist/channel/mcp-tools.js.map +1 -1
  51. package/dist/channel/tool-router.js +2 -1
  52. package/dist/channel/tool-router.js.map +1 -1
  53. package/dist/classic-channel-manager.d.ts +6 -0
  54. package/dist/classic-channel-manager.js +25 -0
  55. package/dist/classic-channel-manager.js.map +1 -1
  56. package/dist/cli.js +50 -4
  57. package/dist/cli.js.map +1 -1
  58. package/dist/config-validator.d.ts +7 -1
  59. package/dist/config-validator.js +17 -8
  60. package/dist/config-validator.js.map +1 -1
  61. package/dist/config.js +2 -0
  62. package/dist/config.js.map +1 -1
  63. package/dist/daemon.d.ts +66 -0
  64. package/dist/daemon.js +421 -19
  65. package/dist/daemon.js.map +1 -1
  66. package/dist/dashboard-permissions.d.ts +15 -4
  67. package/dist/dashboard-permissions.js +49 -23
  68. package/dist/dashboard-permissions.js.map +1 -1
  69. package/dist/doctor.js +2 -1
  70. package/dist/doctor.js.map +1 -1
  71. package/dist/fleet-control-audit.d.ts +1 -1
  72. package/dist/fleet-control-audit.js +3 -0
  73. package/dist/fleet-control-audit.js.map +1 -1
  74. package/dist/fleet-manager.d.ts +90 -3
  75. package/dist/fleet-manager.js +569 -41
  76. package/dist/fleet-manager.js.map +1 -1
  77. package/dist/forged-envelope-scan.d.ts +9 -0
  78. package/dist/forged-envelope-scan.js +30 -5
  79. package/dist/forged-envelope-scan.js.map +1 -1
  80. package/dist/general-instance.d.ts +24 -0
  81. package/dist/general-instance.js +53 -0
  82. package/dist/general-instance.js.map +1 -1
  83. package/dist/general-knowledge/skills/docker-install/SKILL.md +25 -0
  84. package/dist/general-knowledge/skills/fleet-config/SKILL.md +5 -3
  85. package/dist/general-knowledge/skills/fleet-restart/SKILL.md +1 -1
  86. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +34 -0
  87. package/dist/general-knowledge/skills/session-management/SKILL.md +1 -1
  88. package/dist/general-knowledge/skills/tui-effort/SKILL.md +1 -1
  89. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +32 -0
  90. package/dist/general-knowledge/skills/web-appearance/SKILL.md +32 -0
  91. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +60 -18
  92. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +60 -18
  93. package/dist/instance-config-impact.js +3 -2
  94. package/dist/instance-config-impact.js.map +1 -1
  95. package/dist/instance-lifecycle.d.ts +4 -0
  96. package/dist/instance-lifecycle.js +7 -0
  97. package/dist/instance-lifecycle.js.map +1 -1
  98. package/dist/kiro-migrate.d.ts +19 -2
  99. package/dist/kiro-migrate.js +133 -35
  100. package/dist/kiro-migrate.js.map +1 -1
  101. package/dist/kiro-reconcile.d.ts +21 -0
  102. package/dist/kiro-reconcile.js +88 -0
  103. package/dist/kiro-reconcile.js.map +1 -0
  104. package/dist/kiro-session-proof.d.ts +41 -0
  105. package/dist/kiro-session-proof.js +89 -0
  106. package/dist/kiro-session-proof.js.map +1 -0
  107. package/dist/kiro-tui-forensic.d.ts +19 -0
  108. package/dist/kiro-tui-forensic.js +124 -0
  109. package/dist/kiro-tui-forensic.js.map +1 -0
  110. package/dist/locale.js +198 -0
  111. package/dist/locale.js.map +1 -1
  112. package/dist/log-service.js +2 -2
  113. package/dist/log-service.js.map +1 -1
  114. package/dist/needs-you.d.ts +3 -1
  115. package/dist/needs-you.js +4 -2
  116. package/dist/needs-you.js.map +1 -1
  117. package/dist/office-levels.d.ts +64 -0
  118. package/dist/office-levels.js +66 -0
  119. package/dist/office-levels.js.map +1 -0
  120. package/dist/office-share-page.d.ts +3 -1
  121. package/dist/office-share-page.js +8 -3
  122. package/dist/office-share-page.js.map +1 -1
  123. package/dist/office-share-service.d.ts +6 -3
  124. package/dist/office-share-service.js +12 -8
  125. package/dist/office-share-service.js.map +1 -1
  126. package/dist/office-share.d.ts +80 -13
  127. package/dist/office-share.js +145 -25
  128. package/dist/office-share.js.map +1 -1
  129. package/dist/outbound-handlers.js +20 -1
  130. package/dist/outbound-handlers.js.map +1 -1
  131. package/dist/outbound-schemas.d.ts +47 -2
  132. package/dist/outbound-schemas.js +41 -2
  133. package/dist/outbound-schemas.js.map +1 -1
  134. package/dist/public-web-gateway.js +1 -1
  135. package/dist/public-web-gateway.js.map +1 -1
  136. package/dist/quickstart-api.d.ts +39 -1
  137. package/dist/quickstart-api.js +115 -17
  138. package/dist/quickstart-api.js.map +1 -1
  139. package/dist/remote-card-answer.d.ts +83 -0
  140. package/dist/remote-card-answer.js +208 -0
  141. package/dist/remote-card-answer.js.map +1 -0
  142. package/dist/remote-card-prompt.d.ts +38 -0
  143. package/dist/remote-card-prompt.js +86 -0
  144. package/dist/remote-card-prompt.js.map +1 -0
  145. package/dist/scheduler/task-board.d.ts +28 -6
  146. package/dist/scheduler/task-board.js +66 -11
  147. package/dist/scheduler/task-board.js.map +1 -1
  148. package/dist/service-installer.d.ts +13 -0
  149. package/dist/service-installer.js +15 -0
  150. package/dist/service-installer.js.map +1 -1
  151. package/dist/service-owner.d.ts +25 -0
  152. package/dist/service-owner.js +43 -2
  153. package/dist/service-owner.js.map +1 -1
  154. package/dist/settings-api.d.ts +2 -0
  155. package/dist/settings-api.js +18 -12
  156. package/dist/settings-api.js.map +1 -1
  157. package/dist/settings-change.js +10 -5
  158. package/dist/settings-change.js.map +1 -1
  159. package/dist/settings-effect.js +7 -4
  160. package/dist/settings-effect.js.map +1 -1
  161. package/dist/settings-instance-patch.js +1 -1
  162. package/dist/settings-instance-patch.js.map +1 -1
  163. package/dist/setup-form.d.ts +2 -2
  164. package/dist/setup-form.js +139 -93
  165. package/dist/setup-form.js.map +1 -1
  166. package/dist/setup-i18n.d.ts +25 -0
  167. package/dist/setup-i18n.js +226 -0
  168. package/dist/setup-i18n.js.map +1 -0
  169. package/dist/task-board-waker.js +7 -2
  170. package/dist/task-board-waker.js.map +1 -1
  171. package/dist/tool-permissions.js +18 -0
  172. package/dist/tool-permissions.js.map +1 -1
  173. package/dist/transcript-sources.d.ts +1 -0
  174. package/dist/transcript-sources.js +20 -3
  175. package/dist/transcript-sources.js.map +1 -1
  176. package/dist/types.d.ts +10 -0
  177. package/dist/ui/backend-signin.js +4 -3
  178. package/dist/ui/chat-store.js +3 -2
  179. package/dist/ui/chat-strings.js +2 -2
  180. package/dist/ui/fleet-office-card.js +114 -0
  181. package/dist/ui/fleet-office-editor.js +81 -9
  182. package/dist/ui/fleet-office-layout.js +87 -10
  183. package/dist/ui/fleet-office-levels.js +105 -0
  184. package/dist/ui/fleet-office-model.js +9 -1
  185. package/dist/ui/fleet-office-pack.d.ts +18 -3
  186. package/dist/ui/fleet-office-pack.js +217 -23
  187. package/dist/ui/fleet-office-skin.js +39 -3
  188. package/dist/ui/fleet-office-store.js +74 -3
  189. package/dist/ui/fleet-office-view.js +8 -0
  190. package/dist/ui/fleet-office-zones.js +102 -19
  191. package/dist/ui/fleet-office.js +323 -50
  192. package/dist/ui/fleet-org.js +2 -0
  193. package/dist/ui/fleet-task-card.js +2 -2
  194. package/dist/ui/fleet-task-common.js +30 -7
  195. package/dist/ui/fleet-task-work.js +2 -2
  196. package/dist/ui/fleet-tasks.js +10 -11
  197. package/dist/ui/office-share-dialog.js +31 -7
  198. package/dist/ui/office-share-page.js +50 -1
  199. package/dist/ui/panel-chat.js +14 -2
  200. package/dist/ui/panel-log.js +4 -4
  201. package/dist/ui/panel-needs.js +1 -1
  202. package/dist/ui/panel-settings.js +16 -8
  203. package/dist/ui/settings-discord.js +3 -2
  204. package/dist/ui/settings-guests.js +17 -7
  205. package/dist/ui/settings-model.js +17 -9
  206. package/dist/ui/settings-registry.js +1 -0
  207. package/dist/ui/settings-strings.js +16 -4
  208. package/dist/ui/settings-token.js +3 -2
  209. package/dist/ui/settings-wizard.js +25 -9
  210. package/dist/ui/shared/app-i18n.js +2 -2
  211. package/dist/ui/shared/app-stream.js +1 -1
  212. package/dist/ui/shared/app.css +74 -2
  213. package/dist/ui/shared/setup-host-text.js +107 -0
  214. package/dist/ui/structured-signin.js +89 -38
  215. package/dist/usage-rollup.d.ts +34 -3
  216. package/dist/usage-rollup.js +56 -3
  217. package/dist/usage-rollup.js.map +1 -1
  218. package/dist/web-api.d.ts +10 -1
  219. package/dist/web-api.js +207 -17
  220. package/dist/web-api.js.map +1 -1
  221. package/dist/web-auth.js +1 -1
  222. package/dist/web-auth.js.map +1 -1
  223. package/dist/web-office-acts.d.ts +5 -2
  224. package/dist/web-office-acts.js +9 -4
  225. package/dist/web-office-acts.js.map +1 -1
  226. package/dist/web-office-pack-read.d.ts +35 -0
  227. package/dist/web-office-pack-read.js +252 -0
  228. package/dist/web-office-pack-read.js.map +1 -0
  229. package/dist/web-office-pack-tools.d.ts +241 -0
  230. package/dist/web-office-pack-tools.js +785 -0
  231. package/dist/web-office-pack-tools.js.map +1 -0
  232. package/dist/web-office-skins.d.ts +68 -4
  233. package/dist/web-office-skins.js +324 -33
  234. package/dist/web-office-skins.js.map +1 -1
  235. package/dist/web-office-store.d.ts +47 -4
  236. package/dist/web-office-store.js +149 -15
  237. package/dist/web-office-store.js.map +1 -1
  238. package/dist/web-settings-patch.d.ts +3 -1
  239. package/dist/web-settings-patch.js +4 -1
  240. package/dist/web-settings-patch.js.map +1 -1
  241. package/dist/workflow-templates/default.md +1 -1
  242. package/package.json +2 -2
  243. package/templates/windows-shortcuts.ps1 +18 -17
@@ -24,10 +24,21 @@
24
24
  // "areas": { named zones: a rect [x, y, w, h] and its seats, in the order
25
25
  // "desks": { "rect": [40, 120, 620, 440], "seats": [[120, 220], [260, 220], …] }, they fill up
26
26
  // "lounge": { "rect": [700, 120, 540, 260], "seats": [[760, 260], …] } },
27
+ // a seat is [x, y], or (#2691) { "x": 760, "y": 260, "for":
28
+ // ["atlas", …], "lend": false }: reserved for those instances
29
+ // (names checked like byInstance's, ≤ 16, none twice) — whoever
30
+ // of them comes to the area takes it first, and nobody else does
31
+ // unless "lend" (default false) lends it while they are all
32
+ // elsewhere (a borrower moves off when one comes). A checked
33
+ // seat is { x, y } — plus, reserved, `for` and `lend`
27
34
  // "stateZones": { "working": "desks", "idle": "lounge", "needs": "alert", "default": "lounge" },
28
35
  // the org chart's states (fleet-org.js ORG_STATES) → an area; a
29
36
  // state it does not name goes to "default", else the first area
30
37
  // "door": [640, 700], optional: where someone new walks in from (default: bottom middle)
38
+ // "mailDrop": [640, 420], optional (#2736): where a letter from another office lands (more
39
+ // waiting step GAP px right of it, past any seat); default: the
40
+ // waypoint nearest the middle off every seat (fleet-office-zones.js
41
+ // dropPoint)
31
42
  // "waypoints": [{ "id": "hall", "x": 640, "y": 600 }, …], "edges": [["hall", "east"], …],
32
43
  // optional: the walkable graph (else people walk straight)
33
44
  // "straight": true, optional (#2634): every walk goes across, then up or down — each
@@ -35,7 +46,7 @@
35
46
  // always fit (graph edges laid out across and up/down put the
36
47
  // corners on the aisles); default false
37
48
  // "furniture": [ { "id": "rack", "sprite": STRIP, "x": 1020, "y": 140, "depth": 2,
38
- // "activeWhen": "crashed", "activeSprite": STRIP, "activeFor": "atlas", "loop": true, "label": "Rack",
49
+ // "activeWhen": "crashed", "activeSprite": STRIP, "activeFor": "atlas", "loop": true, "activeOn": "seated", "hides": true, "label": "Rack",
39
50
  // "night": { "sprite": STRIP, "x": 300, "y": 420 },
40
51
  // "walk": { "walk": STRIP, "walkDown": STRIP, "walkUp": STRIP, "walkSide": STRIP } } ],
41
52
  // "label" (#2574): a name tag under it, like a person's
@@ -55,7 +66,14 @@
55
66
  // strip (or `activeSprite`); otherwise it holds the first frame —
56
67
  // or, with "loop": true (#2570), plays its strip all along.
57
68
  // "activeFor" (an instance's name, or a list): only their state
58
- // counts, not anyone's (a room several people take turns in)
69
+ // counts, not anyone's (a room several people take turns in).
70
+ // "activeOn" (#2698): "state" (the default) turns it active as the
71
+ // state changes; "seated" only once one of them in that state is
72
+ // at the seat zones gave them, not walking — back as they walk off.
73
+ // "hides": true (#2698, only with activeFor): while active, the
74
+ // people it is active for are not drawn (walking, they always
75
+ // are); their name tags and bubbles are drawn on it, in one row at
76
+ // its tag point, and hovering or clicking it hits them
59
77
  // "characters": { "default": { "working": STRIP, "idle": STRIP, "paused": STRIP, "needs": STRIP, "walk": STRIP,
60
78
  // "walkDown": STRIP, "walkUp": STRIP, "walkSide": STRIP },
61
79
  // "fox": { … } }, "default" with at least "idle" is required; a missing pose is idle.
@@ -73,6 +91,13 @@
73
91
  // ≤ 40 characters each), in place of the page's own
74
92
  // "room": { "floor": "floor.png", "wall": "wall.png" }, "desk": { "back": …, "front": …, "lit": … }
75
93
  // optional: for the desks layout (a pack without stateZones)
94
+ // "levels": { optional (#2661): the Office levels' words
95
+ // "titles": [{ "from": 1, "name": "Intern" }, …], the title from each level up (≤ 32, `from` 0–100000
96
+ // rising, a name 1–24 characters), in place of the page's own
97
+ // "fixed": { "atlas": { "title": "Founder", "hideLevel": true } } }
98
+ // a title pinned on an instance (or an NPC: a furniture item's
99
+ // id) whatever its level (≤ 500); hideLevel: its badge is a mark,
100
+ // its card has no "Lv n" or XP bar
76
101
  // }
77
102
  //
78
103
  // A STRIP is { "src": "idle.png", "frames": 4, "w": 32, "h": 48, "fps": 4, "ax": 16, "ay": 44 }: `frames` frames of
@@ -102,14 +127,21 @@
102
127
  // { "pack": "cozy", the pack they were made on (on another pack they are ignored)
103
128
  // "seats": { "desks": [[120, 220], …] }, an area's seats: as many as the pack's, each inside the area's rect
104
129
  // "furniture": { "rack": [1000, 150] }, an item's (x, y)
105
- // "door": [640, 700],
130
+ // "furnitureNight": { "elder": [300, 420] }, an item's night place (#2665: an item with a "night" only)
131
+ // "seatFor": { "desks": { "0": { "for": ["atlas"], "lend": false } } },
132
+ // a seat's reservation by area and slot (#2691), in place of the
133
+ // pack's own for that seat; "for": [] is none (lends nothing)
134
+ // "door": [640, 700], "mailDrop": [640, 420], (#2736: kept beside the offices on disk, as furnitureNight)
106
135
  // "waypoints": [{ "id": "hall", "x": 640, "y": 600 }, …], "edges": [["hall", "east"], …] }
107
136
  // the walkable graph, replaced whole (edges need waypoints)
108
137
  //
109
138
  // Every coordinate is on the same 1280×720 logical canvas, in the same ranges as the pack's. checkLayoutOverrides is
110
139
  // the shape (the host checks it on every PUT /ui/offices); applyOverrides puts them on a checked pack, refusing a
111
140
  // seat outside its area, an area or furniture the pack does not have, or a seat count that differs (the host checks
112
- // that too, for a pack it keeps).
141
+ // that too, for a pack it keeps). A night place goes on the item's `night` (its x, y), so at bedtime (nightFurniture)
142
+ // it is the office's, else the pack's night x/y, else the item's (moved) day place. The host keeps `furnitureNight`
143
+ // and `seatFor` (#2691) beside the offices on disk, never inside them: an older host's stricter check would drop the
144
+ // office (#2665, web-office-store.ts).
113
145
 
114
146
  export const LOGICAL = { w: 1280, h: 720 };
115
147
  /** The people's depth: furniture below it is drawn under them, the rest over them. */
@@ -129,7 +161,7 @@ const RESERVED = new Set(["cozy"]);
129
161
  const ID = /^[A-Za-z0-9_-]{1,40}$/;
130
162
  const SRC = /^[A-Za-z0-9._/-]{1,200}$/;
131
163
  const HEX = /^#[0-9a-f]{6}$/i;
132
- const LIMIT = { areas: 32, seats: 64, furniture: 128, waypoints: 512, edges: 2048, sets: 64, byInstance: 500, lines: 12, line: 40 };
164
+ const LIMIT = { areas: 32, seats: 64, furniture: 128, waypoints: 512, edges: 2048, sets: 64, byInstance: 500, lines: 12, line: 40, seatFor: 16, titles: 32, title: 24, fixed: 500 };
133
165
 
134
166
  class Refused extends Error { constructor(at, why) { super(`${at}: ${why}`); } }
135
167
  /** `o[k]` when it is o's own property, else undefined: a name from a pack or a page never reaches Object.prototype. */
@@ -180,10 +212,33 @@ function walkOf(v, at, scale) {
180
212
  if (!w.walk) throw new Refused(`${at}.walk`, "required");
181
213
  return w;
182
214
  }
183
- /** Whose state an item's activeWhen watches (#2570): an instance's name, or a list of up to 8. */
184
- function names(v, at) {
215
+ /** Whose state an item's activeWhen watches (#2570): an instance's name, or a list of up to `max` (8). */
216
+ function names(v, at, max = 8) {
185
217
  const one = (s, here) => { if (typeof s !== "string" || !s || s.length > 200 || inherited(s) || /[\u0000-\u001f\u007f]/.test(s)) throw new Refused(here, "an instance's name"); return s; };
186
- return Array.isArray(v) ? list(v, 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 };
187
242
  }
188
243
  function strips(v, names, at, scale) {
189
244
  obj(v, at);
@@ -195,6 +250,43 @@ function states(v, at) {
195
250
  return Array.isArray(v) ? list(v, 8, at).map((s, i) => one(s, `${at}[${i}]`)) : [one(v, at)];
196
251
  }
197
252
 
253
+ /** #2661: a title's words — 1–LIMIT.title characters on one line. */
254
+ function titleText(v, at) {
255
+ if (typeof v !== "string" || !v.trim() || [...v.trim()].length > LIMIT.title || /[\u0000-\u001f\u007f]/.test(v)) throw new Refused(at, `1–${LIMIT.title} characters`);
256
+ return v.trim();
257
+ }
258
+ /** #2661: a pack's `levels` (titles, fixed), checked; null when it has none. */
259
+ function levelsOf(v) {
260
+ if (v === undefined) return null;
261
+ obj(v, "levels");
262
+ keys(v, ["titles", "fixed"], "levels");
263
+ const out = {};
264
+ if (v.titles !== undefined) {
265
+ let last = -1;
266
+ out.titles = list(v.titles, LIMIT.titles, "levels.titles").map((t, i) => {
267
+ const at = `levels.titles[${i}]`;
268
+ obj(t, at); keys(t, ["from", "name"], at);
269
+ const from = int(t.from, 0, 100000, `${at}.from`);
270
+ if (from <= last) throw new Refused(`${at}.from`, "higher than the title before it");
271
+ last = from;
272
+ return { from, name: titleText(t.name, `${at}.name`) };
273
+ });
274
+ }
275
+ if (v.fixed !== undefined) {
276
+ obj(v.fixed, "levels.fixed");
277
+ if (Object.keys(v.fixed).length > LIMIT.fixed) throw new Refused("levels.fixed", `at most ${LIMIT.fixed}`);
278
+ out.fixed = {};
279
+ for (const [k, f] of Object.entries(v.fixed)) {
280
+ const at = `levels.fixed.${k}`;
281
+ if (inherited(k)) throw new Refused(at, "an instance's name (not an inherited one)");
282
+ obj(f, at); keys(f, ["title", "hideLevel"], at);
283
+ if (f.hideLevel !== undefined && typeof f.hideLevel !== "boolean") throw new Refused(`${at}.hideLevel`, "true or false");
284
+ out.fixed[k] = { title: titleText(f.title, `${at}.title`), hideLevel: f.hideLevel === true };
285
+ }
286
+ }
287
+ return out;
288
+ }
289
+
198
290
  /**
199
291
  * A pack's manifest, strictly: { ok: true, pack } or { ok: false, error: "<where>: <why>" }. `kept`: a pack the host
200
292
  * keeps already (read back, not uploaded), which may hold a RESERVED id.
@@ -202,7 +294,7 @@ function states(v, at) {
202
294
  export function checkSkinPack(x, { kept = false } = {}) {
203
295
  try {
204
296
  obj(x, "pack");
205
- keys(x, ["format", "id", "scale", "background", "foreground", "matte", "areas", "stateZones", "door", "waypoints", "edges", "straight", "furniture", "characters", "byInstance", "props", "bubbles", "room", "desk"], "pack");
297
+ keys(x, ["format", "id", "scale", "background", "foreground", "matte", "areas", "stateZones", "door", "mailDrop", "waypoints", "edges", "straight", "furniture", "characters", "byInstance", "props", "bubbles", "room", "desk", "levels"], "pack");
206
298
  if (x.format !== 1) throw new Refused("format", "1");
207
299
  if (typeof x.id !== "string" || !ID.test(x.id)) throw new Refused("id", "1–40 letters, digits, _ or -");
208
300
  if (BUILT_IN.has(x.id) || (!kept && RESERVED.has(x.id))) throw new Refused("id", "a built-in skin's id");
@@ -219,11 +311,7 @@ export function checkSkinPack(x, { kept = false } = {}) {
219
311
  if (!Array.isArray(a.rect) || a.rect.length !== 4) throw new Refused(`${at}.rect`, "[x, y, w, h]");
220
312
  const [rx, ry] = [num(a.rect[0], 0, LOGICAL.w, `${at}.rect[0]`), num(a.rect[1], 0, LOGICAL.h, `${at}.rect[1]`)];
221
313
  const rect = { x: rx, y: ry, w: num(a.rect[2], 1, LOGICAL.w - rx, `${at}.rect[2]`), h: num(a.rect[3], 1, LOGICAL.h - ry, `${at}.rect[3]`) };
222
- const seats = list(a.seats, LIMIT.seats, `${at}.seats`).map((s, i) => {
223
- const p = point(s, `${at}.seats[${i}]`);
224
- if (p.x < rect.x || p.x > rect.x + rect.w || p.y < rect.y || p.y > rect.y + rect.h) throw new Refused(`${at}.seats[${i}]`, "outside its area's rect");
225
- return p;
226
- });
314
+ const seats = list(a.seats, LIMIT.seats, `${at}.seats`).map((s, i) => seatOf(s, `${at}.seats[${i}]`, rect));
227
315
  if (!seats.length) throw new Refused(`${at}.seats`, "at least one seat");
228
316
  areas[name] = { name, rect, seats };
229
317
  }
@@ -255,17 +343,22 @@ export function checkSkinPack(x, { kept = false } = {}) {
255
343
  const fids = new Set();
256
344
  const furniture = list(x.furniture ?? [], LIMIT.furniture, "furniture").map((f, i) => {
257
345
  const at = `furniture[${i}]`;
258
- obj(f, at); keys(f, ["id", "sprite", "x", "y", "depth", "activeWhen", "activeSprite", "activeFor", "loop", "label", "night", "walk"], at);
346
+ obj(f, at); keys(f, ["id", "sprite", "x", "y", "depth", "activeWhen", "activeSprite", "activeFor", "loop", "activeOn", "hides", "label", "night", "walk"], at);
259
347
  if (!nameOk(f.id) || fids.has(f.id)) throw new Refused(`${at}.id`, "a new name of letters, digits, _ or - (not an inherited one)");
260
348
  fids.add(f.id);
261
349
  if (f.activeSprite !== undefined && f.activeWhen === undefined) throw new Refused(`${at}.activeSprite`, "only with activeWhen");
262
350
  if (f.activeFor !== undefined && f.activeWhen === undefined) throw new Refused(`${at}.activeFor`, "only with activeWhen");
263
351
  if (f.loop !== undefined && typeof f.loop !== "boolean") throw new Refused(`${at}.loop`, "true or false");
352
+ if (f.activeOn !== undefined && f.activeOn !== "state" && f.activeOn !== "seated") throw new Refused(`${at}.activeOn`, '"state" or "seated"');
353
+ if (f.activeOn !== undefined && f.activeWhen === undefined) throw new Refused(`${at}.activeOn`, "only with activeWhen");
354
+ if (f.hides !== undefined && typeof f.hides !== "boolean") throw new Refused(`${at}.hides`, "true or false");
355
+ if (f.hides !== undefined && f.activeFor === undefined) throw new Refused(`${at}.hides`, "only with activeFor");
264
356
  if (f.label !== undefined && (typeof f.label !== "string" || !f.label.trim() || [...f.label].length > LIMIT.line || /[\u0000-\u001f\u007f]/.test(f.label))) throw new Refused(`${at}.label`, `1–${LIMIT.line} characters`);
265
357
  return { id: f.id, sprite: checkStrip(f.sprite, `${at}.sprite`, scale), x: num(f.x, 0, LOGICAL.w, `${at}.x`), y: num(f.y, 0, LOGICAL.h, `${at}.y`),
266
358
  depth: f.depth === "y" ? "y" : depth(f.depth ?? 0, `${at}.depth`), activeWhen: f.activeWhen === undefined ? null : states(f.activeWhen, `${at}.activeWhen`),
267
359
  activeSprite: f.activeSprite === undefined ? null : checkStrip(f.activeSprite, `${at}.activeSprite`, scale),
268
- activeFor: f.activeFor === undefined ? null : names(f.activeFor, `${at}.activeFor`), loop: f.loop === true, 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),
269
362
  walk: walkOf(f.walk, `${at}.walk`, scale) };
270
363
  }).sort((a, b) => order(a) - order(b));
271
364
  const sets = obj(x.characters, "characters");
@@ -299,14 +392,17 @@ export function checkSkinPack(x, { kept = false } = {}) {
299
392
  });
300
393
  }
301
394
  }
395
+ const levels = levelsOf(x.levels);
302
396
  const room = x.room === undefined ? {} : obj(x.room, "room"), desk = x.desk === undefined ? {} : obj(x.desk, "desk");
303
397
  keys(room, ["floor", "wall"], "room"); keys(desk, ["back", "front", "lit"], "desk");
304
398
  const opt = (v, at) => (v === undefined ? undefined : src(v, at));
305
399
  return { ok: true, pack: {
306
400
  format: 1, id: x.id, scale, background: opt(x.background, "background"), foreground: opt(x.foreground, "foreground"), matte: x.matte === undefined ? undefined : x.matte.toLowerCase(), areas, stateZones,
307
- door: x.door === undefined ? { x: LOGICAL.w / 2, y: LOGICAL.h - 10 } : point(x.door, "door"), 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,
308
403
  props: x.props === undefined ? {} : strips(x.props, PROPS, "props", scale), bubbles,
309
404
  room: { floor: opt(room.floor, "room.floor"), wall: opt(room.wall, "room.wall") }, desk: { back: opt(desk.back, "desk.back"), front: opt(desk.front, "desk.front"), lit: opt(desk.lit, "desk.lit") },
405
+ ...(levels ? { levels } : {}),
310
406
  } };
311
407
  } catch (err) {
312
408
  if (err instanceof Refused) return { ok: false, error: err.message };
@@ -337,11 +433,17 @@ export function packStrips(pack) {
337
433
  // ── Layout overrides ──
338
434
 
339
435
  const OV_LIMIT = { areas: LIMIT.areas, seats: LIMIT.seats, furniture: LIMIT.furniture, waypoints: LIMIT.waypoints, edges: LIMIT.edges };
436
+ /** A seat's slot in `seatFor` (#2691): its index in the area's order, as a key ("0", "1", …). */
437
+ const SLOT = /^(0|[1-9][0-9]{0,2})$/;
438
+ /** A seat's reservation as an override keeps it: { for, lend }, or null (not reserved). */
439
+ const reservedOf = (s) => (s.for && s.for.length ? { for: [...s.for], lend: s.lend === true } : null);
440
+ /** A seat with reservation `r` ({ for, lend }; for [] or null: none) in place of its own. */
441
+ const reserve = (s, r) => { const { for: _f, lend: _l, ...p } = s; return r && r.for.length ? { ...p, for: [...r.for], lend: r.lend === true } : p; };
340
442
  /** An office's layout overrides, strictly (their shape and ranges): { ok: true, overrides } or { ok: false, error }. */
341
443
  export function checkLayoutOverrides(x) {
342
444
  try {
343
445
  obj(x, "overrides");
344
- keys(x, ["pack", "seats", "furniture", "door", "waypoints", "edges"], "overrides");
446
+ keys(x, ["pack", "seats", "furniture", "furnitureNight", "seatFor", "door", "mailDrop", "waypoints", "edges"], "overrides");
345
447
  if (typeof x.pack !== "string" || !ID.test(x.pack)) throw new Refused("overrides.pack", "1–40 letters, digits, _ or -");
346
448
  const out = { pack: x.pack };
347
449
  if (x.seats !== undefined) {
@@ -366,7 +468,38 @@ export function checkLayoutOverrides(x) {
366
468
  out.furniture[id] = [q.x, q.y];
367
469
  }
368
470
  }
471
+ if (x.furnitureNight !== undefined) {
472
+ obj(x.furnitureNight, "overrides.furnitureNight");
473
+ if (Object.keys(x.furnitureNight).length > OV_LIMIT.furniture) throw new Refused("overrides.furnitureNight", `at most ${OV_LIMIT.furniture}`);
474
+ out.furnitureNight = {};
475
+ for (const [id, p] of Object.entries(x.furnitureNight)) {
476
+ if (!nameOk(id)) throw new Refused(`overrides.furnitureNight.${id}`, "an item's id");
477
+ const q = point(p, `overrides.furnitureNight.${id}`);
478
+ out.furnitureNight[id] = [q.x, q.y];
479
+ }
480
+ }
481
+ if (x.seatFor !== undefined) {
482
+ obj(x.seatFor, "overrides.seatFor");
483
+ if (Object.keys(x.seatFor).length > OV_LIMIT.areas) throw new Refused("overrides.seatFor", `at most ${OV_LIMIT.areas} areas`);
484
+ out.seatFor = {};
485
+ for (const [area, slots] of Object.entries(x.seatFor)) {
486
+ const at = `overrides.seatFor.${area}`;
487
+ if (!nameOk(area)) throw new Refused(at, "an area's name");
488
+ obj(slots, at);
489
+ if (Object.keys(slots).length > OV_LIMIT.seats) throw new Refused(at, `at most ${OV_LIMIT.seats} seats`);
490
+ out.seatFor[area] = {};
491
+ for (const [slot, r] of Object.entries(slots)) {
492
+ const here = `${at}.${slot}`;
493
+ if (!SLOT.test(slot) || Number(slot) >= OV_LIMIT.seats) throw new Refused(here, `a seat's number, 0 to ${OV_LIMIT.seats - 1}`);
494
+ obj(r, here); keys(r, ["for", "lend"], here);
495
+ if (!Array.isArray(r.for)) throw new Refused(`${here}.for`, "a list");
496
+ if (r.lend !== undefined && typeof r.lend !== "boolean") throw new Refused(`${here}.lend`, "true or false");
497
+ out.seatFor[area][slot] = { for: owners(r.for, `${here}.for`, true), lend: r.lend === true };
498
+ }
499
+ }
500
+ }
369
501
  if (x.door !== undefined) { const q = point(x.door, "overrides.door"); out.door = [q.x, q.y]; }
502
+ if (x.mailDrop !== undefined) { const q = point(x.mailDrop, "overrides.mailDrop"); out.mailDrop = [q.x, q.y]; }
370
503
  if (x.edges !== undefined && x.waypoints === undefined) throw new Refused("overrides.edges", "only with waypoints");
371
504
  if (x.waypoints !== undefined) {
372
505
  const ids = new Set();
@@ -408,21 +541,68 @@ export function applyOverrides(pack, ov) {
408
541
  const r = a.rect;
409
542
  areas[name] = { ...a, seats: pts.map(([x, y], i) => {
410
543
  if (x < r.x || x > r.x + r.w || y < r.y || y > r.y + r.h) throw new Refused(`${at}[${i}]`, "outside its area's rect");
411
- return { x, y };
544
+ return { ...a.seats[i], x, y }; // its reservation kept (#2691)
412
545
  }) };
413
546
  }
547
+ for (const [name, slots] of Object.entries(ov.seatFor || {})) {
548
+ const a = own(areas, name), at = `overrides.seatFor.${name}`;
549
+ if (!a) throw new Refused(at, "an area the pack defines");
550
+ for (const slot of Object.keys(slots)) if (!a.seats[Number(slot)]) throw new Refused(`${at}.${slot}`, `a seat the area has (it has ${a.seats.length})`);
551
+ areas[name] = { ...a, seats: a.seats.map((s, i) => { const v = own(slots, String(i)); return v ? reserve(s, v) : s; }) };
552
+ }
414
553
  const fids = new Set(pack.furniture.map((f) => f.id));
415
554
  for (const id of Object.keys(ov.furniture || {})) if (!fids.has(id)) throw new Refused(`overrides.furniture.${id}`, "an item the pack has");
416
- const furniture = pack.furniture.map((f) => { const q = own(ov.furniture, f.id); return q ? { ...f, x: q[0], y: q[1] } : f; });
555
+ const nids = new Set(pack.furniture.filter((f) => f.night).map((f) => f.id));
556
+ for (const id of Object.keys(ov.furnitureNight || {})) if (!nids.has(id)) throw new Refused(`overrides.furnitureNight.${id}`, "an item with a night the pack has");
557
+ const furniture = pack.furniture.map((f) => {
558
+ const q = own(ov.furniture, f.id), n = own(ov.furnitureNight, f.id);
559
+ if (!q && !n) return f;
560
+ return { ...f, ...(q ? { x: q[0], y: q[1] } : {}), ...(n ? { night: { ...f.night, x: n[0], y: n[1] } } : {}) };
561
+ });
417
562
  const door = ov.door ? { x: ov.door[0], y: ov.door[1] } : pack.door;
563
+ const mailDrop = ov.mailDrop ? { x: ov.mailDrop[0], y: ov.mailDrop[1] } : pack.mailDrop ?? null;
418
564
  const graph = ov.waypoints ? { waypoints: ov.waypoints.map((w) => ({ ...w })), edges: (ov.edges || []).map((e) => [e[0], e[1]]) } : { waypoints: pack.waypoints, edges: pack.edges };
419
- return { ok: true, pack: { ...pack, areas, furniture, door, ...graph } };
565
+ return { ok: true, pack: { ...pack, areas, furniture, door, mailDrop, ...graph } };
420
566
  } catch (err) {
421
567
  if (err instanceof Refused) return { ok: false, error: err.message };
422
568
  throw err;
423
569
  }
424
570
  }
425
571
 
572
+ /**
573
+ * #2665: overrides with only the night places `pack` can take (an item it has, with a `night`): a pack changed since
574
+ * (an item dropped, or its night) drops that entry, never the rest of the layout. applyOverrides itself stays strict
575
+ * (a write naming an item the pack lacks is refused, as for `furniture`).
576
+ */
577
+ export function fitNight(pack, ov) {
578
+ if (!ov || !ov.furnitureNight || ov.pack !== pack.id) return ov;
579
+ const ok = new Set(pack.furniture.filter((f) => f.night).map((f) => f.id));
580
+ const kept = Object.entries(ov.furnitureNight).filter(([id]) => ok.has(id));
581
+ if (kept.length === Object.keys(ov.furnitureNight).length) return ov;
582
+ const { furnitureNight: _gone, ...rest } = ov;
583
+ return kept.length ? { ...rest, furnitureNight: Object.fromEntries(kept) } : rest;
584
+ }
585
+
586
+ /**
587
+ * #2691: overrides with only the reservations `pack` can take (an area it has, a seat that area has): a pack changed
588
+ * since drops that entry, never the rest of the layout (as fitNight; applyOverrides stays strict).
589
+ */
590
+ export function fitSeatFor(pack, ov) {
591
+ if (!ov || !ov.seatFor || ov.pack !== pack.id) return ov;
592
+ let dropped = false;
593
+ const kept = {};
594
+ for (const [area, slots] of Object.entries(ov.seatFor)) {
595
+ const a = own(pack.areas, area);
596
+ const ok = Object.entries(slots).filter(([slot]) => a && !!a.seats[Number(slot)]);
597
+ // An area the pack no longer has is dropped even with no slots under it (`{}`: 0 kept of 0, yet applyOverrides refuses it).
598
+ if (!a || ok.length !== Object.keys(slots).length) dropped = true;
599
+ if (ok.length) kept[area] = Object.fromEntries(ok);
600
+ }
601
+ if (!dropped) return ov;
602
+ const { seatFor: _gone, ...rest } = ov;
603
+ return Object.keys(kept).length ? { ...rest, seatFor: kept } : rest;
604
+ }
605
+
426
606
  /**
427
607
  * What an edited layout keeps as overrides: only the parts that differ from the pack's own (`edited` is a pack with
428
608
  * the same areas and furniture, positions moved); null when nothing does (the office goes back to the pack's).
@@ -431,12 +611,26 @@ export function overridesBetween(pack, edited) {
431
611
  const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
432
612
  const ov = { pack: pack.id };
433
613
  for (const [name, a] of Object.entries(pack.areas)) {
434
- const e = own(edited.areas, name);
435
- if (e && !same(a.seats, e.seats)) (ov.seats ||= {})[name] = e.seats.map((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
+ });
436
621
  }
437
622
  const at = new Map(pack.furniture.map((f) => [f.id, f]));
438
623
  for (const f of edited.furniture) { const o = at.get(f.id); if (o && (o.x !== f.x || o.y !== f.y)) (ov.furniture ||= {})[f.id] = [f.x, f.y]; }
624
+ // #2665: a night place that differs from the pack's (its own x/y, or in place: null); in place again is no entry.
625
+ for (const f of edited.furniture) {
626
+ const o = at.get(f.id);
627
+ if (!o || !o.night || !f.night || f.night.x === null || f.night.y === null) continue;
628
+ if (o.night.x !== f.night.x || o.night.y !== f.night.y) (ov.furnitureNight ||= {})[f.id] = [f.night.x, f.night.y];
629
+ }
439
630
  if (pack.door.x !== edited.door.x || pack.door.y !== edited.door.y) ov.door = [edited.door.x, edited.door.y];
631
+ // #2736: a mail drop placed (the pack has none) or moved from the pack's.
632
+ const md = pack.mailDrop ?? null, ed = edited.mailDrop ?? null;
633
+ if (ed && (!md || md.x !== ed.x || md.y !== ed.y)) ov.mailDrop = [ed.x, ed.y];
440
634
  if (!same(pack.waypoints, edited.waypoints) || !same(pack.edges, edited.edges)) {
441
635
  ov.waypoints = edited.waypoints.map((w) => ({ id: w.id, x: w.x, y: w.y }));
442
636
  ov.edges = edited.edges.map((e) => [e[0], e[1]]);
@@ -13,8 +13,9 @@
13
13
  // @typedef {"working"|"idle"|"paused"|"needs"|"walk"} OfficePose (fleet-office-model.js POSES; "away" draws no one)
14
14
  // @typedef {{ pal: OfficePalette, mode: "dark"|"light", labels: { lobby, closed, areas: { [area]: string } } }} OfficeEnv
15
15
  // @typedef {{ name: string, label: string, state: string, pose: OfficePose|"away", light: "warn"|"danger"|null,
16
- // lamp: boolean, sign: "closed"|null, coat: string, work: number, dim: boolean, match: boolean, free: boolean }} 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",