@7365admin1/layer-common 3.2.1-staging.68 → 3.2.1-staging.69

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.
@@ -0,0 +1,65 @@
1
+ ---
2
+ "@7365admin1/layer-common": patch
3
+ ---
4
+
5
+ Mark the selected camera on the wall, and stop telling the reader to raise a
6
+ ticket without saying what for.
7
+
8
+ Two things the owner found on the live wall after the last copy pass. Both are
9
+ copy and presentation only — **no behaviour, no capability gating, no permission
10
+ check and no request is changed.**
11
+
12
+ **1. The camera's name was still printed twice.**
13
+
14
+ The tile said `BBQ AREA` and the panel underneath said it again. The last pass
15
+ merged the panel's heading into its status line, which changed the layout and
16
+ left the repetition exactly where it was.
17
+
18
+ The name was there to answer "which of these nine tiles is this panel about?".
19
+ That is a real question and a name is the wrong answer to it — it makes the
20
+ reader scan the wall for a matching caption. So:
21
+
22
+ - **the selected tile is now visibly selected**: a 3 px accent ring with a scrim
23
+ hairline inside it, so the ring holds against a bright picture as well as a
24
+ dark one, plus the accent underline on the name chip that the toolbar already
25
+ uses to mark a selected tool. Both read at 1x1, 2x2 and 3x3;
26
+ - **the camera's name is removed from the panel entirely**. The panel opens on
27
+ the recorder's state — "● Recorder responding";
28
+ - `aria-current` marks the same tile for anyone not looking at the ring.
29
+
30
+ The highlight had never worked in the most common case, which is why the name
31
+ was load-bearing: it was bound to the raw clicked id, and before anyone clicks —
32
+ or after paging — nothing on the wall was marked while the panel was already
33
+ describing the first tile. It is now bound to the tile the panel actually
34
+ resolves to, so the two can never disagree.
35
+
36
+ **The panel can never be about a tile that is off screen**, so it never needs to
37
+ name one: the focused camera is resolved out of the current PAGE, so paging away
38
+ from a selection re-resolves to the first tile of the page in front of you and
39
+ the ring follows. Paging back restores the original selection. Extracted as
40
+ `focusedCamera()` in `camera-wall.ts` with four tests, including the 5-cameras
41
+ -across-2-pages case.
42
+
43
+ **2. "Ask your Seven365 administrator" was a dead end.**
44
+
45
+ It was the whole of the advice under every switched-off control, and it tells
46
+ the administrator nothing and a property manager only to go and wait. The screen
47
+ never said what the request should ask for.
48
+
49
+ The advice now names the two changes that are actually missing, because they are
50
+ two different jobs for two different people: **the site's network has to let our
51
+ server reach the camera recorder**, and **that recorder then has to be added to
52
+ the server's configuration**. Both are stated in plain words, with who arranges
53
+ them, and with the fact that neither is switchable from this page.
54
+
55
+ No environment variable name appears in any of it — those belong in
56
+ `iservice365-core/docs/camera-integration-config.md`, not on a property
57
+ manager's screen — and there is a test that keeps it that way. The other reason
58
+ codes (`device-http-disabled`, `device-http-unreachable`, `device-http-locked-out`,
59
+ `control-not-enabled`, `no-recorder-configured`, and the unknown-code fallback)
60
+ get the same treatment; the Site Settings ones already said what to do and are
61
+ unchanged.
62
+
63
+ The truthful-status rules are untouched: the wall still refuses to say "Live"
64
+ until the video service reports a decoded frame, and the recorder's state is
65
+ still a separate fact from the tile's badge.
@@ -126,7 +126,7 @@
126
126
  :online="online"
127
127
  :health="healthById[camera._id]"
128
128
  :now="now"
129
- :focused="camera._id === focusedId"
129
+ :focused="camera._id === focused?._id"
130
130
  @focus="focusedId = $event._id"
131
131
  />
132
132
  </div>
@@ -171,32 +171,39 @@
171
171
  -->
172
172
  <div v-if="focused" class="vms__caps">
173
173
  <!--
174
- THE HEADER IS GONE, AND THE CAMERA'S NAME IS NOW THE SUBJECT OF A
175
- SENTENCE INSTEAD.
176
-
177
- This panel used to open with the camera's name on its own line, directly
178
- under a tile already captioned with that name. The owner saw it on the
179
- live wall and asked why the name was there twice a fair question,
180
- because as a bare heading it was not doing anything the tile had not
181
- already done.
182
-
183
- It cannot simply be deleted: on a 3x3 wall the panel describes ONE of
184
- nine tiles and the reader has to know which. So the name stays, once,
185
- as the thing the status line is about "BBQ AREA · Recorder
186
- responding" is a statement, where "BBQ AREA" over "Recorder responding"
187
- was a label followed by a fact about something unnamed.
174
+ THE CAMERA'S NAME IS GONE FROM THIS PANEL ENTIRELY. THE SELECTED TILE
175
+ SAYS WHICH CAMERA THIS IS.
176
+
177
+ It was here twice before: once as a heading, then after a first
178
+ attempt at this as the subject of the status line. The owner's point
179
+ was never the line break. It was the REPETITION: the tile directly above
180
+ is already captioned "BBQ AREA", and printing it again underneath adds
181
+ no fact.
182
+
183
+ The reason it was kept is real the panel describes ONE of up to nine
184
+ tiles and the reader has to know which but a name is the wrong way to
185
+ answer it. Naming a tile makes the reader search the wall for a matching
186
+ caption; an accent ring makes the wall answer at a glance. So the ring
187
+ is the identification (`vms-tile--focused`) and the panel is free to be
188
+ about the camera rather than about naming it.
189
+
190
+ **And the panel can never describe a tile that is off screen**, so it
191
+ never needs to fall back to naming one. `focused` is resolved out of
192
+ `tiles` — the CURRENT page — so paging away re-resolves it to the first
193
+ tile of the page you are now looking at. There is no state in which this
194
+ panel is about something you cannot see. That is asserted in
195
+ `camera-wall.test.ts` so it cannot quietly stop being true.
188
196
  -->
189
197
  <div class="vms__health">
190
- <span class="vms__health-state">
191
- <span class="vms__health-dot" :class="`vms__health-dot--${focusedHealth.tone}`" />
192
- {{ focused.name || focused._id }}
193
- </span>
194
198
  <!--
195
199
  THE RECORDER's own state — a different fact from the tile badge above,
196
200
  which is about the picture. They can disagree, and when they do the
197
201
  reader needs both, not a merged verdict.
198
202
  -->
199
- <span class="vms__health-item">{{ focusedHealth.label }}</span>
203
+ <span class="vms__health-state">
204
+ <span class="vms__health-dot" :class="`vms__health-dot--${focusedHealth.tone}`" />
205
+ {{ focusedHealth.label }}
206
+ </span>
200
207
  <!--
201
208
  "Last picture" ONLY WHEN THERE HAS BEEN ONE.
202
209
 
@@ -283,6 +290,7 @@ import {
283
290
  capabilityView,
284
291
  clampPage,
285
292
  DEFAULT_WALL_LAYOUT,
293
+ focusedCamera,
286
294
  healthView,
287
295
  isDenseLayout,
288
296
  sharedCapabilityNote,
@@ -441,10 +449,28 @@ function togglePick(id: string) {
441
449
 
442
450
  /* --------------------------------------------------------- what is focused */
443
451
 
452
+ /**
453
+ * WHICH TILE THE PANEL IS ABOUT — and it is always a tile you can see.
454
+ *
455
+ * `focusedId` is what somebody clicked; `focused` is what the panel actually
456
+ * describes, and the two are deliberately not the same thing. It is resolved out
457
+ * of `tiles`, which is the CURRENT PAGE, so:
458
+ *
459
+ * - before anyone clicks, the panel describes the first tile on the wall;
460
+ * - paging away from your selection re-resolves to the first tile of the page
461
+ * you are now on, rather than describing a camera that is no longer drawn;
462
+ * - paging back returns to your selection, because `focusedId` is not cleared.
463
+ *
464
+ * **The tile highlight is bound to `focused`, not to `focusedId`** — that one
465
+ * character was the whole defect behind "the panel repeats the tile's name".
466
+ * Bound to `focusedId`, the fallback cases above lit NO tile at all: the panel
467
+ * described the first camera on the page and nothing on the wall said so, which
468
+ * is exactly why the name had to be printed in the panel to make sense of it.
469
+ * Bound to `focused`, the wall always shows which tile is being described, and
470
+ * the name in the panel becomes the repetition the owner said it was.
471
+ */
444
472
  const focusedId = ref("");
445
- const focused = computed(
446
- () => tiles.value.find((c) => c._id === focusedId.value) || tiles.value[0] || null
447
- );
473
+ const focused = computed(() => focusedCamera(tiles.value, focusedId.value));
448
474
 
449
475
  function capability(key: string) {
450
476
  return capabilityView(focused.value, key);
@@ -5,6 +5,7 @@
5
5
  :class="{ 'vms-tile--dense': dense, 'vms-tile--focused': focused, 'vms-tile--empty': !camera }"
6
6
  :tabindex="camera ? 0 : -1"
7
7
  :aria-label="camera ? `${camera.name || camera._id}, ${view.label}` : 'Empty position'"
8
+ :aria-current="focused ? 'true' : undefined"
8
9
  @click="camera && $emit('focus', camera)"
9
10
  @dblclick="camera && toggleFullscreen()"
10
11
  @keydown.enter="camera && toggleFullscreen()"
@@ -486,8 +487,40 @@ async function toggleFullscreen() {
486
487
  min-height: 0;
487
488
  }
488
489
 
490
+ /*
491
+ THE SELECTED TILE, AND WHY IT IS NOW LOUDER.
492
+
493
+ The panel under the wall describes ONE tile. It used to say which by printing
494
+ the camera's name — directly under a tile already captioned with that name —
495
+ because the ring here was too quiet to be doing that job: 2 px of #5b9be0
496
+ disappears against a bright picture, and at nine tiles it was a hairline the
497
+ eye skipped. So the panel was answering "which one?" in words, and the words
498
+ read as the name repeated twice.
499
+
500
+ Two marks now, and between them they hold at 1x1, 2x2 and 3x3:
501
+
502
+ - a **3 px accent ring**, with a 1 px scrim hairline INSIDE it. The hairline is
503
+ what makes the ring survive a white picture — accent-on-white is a weak edge,
504
+ accent-on-near-black is not, so the ring carries its own dark backing rather
505
+ than depending on what the camera happens to be pointing at.
506
+ - the **name chip underlined in the accent**, which is the same mark the
507
+ toolbar already uses for a selected tool (`.vms__tool--on`). At 3x3 the ring
508
+ is 3 px on a ~118 px tile; the underline sits right beside the caption a
509
+ supervisor is already reading, so identification does not depend on
510
+ perceiving 3 px from across a room.
511
+
512
+ INVENTED VALUES, recorded: the ring is 3 px where it was 2 px. Everything else
513
+ is existing — `--vms-accent`, `--vms-scrim-solid`, and the toolbar's 2 px
514
+ underline idiom. No new colour.
515
+ */
489
516
  .vms-tile--focused {
490
- box-shadow: inset 0 0 0 2px var(--vms-accent, #5b9be0);
517
+ box-shadow:
518
+ inset 0 0 0 3px var(--vms-accent, #5b9be0),
519
+ inset 0 0 0 4px var(--vms-scrim-solid, rgba(7, 9, 12, 0.82));
520
+ }
521
+
522
+ .vms-tile--focused .vms-tile__name {
523
+ box-shadow: inset 0 -2px 0 var(--vms-accent, #5b9be0);
491
524
  }
492
525
 
493
526
  .vms-tile:focus-visible {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@7365admin1/layer-common",
3
3
  "license": "MIT",
4
4
  "type": "module",
5
- "version": "3.2.1-staging.68",
5
+ "version": "3.2.1-staging.69",
6
6
  "author": "7365admin1",
7
7
  "main": "./nuxt.config.ts",
8
8
  "publishConfig": {
@@ -4,9 +4,11 @@ import { test } from "node:test";
4
4
  import {
5
5
  agoLabel,
6
6
  capabilityView,
7
+ CAPABILITY_NEXT_STEPS,
7
8
  clampPage,
8
9
  clampPan,
9
10
  clampZoom,
11
+ focusedCamera,
10
12
  healthView,
11
13
  isDenseLayout,
12
14
  playerOrigin,
@@ -70,6 +72,46 @@ test("a selection sets the order, and a stale id is dropped rather than drawn",
70
72
  assert.equal(tiles[0]._id, "b");
71
73
  });
72
74
 
75
+ /* ------------------------------------------- which tile the panel is about */
76
+
77
+ /*
78
+ THE PANEL STOPPED PRINTING THE CAMERA'S NAME, AND THESE ARE THE TWO FACTS
79
+ THAT LET IT.
80
+
81
+ The name was there so the reader could tell which of up to nine tiles the
82
+ panel described. It is now the selected tile's accent ring that answers that —
83
+ which is only safe while the panel can never be about a tile that is not
84
+ drawn. Five cameras across two pages is exactly the case the owner asked
85
+ about, so it is exactly the case pinned here.
86
+ */
87
+ const FIVE = ["a", "b", "c", "d", "e"].map((id) => ip({ _id: id, name: id.toUpperCase() }));
88
+
89
+ test("nothing selected yet: the panel is about the first tile ON THIS PAGE", () => {
90
+ assert.equal(focusedCamera(wallTiles(FIVE, [], "2x2", 0), "")?._id, "a");
91
+ // Page 2 of the same wall holds one camera, and the panel is about that one —
92
+ // not still about "a", which is no longer on screen.
93
+ assert.equal(focusedCamera(wallTiles(FIVE, [], "2x2", 1), "")?._id, "e");
94
+ });
95
+
96
+ test("paging away from your selection never leaves the panel describing an off-page camera", () => {
97
+ // Selected "a" on page 1, then paged to page 2. "a" is not drawn there.
98
+ const pageTwo = wallTiles(FIVE, [], "2x2", 1);
99
+ const shown = focusedCamera(pageTwo, "a");
100
+ assert.equal(shown?._id, "e");
101
+ assert.ok(
102
+ pageTwo.some((c) => c._id === shown?._id),
103
+ "the panel must always describe a tile on the current page"
104
+ );
105
+ });
106
+
107
+ test("paging back restores the selection, because the raw id is kept", () => {
108
+ assert.equal(focusedCamera(wallTiles(FIVE, [], "2x2", 0), "c")?._id, "c");
109
+ });
110
+
111
+ test("an empty wall has nothing to describe", () => {
112
+ assert.equal(focusedCamera([], "a"), null);
113
+ });
114
+
73
115
  /* ---------------------------------------------------------------- rule 2/3 */
74
116
 
75
117
  test("no descriptor at all counts as NOT available", () => {
@@ -132,7 +174,38 @@ test("a switched-off control says what would switch it on and who to ask", () =>
132
174
  // The server's sentence is still verbatim — rule 3 is not traded away for rule 5.
133
175
  assert.match(v.detail, /^No direct connection/);
134
176
  assert.match(v.nextStep, /Seven365 administrator/);
135
- assert.match(v.nextStep, /not something to switch on from this page/i);
177
+ assert.match(v.nextStep, /not switchable from this page/i);
178
+ });
179
+
180
+ /*
181
+ THE ADVICE HAS TO NAME THE TWO CHANGES, AND IT HAS TO STAY OUT OF THE
182
+ DEPLOYMENT MANUAL.
183
+
184
+ "Ask your Seven365 administrator" was the whole of the old advice and it was a
185
+ dead end — worse than useless for the owner, who IS the administrator. What
186
+ makes the new line actionable is that it names the two real changes: the
187
+ network has to let the server reach the recorder, and the recorder then has to
188
+ be configured on the server. Both halves are pinned here, because dropping
189
+ either one silently returns the sentence to a shrug.
190
+
191
+ And the other direction is pinned too: no environment variable name may appear
192
+ in copy a property manager reads. The names are correct, they are in
193
+ `iservice365-core/docs/camera-integration-config.md`, and they are unreadable
194
+ to the person this screen is written for.
195
+ */
196
+ test("the setup advice names the network change AND the server change", () => {
197
+ const advice = CAPABILITY_NEXT_STEPS["device-http-not-configured"];
198
+ assert.match(advice, /network/i);
199
+ assert.match(advice, /recorder/i);
200
+ assert.match(advice, /server's configuration/i);
201
+ });
202
+
203
+ test("no environment variable name is ever shown to a reader", () => {
204
+ // Anything SCREAMING_SNAKE would be a deployment detail leaking into product
205
+ // copy. `CAMERA_DEVICE_HTTP` and friends belong in the deployment document.
206
+ for (const [reason, advice] of Object.entries(CAPABILITY_NEXT_STEPS)) {
207
+ assert.doesNotMatch(advice, /[A-Z][A-Z0-9]*_[A-Z0-9_]+/, `${reason} names a variable`);
208
+ }
136
209
  });
137
210
 
138
211
  /* ---------------------------------------------------------------- rule 7 */
@@ -196,6 +196,30 @@ export function wallTiles(
196
196
  return chosen.slice(start, start + tiles);
197
197
  }
198
198
 
199
+ /**
200
+ * WHICH TILE THE DETAIL PANEL IS ABOUT, and the guarantee that makes the panel
201
+ * able to stop naming it.
202
+ *
203
+ * The panel used to print the camera's name so the reader could tell which of
204
+ * up to nine tiles it was describing. The name is gone — the selected tile wears
205
+ * an accent ring instead — and that only works because **this can never return a
206
+ * camera that is not on the page being looked at.** It resolves out of the
207
+ * CURRENT page's tiles, so a selection left behind on page 1 does not leave the
208
+ * panel describing an invisible camera from page 2; it falls back to the first
209
+ * tile of the page you are actually on, which the ring then marks.
210
+ *
211
+ * The caller keeps the raw id, so paging back restores the original selection.
212
+ *
213
+ * Extracted from the component purely so this guarantee is a tested rule rather
214
+ * than a line of template nobody would think to check.
215
+ */
216
+ export function focusedCamera(
217
+ tiles: TWallCamera[],
218
+ focusedId: string | undefined
219
+ ): TWallCamera | null {
220
+ return tiles.find((c) => c._id === focusedId) || tiles[0] || null;
221
+ }
222
+
199
223
  /**
200
224
  * How many grid rows this page actually needs — the other half of packing.
201
225
  *
@@ -253,22 +277,49 @@ const NOT_PROBED = "This camera has not been asked what it can do yet.";
253
277
  * **Every one of these says the same true and unwelcome thing in a different
254
278
  * way: none of it is switched on from this page.** Saying so plainly is the
255
279
  * point. An empty settings screen that quietly does nothing would be worse.
280
+ *
281
+ * ## "Ask your Seven365 administrator" was a dead end, and it has been replaced
282
+ *
283
+ * That was the whole of the advice, and the owner — who IS the administrator —
284
+ * read it on the live wall and pointed out that it tells him nothing. It tells a
285
+ * property manager nothing either: they raise a ticket and wait, because the
286
+ * screen never said what the ticket should ASK FOR.
287
+ *
288
+ * The real answer is two changes, and they are two different jobs for two
289
+ * different people:
290
+ *
291
+ * 1. **a network change** — the site's network has to let our server reach the
292
+ * camera recorder. Today it does not: the recorder answers video only.
293
+ * 2. **a configuration change on our server** — that recorder then has to be
294
+ * added to the server's configuration.
295
+ *
296
+ * So each line below names the change that is missing and who does it, in that
297
+ * order. **No environment variable is named**, deliberately: the names belong in
298
+ * the deployment documentation (`iservice365-core/docs/camera-integration-config.md`),
299
+ * not on a property manager's screen, and printing them would make the sentence
300
+ * unreadable to the person it is written for without helping the person who
301
+ * needs them, who has the document.
302
+ *
303
+ * Nor does any line point at the camera setup guide. That guide covers ADDING a
304
+ * camera in Site Settings; it does not cover recorder connectivity, so sending
305
+ * the reader there would be sending them somewhere that does not answer the
306
+ * question. Naming the two changes IS the actionable content.
256
307
  */
257
308
  export const CAPABILITY_NEXT_STEPS: Record<string, string> = {
258
309
  "device-http-not-configured":
259
- "Not something to switch on from this page it needs a change on the server. Ask your Seven365 administrator.",
310
+ "Not switchable from this page. It needs the site's network opened so our server can reach the camera recorder, and that recorder then added to the server's configuration your Seven365 administrator arranges both with whoever installed the cameras.",
260
311
  "device-http-disabled":
261
- "Switched off on the server. Ask your Seven365 administrator to switch it on.",
312
+ "Not switchable from this page. The connection to the recorder exists but is turned off in the server's configuration your Seven365 administrator turns it on.",
262
313
  "device-http-unreachable":
263
- "The recorder is not answering. Ask your Seven365 administrator or the camera installer to check it.",
314
+ "Our server cannot reach this camera's recorder. Usually the site's network is blocking it or the recorder is down — your Seven365 administrator and whoever installed the cameras check it between them.",
264
315
  "device-http-locked-out":
265
- "Sign-ins to the recorder are being held back so its account is not locked out. Ask your Seven365 administrator.",
316
+ "Sign-ins to the recorder are being held back so its account is not locked out. It clears on its own; if it keeps happening the password held for the recorder is wrong and your Seven365 administrator corrects it in the server's configuration.",
266
317
  "control-not-enabled":
267
- "Switched off on the server. It moves real hardware, so a Seven365 backend lead has to approve it first.",
318
+ "Not switchable from this page. This one moves real hardware, so a Seven365 backend lead has to approve it before it is turned on in the server's configuration.",
268
319
  "device-no-ptz":
269
320
  "This camera cannot move, so there is nothing to switch on. A different camera would be needed.",
270
321
  "no-recorder-configured":
271
- "No recorder is set up for this camera's video service. Ask your Seven365 administrator to add it.",
322
+ "Not switchable from this page. This camera's video service has no recorder linked to it in the server's configuration — your Seven365 administrator adds it.",
272
323
  "no-channel-in-address":
273
324
  "This camera's address does not say which stream it is. Correct it in Site Settings, under CCTV.",
274
325
  "not-a-relay-player-url":
@@ -292,7 +343,7 @@ export const CAPABILITY_NEXT_STEPS: Record<string, string> = {
292
343
  * setting on this page, and there is still somebody to ask.
293
344
  */
294
345
  const NEXT_STEP_FALLBACK =
295
- "This is not something to switch on from this page. Ask your Seven365 administrator.";
346
+ "Not switchable from this page it needs a change to the server's configuration, which your Seven365 administrator makes.";
296
347
 
297
348
  /**
298
349
  * The capabilities the wall draws a control for, and their plain names.