@7365admin1/layer-common 3.2.1-staging.67 → 3.2.1-staging.68
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,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/layer-common": patch
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Rewrite the camera wall's detail panel for the people who read it, and stop
|
|
6
|
+
repeating the same sentence four times.
|
|
7
|
+
|
|
8
|
+
The panel under the tiles was written for a developer. On the live wall it
|
|
9
|
+
read:
|
|
10
|
+
|
|
11
|
+
> BBQ AREA
|
|
12
|
+
> The video page loaded, but this browser cannot confirm a picture is arriving.
|
|
13
|
+
> The player belongs to the video service and a browser cannot look inside it.
|
|
14
|
+
> Judge the tile by what you can see, and report a blank or frozen picture.
|
|
15
|
+
> ● Recorder responding Last picture this server received: none yet
|
|
16
|
+
> Firmware and device clock are not available: this recorder is reachable over
|
|
17
|
+
> video only.
|
|
18
|
+
|
|
19
|
+
Every one of those statements is true, and the screen was still wrong: it
|
|
20
|
+
explained cross-origin isolation to a guard, repeated the camera's name the
|
|
21
|
+
tile already showed, put a green light beside the words "none yet", and printed
|
|
22
|
+
the same reason on four capability cards in a row.
|
|
23
|
+
|
|
24
|
+
Changes, all copy and layout — **no behaviour, no capability gating and no
|
|
25
|
+
permission check is touched**:
|
|
26
|
+
|
|
27
|
+
- **The badge reads "Not confirmed"**, not "Not verified". "Not verified"
|
|
28
|
+
describes a check we failed to make; "Not confirmed" describes the picture,
|
|
29
|
+
which is the thing the reader cares about. Same claim, and the wall still
|
|
30
|
+
refuses to say "Live" until the video service reports a decoded frame.
|
|
31
|
+
- **The sentence behind it is now an instruction**: "This page cannot confirm
|
|
32
|
+
the picture is arriving, so check the tile yourself. If it is blank or frozen,
|
|
33
|
+
report the camera." Why the software cannot confirm it is our problem, and it
|
|
34
|
+
has been removed from the product.
|
|
35
|
+
- **The camera's name is no longer a heading of its own.** It is the subject of
|
|
36
|
+
the status line — "BBQ AREA · Recorder responding" — so a reader on a 3x3 wall
|
|
37
|
+
still knows which tile the panel describes, without a second caption under the
|
|
38
|
+
first one.
|
|
39
|
+
- **"Last picture" is drawn only when there has been one.** A green "Recorder
|
|
40
|
+
responding" beside "none yet" read as a contradiction. They were never in
|
|
41
|
+
conflict, but a screen that has to be explained is wrong. The tile's own
|
|
42
|
+
health line has always behaved this way; the panel now matches it.
|
|
43
|
+
- **The firmware-and-device-clock sentence is gone**, from the panel and from
|
|
44
|
+
the tile's tooltip. This screen never showed either value, so explaining their
|
|
45
|
+
absence gave the reader nothing to do. The server still sends
|
|
46
|
+
`detailUnavailableReason`; nothing draws it.
|
|
47
|
+
- **One reason for the group instead of four copies.** When every switched-off
|
|
48
|
+
control is off for the same reason, that reason and its next step are stated
|
|
49
|
+
once above the row and each card keeps only its title and its state. When the
|
|
50
|
+
reasons differ — "the recorder is unreachable" and "this camera cannot move"
|
|
51
|
+
are two problems with two different people to ask — the group note is refused
|
|
52
|
+
and each card carries its own again. New rule 7, in `camera-wall.ts`, with
|
|
53
|
+
four tests.
|
|
54
|
+
- Shorter next-step and no-signal wording, and a subject on the recorder's
|
|
55
|
+
status labels ("Recorder not checked yet" rather than "Health not checked
|
|
56
|
+
yet").
|
|
57
|
+
|
|
58
|
+
Rules 3 and 6 are intact: the server's own sentence is still shown verbatim and
|
|
59
|
+
never rewritten, and the recorder's state is still a separate fact from the
|
|
60
|
+
tile's badge.
|
|
@@ -170,44 +170,73 @@
|
|
|
170
170
|
ask for it, and hiding it is what stops anyone asking.
|
|
171
171
|
-->
|
|
172
172
|
<div v-if="focused" class="vms__caps">
|
|
173
|
-
<p class="vms__caps-head">{{ focused.name || focused._id }}</p>
|
|
174
|
-
|
|
175
173
|
<!--
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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.
|
|
183
188
|
-->
|
|
184
|
-
<!--
|
|
185
|
-
WHAT THE BADGE ON THE TILE ACTUALLY MEANS, said once here rather than
|
|
186
|
-
nine times across the wall. "Not verified" is a word a supervisor has
|
|
187
|
-
every right to challenge, and the answer — the picture belongs to
|
|
188
|
-
another origin's page and a browser cannot look inside it — is too long
|
|
189
|
-
for a tile and too important to leave out.
|
|
190
|
-
-->
|
|
191
|
-
<p v-if="embedsPlayer" class="vms__badge-note">{{ UNVERIFIED_DETAIL }}</p>
|
|
192
|
-
|
|
193
189
|
<div class="vms__health">
|
|
194
190
|
<span class="vms__health-state">
|
|
195
191
|
<span class="vms__health-dot" :class="`vms__health-dot--${focusedHealth.tone}`" />
|
|
196
|
-
{{
|
|
192
|
+
{{ focused.name || focused._id }}
|
|
197
193
|
</span>
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
194
|
+
<!--
|
|
195
|
+
THE RECORDER's own state — a different fact from the tile badge above,
|
|
196
|
+
which is about the picture. They can disagree, and when they do the
|
|
197
|
+
reader needs both, not a merged verdict.
|
|
198
|
+
-->
|
|
199
|
+
<span class="vms__health-item">{{ focusedHealth.label }}</span>
|
|
200
|
+
<!--
|
|
201
|
+
"Last picture" ONLY WHEN THERE HAS BEEN ONE.
|
|
202
|
+
|
|
203
|
+
It used to read "Last picture this server received: none yet" beside a
|
|
204
|
+
green "Recorder responding", and the two together read as a
|
|
205
|
+
contradiction: a green light next to "none yet" tells a guard nothing
|
|
206
|
+
they can act on. They were never in conflict — one is the recorder
|
|
207
|
+
answering, the other is a still frame this server has never been set
|
|
208
|
+
up to fetch — but a screen that has to be explained is a screen that
|
|
209
|
+
is wrong. When there IS a picture, its age is worth knowing and it is
|
|
210
|
+
shown. When there is not, the line is silent, exactly as the tile's
|
|
211
|
+
own health line has always behaved.
|
|
212
|
+
-->
|
|
213
|
+
<span v-if="focusedHealth.lastFrame" class="vms__health-item">
|
|
214
|
+
Last picture <b>{{ focusedHealth.lastFrame }}</b>
|
|
201
215
|
</span>
|
|
202
216
|
<span v-if="focusedHealth.detail" class="vms__health-item">
|
|
203
217
|
{{ focusedHealth.detail }}
|
|
204
218
|
</span>
|
|
205
|
-
<span v-if="focusedHealth.deviceDetail" class="vms__health-item">
|
|
206
|
-
{{ focusedHealth.deviceDetail }}
|
|
207
|
-
</span>
|
|
208
219
|
<span v-if="healthFailed" class="vms__health-item">{{ healthFailed }}</span>
|
|
209
220
|
</div>
|
|
210
221
|
|
|
222
|
+
<!--
|
|
223
|
+
WHAT THE TILE's BADGE MEANS, said once here rather than nine times
|
|
224
|
+
across the wall — and said in the reader's terms, not ours. See
|
|
225
|
+
`UNVERIFIED_DETAIL`.
|
|
226
|
+
-->
|
|
227
|
+
<p v-if="embedsPlayer" class="vms__badge-note">{{ UNVERIFIED_DETAIL }}</p>
|
|
228
|
+
|
|
229
|
+
<p class="vms__caps-head">Camera controls</p>
|
|
230
|
+
<!--
|
|
231
|
+
RULE 7. One reason, stated once, when it is genuinely one reason — see
|
|
232
|
+
`sharedCapabilityNote`. When the reasons differ the note is `null` and
|
|
233
|
+
every card carries its own, as before.
|
|
234
|
+
-->
|
|
235
|
+
<div v-if="sharedNote" class="vms__caps-note">
|
|
236
|
+
<p class="vms__cap-detail">{{ sharedNote.detail }}</p>
|
|
237
|
+
<p v-if="sharedNote.nextStep" class="vms__cap-next">{{ sharedNote.nextStep }}</p>
|
|
238
|
+
</div>
|
|
239
|
+
|
|
211
240
|
<div class="vms__caps-row">
|
|
212
241
|
<div
|
|
213
242
|
v-for="c in WALL_CAPABILITY_CONTROLS"
|
|
@@ -224,17 +253,21 @@
|
|
|
224
253
|
{{ capability(c.key).tag }}
|
|
225
254
|
</span>
|
|
226
255
|
</p>
|
|
227
|
-
<!--
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
256
|
+
<!--
|
|
257
|
+
Working: what it does HERE. Off: the server's sentence, verbatim
|
|
258
|
+
(rule 3) — unless the whole group shares that sentence, in which
|
|
259
|
+
case it has already been said once above and the card carries only
|
|
260
|
+
its title and its tag.
|
|
261
|
+
-->
|
|
262
|
+
<p v-if="capabilityText(c)" class="vms__cap-detail">{{ capabilityText(c) }}</p>
|
|
231
263
|
<!--
|
|
232
264
|
RULE 5. The line the server cannot write, because it does not know
|
|
233
265
|
who the reader can go and ask. Somebody read "Not set up yet" on this
|
|
234
266
|
screen and asked how to set it up — which is a fair question that the
|
|
235
|
-
screen was not answering.
|
|
267
|
+
screen was not answering. Suppressed here for the same reason as
|
|
268
|
+
the sentence above it: it is in the group note.
|
|
236
269
|
-->
|
|
237
|
-
<p v-if="capability(c.key).nextStep" class="vms__cap-next">
|
|
270
|
+
<p v-if="!sharedNote && capability(c.key).nextStep" class="vms__cap-next">
|
|
238
271
|
{{ capability(c.key).nextStep }}
|
|
239
272
|
</p>
|
|
240
273
|
</div>
|
|
@@ -252,6 +285,7 @@ import {
|
|
|
252
285
|
DEFAULT_WALL_LAYOUT,
|
|
253
286
|
healthView,
|
|
254
287
|
isDenseLayout,
|
|
288
|
+
sharedCapabilityNote,
|
|
255
289
|
wallCameras,
|
|
256
290
|
wallLayout,
|
|
257
291
|
wallPageCount,
|
|
@@ -417,7 +451,29 @@ function capability(key: string) {
|
|
|
417
451
|
}
|
|
418
452
|
|
|
419
453
|
/**
|
|
420
|
-
*
|
|
454
|
+
* Rule 7: the one reason the whole group is off, or `null` when they differ.
|
|
455
|
+
* When it is set, the cards stop repeating it — see the template.
|
|
456
|
+
*/
|
|
457
|
+
const sharedNote = computed(() =>
|
|
458
|
+
sharedCapabilityNote(
|
|
459
|
+
focused.value,
|
|
460
|
+
WALL_CAPABILITY_CONTROLS.map((c) => c.key)
|
|
461
|
+
)
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* What one card says under its title: what the control does when it works, the
|
|
466
|
+
* server's reason when it does not, and NOTHING when that reason has already
|
|
467
|
+
* been given once for the group.
|
|
468
|
+
*/
|
|
469
|
+
function capabilityText(control: (typeof WALL_CAPABILITY_CONTROLS)[number]) {
|
|
470
|
+
const view = capability(control.key);
|
|
471
|
+
if (view.available) return control.ready;
|
|
472
|
+
return sharedNote.value ? "" : view.detail;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Whether this camera embeds a player at all — i.e. whether the "Not confirmed"
|
|
421
477
|
* badge is something its tile can ever show. A camera the server has already
|
|
422
478
|
* refused has a reason of its own on the tile and does not need this one too.
|
|
423
479
|
*/
|
|
@@ -470,7 +526,7 @@ async function sweepHealth() {
|
|
|
470
526
|
} catch {
|
|
471
527
|
// Kept, not cleared: the last known health is still the most useful thing
|
|
472
528
|
// we have, and "the recorder was fine a moment ago" is a real answer.
|
|
473
|
-
healthFailed.value = "
|
|
529
|
+
healthFailed.value = "Could not check the recorder just now.";
|
|
474
530
|
}
|
|
475
531
|
}
|
|
476
532
|
|
|
@@ -831,14 +887,31 @@ async function toggleFullscreen() {
|
|
|
831
887
|
border-top: 1px solid var(--vms-line);
|
|
832
888
|
}
|
|
833
889
|
|
|
890
|
+
/*
|
|
891
|
+
Now a GROUP LABEL rather than the camera's name repeated: it names what the
|
|
892
|
+
row below it is, which is the one job a heading here has. The camera is
|
|
893
|
+
identified once, in the status line above.
|
|
894
|
+
*/
|
|
834
895
|
.vms__caps-head {
|
|
835
|
-
margin: 0 0
|
|
896
|
+
margin: 0 0 6px;
|
|
836
897
|
font-size: 11px;
|
|
837
898
|
letter-spacing: 0.06em;
|
|
838
899
|
text-transform: uppercase;
|
|
839
900
|
color: var(--vms-text-dim);
|
|
840
901
|
}
|
|
841
902
|
|
|
903
|
+
/*
|
|
904
|
+
RULE 7's one-sentence-for-the-group. Sits between the group label and the
|
|
905
|
+
cards, indented on the same 2 px accent rail the cards wear, so it reads as
|
|
906
|
+
belonging to all of them rather than to the first one.
|
|
907
|
+
*/
|
|
908
|
+
.vms__caps-note {
|
|
909
|
+
margin: 0 0 8px;
|
|
910
|
+
padding-left: 10px;
|
|
911
|
+
border-left: 2px solid var(--vms-accent);
|
|
912
|
+
max-width: 92ch;
|
|
913
|
+
}
|
|
914
|
+
|
|
842
915
|
.vms__caps-row {
|
|
843
916
|
display: grid;
|
|
844
917
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
@@ -178,10 +178,10 @@ import {
|
|
|
178
178
|
* origin's document is blocked by the browser — correctly.
|
|
179
179
|
*
|
|
180
180
|
* That ceiling has not moved. What has changed is that the badge no longer
|
|
181
|
-
* pretends otherwise. A loaded page earns **"Not
|
|
181
|
+
* pretends otherwise. A loaded page earns **"Not confirmed"**, not "Live"; only
|
|
182
182
|
* a frame the video service itself reports over `postMessage` earns "Live".
|
|
183
183
|
* The service sends nothing today, so every tile on this estate sits at "Not
|
|
184
|
-
*
|
|
184
|
+
* confirmed" — which is the true answer. It went in because on the live wall two
|
|
185
185
|
* cameras rendered a blank rectangle under a green "Live" badge, which is the
|
|
186
186
|
* one thing a monitoring screen must never do.
|
|
187
187
|
*
|
|
@@ -239,10 +239,13 @@ const healthLine = computed(() => {
|
|
|
239
239
|
return lastFrame && !props.dense ? `${label} · ${lastFrame}` : label;
|
|
240
240
|
});
|
|
241
241
|
|
|
242
|
-
/**
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
242
|
+
/**
|
|
243
|
+
* Everything the line had no room for, on hover. The firmware-and-clock
|
|
244
|
+
* sentence used to be appended here too; it is gone for the same reason it is
|
|
245
|
+
* gone from the panel — this screen never showed either value, so explaining
|
|
246
|
+
* their absence gave the reader nothing to do. See `THealthView`.
|
|
247
|
+
*/
|
|
248
|
+
const healthTitle = computed(() => recorder.value.detail || "");
|
|
246
249
|
|
|
247
250
|
/* -------------------------------------------------------------------- zoom */
|
|
248
251
|
|
|
@@ -432,7 +435,7 @@ function restartFrameClock() {
|
|
|
432
435
|
* direction.
|
|
433
436
|
*
|
|
434
437
|
* The timeout only fires while nothing at all has been heard: a page that
|
|
435
|
-
* loaded is already at `page-up` and stays there. Sitting on "Not
|
|
438
|
+
* loaded is already at `page-up` and stays there. Sitting on "Not confirmed"
|
|
436
439
|
* indefinitely is deliberate — declaring a camera dead on a timer, with no
|
|
437
440
|
* evidence, is the same mistake as declaring it live on a timer.
|
|
438
441
|
*/
|
package/package.json
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
isDenseLayout,
|
|
12
12
|
playerOrigin,
|
|
13
13
|
playerSignal,
|
|
14
|
+
sharedCapabilityNote,
|
|
14
15
|
tileView,
|
|
15
16
|
wallCameras,
|
|
16
17
|
wallLayout,
|
|
@@ -134,6 +135,66 @@ test("a switched-off control says what would switch it on and who to ask", () =>
|
|
|
134
135
|
assert.match(v.nextStep, /not something to switch on from this page/i);
|
|
135
136
|
});
|
|
136
137
|
|
|
138
|
+
/* ---------------------------------------------------------------- rule 7 */
|
|
139
|
+
|
|
140
|
+
const OFF_FOR_THE_SAME_REASON = {
|
|
141
|
+
state: "unsupported",
|
|
142
|
+
reason: "device-http-not-configured",
|
|
143
|
+
detail: "No direct connection to this camera's recorder is configured on this server.",
|
|
144
|
+
};
|
|
145
|
+
const CAP_KEYS = WALL_CAPABILITY_CONTROLS.map((c) => c.key);
|
|
146
|
+
|
|
147
|
+
test("four controls off for one reason state that reason once", () => {
|
|
148
|
+
// What the owner actually saw: the same sentence and the same next step,
|
|
149
|
+
// four times in a row, under four different titles.
|
|
150
|
+
const cam = ip({
|
|
151
|
+
capabilities: {
|
|
152
|
+
// Exactly the live wall: zoom works, the other four are off together.
|
|
153
|
+
digitalZoom: { state: "supported" },
|
|
154
|
+
playback: OFF_FOR_THE_SAME_REASON,
|
|
155
|
+
events: OFF_FOR_THE_SAME_REASON,
|
|
156
|
+
ptz: OFF_FOR_THE_SAME_REASON,
|
|
157
|
+
presets: OFF_FOR_THE_SAME_REASON,
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
const note = sharedCapabilityNote(cam, CAP_KEYS);
|
|
161
|
+
assert.match(note.detail, /^No direct connection/);
|
|
162
|
+
assert.match(note.nextStep, /Seven365 administrator/);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("controls off for DIFFERENT reasons are never merged into one", () => {
|
|
166
|
+
// "The recorder is unreachable" and "this camera cannot move" are two
|
|
167
|
+
// problems with two different people to ask. Flattening them would be a lie
|
|
168
|
+
// of convenience, so the group note is refused and each card keeps its own.
|
|
169
|
+
const cam = ip({
|
|
170
|
+
capabilities: {
|
|
171
|
+
playback: OFF_FOR_THE_SAME_REASON,
|
|
172
|
+
ptz: { state: "unsupported", reason: "device-no-ptz", detail: "This camera cannot move." },
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
assert.equal(sharedCapabilityNote(cam, CAP_KEYS), null);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("one control off on its own does not get a group note", () => {
|
|
179
|
+
const cam = ip({
|
|
180
|
+
capabilities: {
|
|
181
|
+
playback: OFF_FOR_THE_SAME_REASON,
|
|
182
|
+
events: { state: "supported" },
|
|
183
|
+
ptz: { state: "supported" },
|
|
184
|
+
presets: { state: "supported" },
|
|
185
|
+
digitalZoom: { state: "supported" },
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
assert.equal(sharedCapabilityNote(cam, CAP_KEYS), null);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("a camera with no descriptor at all still groups its unknowns", () => {
|
|
192
|
+
// Every control reads "Not checked yet" with the same not-probed sentence —
|
|
193
|
+
// which is four identical cards, the same noise, and the same fix.
|
|
194
|
+
const note = sharedCapabilityNote(ip(), CAP_KEYS);
|
|
195
|
+
assert.match(note.detail, /has not been asked/);
|
|
196
|
+
});
|
|
197
|
+
|
|
137
198
|
test("a reason code this build has never seen still names somebody to ask", () => {
|
|
138
199
|
const cam = ip({
|
|
139
200
|
capabilities: { ptz: { state: "unsupported", reason: "invented-later", detail: "Nope." } },
|
|
@@ -175,15 +236,19 @@ test("a page that has not loaded is Connecting, never Live", () => {
|
|
|
175
236
|
* green "Live". The page had loaded; no picture was arriving. A loaded page is
|
|
176
237
|
* now worth exactly one thing, and it is not the word "Live".
|
|
177
238
|
*/
|
|
178
|
-
test("a loaded player page is NOT Live - it is Not
|
|
239
|
+
test("a loaded player page is NOT Live - it is Not confirmed", () => {
|
|
179
240
|
const v = tileView(ip(), { player: "page-up" });
|
|
180
241
|
assert.equal(v.state, "unverified");
|
|
181
|
-
assert.equal(v.label, "Not
|
|
242
|
+
assert.equal(v.label, "Not confirmed");
|
|
182
243
|
// No scrim: there may be a perfectly good picture under that badge, and
|
|
183
244
|
// covering a working camera would be a worse lie than the one being fixed.
|
|
184
245
|
assert.equal(v.detail, null);
|
|
185
246
|
assert.equal(v.showPlayer, true);
|
|
186
|
-
assert.match(v.note, /cannot confirm
|
|
247
|
+
assert.match(v.note, /cannot confirm the picture is arriving/);
|
|
248
|
+
// Plain English, and an instruction rather than an explanation: the reader is
|
|
249
|
+
// a guard, not a browser engineer. Nothing about origins, players or frames.
|
|
250
|
+
assert.match(v.note, /check the tile/i);
|
|
251
|
+
assert.doesNotMatch(v.note, /browser|origin|player|iframe|service/i);
|
|
187
252
|
});
|
|
188
253
|
|
|
189
254
|
test("only a reported frame earns the word Live", () => {
|
|
@@ -349,16 +414,44 @@ test("reachability alone is enough when the server sends no summary word", () =>
|
|
|
349
414
|
assert.equal(healthView({ reachable: false }).tone, "down");
|
|
350
415
|
});
|
|
351
416
|
|
|
352
|
-
test("the server's health
|
|
417
|
+
test("the server's health sentence is carried through, not rewritten", () => {
|
|
353
418
|
const v = healthView({
|
|
354
419
|
health: "unreachable",
|
|
355
420
|
reachable: false,
|
|
356
421
|
reason: "The recorder did not respond.",
|
|
422
|
+
});
|
|
423
|
+
assert.equal(v.detail, "The recorder did not respond.");
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* The firmware-and-clock sentence is the server explaining why two values it
|
|
428
|
+
* never showed are blank. It was drawn on the panel and appended to the tile's
|
|
429
|
+
* tooltip; a guard can do nothing with a firmware version, so it is not carried
|
|
430
|
+
* at all now. Pinned, because "soften it" was the tempting answer and the wrong
|
|
431
|
+
* one.
|
|
432
|
+
*/
|
|
433
|
+
test("the firmware and device-clock sentence is not carried to the screen", () => {
|
|
434
|
+
const v = healthView({
|
|
435
|
+
health: "ok",
|
|
436
|
+
reachable: true,
|
|
357
437
|
detailUnavailableReason:
|
|
358
438
|
"Firmware and device clock are not available: this recorder is reachable over video only.",
|
|
359
439
|
});
|
|
360
|
-
assert.equal(v.detail,
|
|
361
|
-
assert.
|
|
440
|
+
assert.equal(v.detail, null);
|
|
441
|
+
assert.equal("deviceDetail" in v, false);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* The complaint that started this: a green "Recorder responding" beside "Last
|
|
446
|
+
* picture this server received: none yet" reads as a contradiction. It is not
|
|
447
|
+
* one — but the panel now simply says nothing when there has never been a
|
|
448
|
+
* picture, which is the only reading a guard can act on.
|
|
449
|
+
*/
|
|
450
|
+
test("a recorder that is up with no picture yet reports no picture line at all", () => {
|
|
451
|
+
const v = healthView({ health: "ok", reachable: true, lastFrameAt: null });
|
|
452
|
+
assert.equal(v.tone, "ok");
|
|
453
|
+
assert.equal(v.label, "Recorder responding");
|
|
454
|
+
assert.equal(v.lastFrame, null);
|
|
362
455
|
});
|
|
363
456
|
|
|
364
457
|
test("a picture's age is coarse above a minute, and never a fake time", () => {
|
package/utils/camera-wall.ts
CHANGED
|
@@ -256,15 +256,15 @@ const NOT_PROBED = "This camera has not been asked what it can do yet.";
|
|
|
256
256
|
*/
|
|
257
257
|
export const CAPABILITY_NEXT_STEPS: Record<string, string> = {
|
|
258
258
|
"device-http-not-configured":
|
|
259
|
-
"
|
|
259
|
+
"Not something to switch on from this page — it needs a change on the server. Ask your Seven365 administrator.",
|
|
260
260
|
"device-http-disabled":
|
|
261
|
-
"
|
|
261
|
+
"Switched off on the server. Ask your Seven365 administrator to switch it on.",
|
|
262
262
|
"device-http-unreachable":
|
|
263
|
-
"The recorder
|
|
263
|
+
"The recorder is not answering. Ask your Seven365 administrator or the camera installer to check it.",
|
|
264
264
|
"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
|
|
265
|
+
"Sign-ins to the recorder are being held back so its account is not locked out. Ask your Seven365 administrator.",
|
|
266
266
|
"control-not-enabled":
|
|
267
|
-
"
|
|
267
|
+
"Switched off on the server. It moves real hardware, so a Seven365 backend lead has to approve it first.",
|
|
268
268
|
"device-no-ptz":
|
|
269
269
|
"This camera cannot move, so there is nothing to switch on. A different camera would be needed.",
|
|
270
270
|
"no-recorder-configured":
|
|
@@ -282,7 +282,7 @@ export const CAPABILITY_NEXT_STEPS: Record<string, string> = {
|
|
|
282
282
|
"device-not-probed":
|
|
283
283
|
"The camera is asked what it can do the first time it is opened. Nothing to do — check back in a moment.",
|
|
284
284
|
"no-transport":
|
|
285
|
-
"Nothing on this server can do
|
|
285
|
+
"Not built yet. Nothing on this server can do it.",
|
|
286
286
|
};
|
|
287
287
|
|
|
288
288
|
/**
|
|
@@ -386,6 +386,47 @@ export function capabilityView(
|
|
|
386
386
|
};
|
|
387
387
|
}
|
|
388
388
|
|
|
389
|
+
/**
|
|
390
|
+
* RULE 7 — SAY IT ONCE FOR THE GROUP WHEN IT IS ONE FACT, NOT FOUR.
|
|
391
|
+
*
|
|
392
|
+
* On this estate all four of the switched-off controls are off for the SAME
|
|
393
|
+
* reason: there is no direct connection to the recorder. So the panel drew four
|
|
394
|
+
* cards, each carrying an identical sentence and an identical next step —
|
|
395
|
+
* eight lines of repetition saying one thing. Four copies of a sentence do not
|
|
396
|
+
* make it four facts; they make the reader skim, and skimming is how the one
|
|
397
|
+
* line that mattered got missed.
|
|
398
|
+
*
|
|
399
|
+
* So when every unavailable control shares the same reason, this returns it and
|
|
400
|
+
* the panel prints it once above the row. Each card keeps its own title and its
|
|
401
|
+
* own tag, so the individual state is never lost — only the echo is.
|
|
402
|
+
*
|
|
403
|
+
* Returns `null` the moment the reasons DIFFER, which is the case that must not
|
|
404
|
+
* be flattened: "the recorder is unreachable" and "this camera cannot move" are
|
|
405
|
+
* two different problems with two different people to ask, and merging them
|
|
406
|
+
* would be a lie of convenience. Also `null` when nothing is off (nothing to
|
|
407
|
+
* explain) or when only one control is off (a group note for a group of one is
|
|
408
|
+
* just the card, moved).
|
|
409
|
+
*/
|
|
410
|
+
export function sharedCapabilityNote(
|
|
411
|
+
camera: TWallCamera | null | undefined,
|
|
412
|
+
keys: readonly string[]
|
|
413
|
+
): { detail: string; nextStep: string | null } | null {
|
|
414
|
+
const off = keys
|
|
415
|
+
.map((key) => capabilityView(camera, key))
|
|
416
|
+
.filter((view) => !view.available);
|
|
417
|
+
|
|
418
|
+
if (off.length < 2) return null;
|
|
419
|
+
|
|
420
|
+
const first = off[0];
|
|
421
|
+
const same = off.every(
|
|
422
|
+
(view) => view.detail === first.detail && view.nextStep === first.nextStep
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
return same && first.detail
|
|
426
|
+
? { detail: first.detail, nextStep: first.nextStep }
|
|
427
|
+
: null;
|
|
428
|
+
}
|
|
429
|
+
|
|
389
430
|
/* -------------------------------------------------------------------------- */
|
|
390
431
|
/* Tile state */
|
|
391
432
|
/* -------------------------------------------------------------------------- */
|
|
@@ -428,10 +469,16 @@ export type TTileState =
|
|
|
428
469
|
*
|
|
429
470
|
* `unverified` is the state this wall is in for every camera on this estate
|
|
430
471
|
* today, and saying so is the point — see the note above `tileView`.
|
|
472
|
+
*
|
|
473
|
+
* It used to read "Not verified", which is a word from our side of the fence:
|
|
474
|
+
* it describes a check we did not manage to make. "Not confirmed" describes the
|
|
475
|
+
* PICTURE, which is the only thing the reader cares about, and it is the same
|
|
476
|
+
* claim. The instruction that goes with it — check the tile — is in
|
|
477
|
+
* `UNVERIFIED_DETAIL`, on the badge's tooltip and once on the panel.
|
|
431
478
|
*/
|
|
432
479
|
export const TILE_LABELS = {
|
|
433
480
|
live: "Live",
|
|
434
|
-
unverified: "Not
|
|
481
|
+
unverified: "Not confirmed",
|
|
435
482
|
connecting: "Connecting…",
|
|
436
483
|
"no-signal": "No signal",
|
|
437
484
|
offline: "No connection",
|
|
@@ -548,12 +595,23 @@ export type TTileView = {
|
|
|
548
595
|
};
|
|
549
596
|
|
|
550
597
|
/**
|
|
551
|
-
* The sentence behind "Not
|
|
552
|
-
* rather than nine times across the wall.
|
|
553
|
-
*
|
|
598
|
+
* The sentence behind "Not confirmed", said once on the focused camera's panel
|
|
599
|
+
* rather than nine times across the wall.
|
|
600
|
+
*
|
|
601
|
+
* **Written for a guard or a property manager, not for us.** The first version
|
|
602
|
+
* of this line explained cross-origin isolation — "the player belongs to the
|
|
603
|
+
* video service and a browser cannot look inside it" — which is true, is the
|
|
604
|
+
* reason, and is none of the reader's business. The owner read it on the live
|
|
605
|
+
* wall and said so. A reader of this screen has exactly three questions: is
|
|
606
|
+
* this camera working, what do I do if it is not, and what can I do here. WHY
|
|
607
|
+
* the software cannot answer the first one is our problem, not theirs.
|
|
608
|
+
*
|
|
609
|
+
* So: what we cannot do, then what they should do. Nothing else. The claim is
|
|
610
|
+
* every bit as honest as the paragraph it replaces — it just stops making the
|
|
611
|
+
* reader sit through an engineering lesson to get to the instruction.
|
|
554
612
|
*/
|
|
555
613
|
export const UNVERIFIED_DETAIL =
|
|
556
|
-
"
|
|
614
|
+
"This page cannot confirm the picture is arriving, so check the tile yourself. If it is blank or frozen, report the camera.";
|
|
557
615
|
|
|
558
616
|
/**
|
|
559
617
|
* Rule 4, plus every reason a tile may have nothing to show.
|
|
@@ -632,14 +690,14 @@ export function tileView(
|
|
|
632
690
|
// we genuinely cannot tell a dead camera from a dead video service.
|
|
633
691
|
return showing(
|
|
634
692
|
"no-signal",
|
|
635
|
-
"
|
|
693
|
+
"No picture. The camera or the video service may be down. Report it if it stays like this."
|
|
636
694
|
);
|
|
637
695
|
case "stalled":
|
|
638
696
|
// It WAS playing and stopped. That is the one case where we can say a
|
|
639
697
|
// picture has genuinely gone away, because we saw one arrive first.
|
|
640
698
|
return showing(
|
|
641
699
|
"no-signal",
|
|
642
|
-
"The picture stopped arriving. The camera or the video service may have dropped."
|
|
700
|
+
"The picture stopped arriving. The camera or the video service may have dropped. Report it if it does not come back."
|
|
643
701
|
);
|
|
644
702
|
case "playing":
|
|
645
703
|
return showing("live", null);
|
|
@@ -688,15 +746,27 @@ export type THealthView = {
|
|
|
688
746
|
detail: string | null;
|
|
689
747
|
/** "4 min ago", or `null` when this server has never had a picture. */
|
|
690
748
|
lastFrame: string | null;
|
|
691
|
-
/** Why firmware and clock are blank. The server's sentence, verbatim. */
|
|
692
|
-
deviceDetail: string | null;
|
|
693
749
|
};
|
|
694
750
|
|
|
751
|
+
/*
|
|
752
|
+
THE FIRMWARE-AND-CLOCK LINE IS GONE, AND IT IS NOT COMING BACK.
|
|
753
|
+
|
|
754
|
+
The server sends `detailUnavailableReason` — "Firmware and device clock are
|
|
755
|
+
not available: this recorder is reachable over video only." — so that two
|
|
756
|
+
blank fields do not read as "checked, and fine". That is a good reason to
|
|
757
|
+
send it and a bad reason to DRAW it: this screen never showed firmware or a
|
|
758
|
+
device clock in the first place, so the sentence was explaining the absence
|
|
759
|
+
of something the reader had not been offered and could not use. A guard
|
|
760
|
+
cannot act on a firmware version. Softening the wording would have kept the
|
|
761
|
+
noise, so the line is removed instead. The field is still on
|
|
762
|
+
`TWallCameraHealth` because the API sends it; nothing on the wall draws it.
|
|
763
|
+
*/
|
|
764
|
+
|
|
695
765
|
const HEALTH_LABELS: Record<string, { tone: THealthTone; label: string }> = {
|
|
696
766
|
ok: { tone: "ok", label: "Recorder responding" },
|
|
697
767
|
drifted: { tone: "warn", label: "Recorder clock is out" },
|
|
698
768
|
unreachable: { tone: "down", label: "Recorder not responding" },
|
|
699
|
-
unsupported: { tone: "unknown", label: "
|
|
769
|
+
unsupported: { tone: "unknown", label: "Recorder not checked" },
|
|
700
770
|
};
|
|
701
771
|
|
|
702
772
|
/**
|
|
@@ -716,7 +786,7 @@ export function healthView(
|
|
|
716
786
|
? HEALTH_LABELS.ok
|
|
717
787
|
: health?.reachable === false
|
|
718
788
|
? HEALTH_LABELS.unreachable
|
|
719
|
-
: { tone: "unknown", label: "
|
|
789
|
+
: { tone: "unknown", label: "Recorder not checked yet" };
|
|
720
790
|
|
|
721
791
|
const { tone, label } = known ?? fallback;
|
|
722
792
|
|
|
@@ -725,7 +795,6 @@ export function healthView(
|
|
|
725
795
|
label,
|
|
726
796
|
detail: (health?.reason || "").trim() || null,
|
|
727
797
|
lastFrame: agoLabel(health?.lastFrameAt, now),
|
|
728
|
-
deviceDetail: (health?.detailUnavailableReason || "").trim() || null,
|
|
729
798
|
};
|
|
730
799
|
}
|
|
731
800
|
|