@7365admin1/layer-common 3.1.4-staging.55 → 3.1.4-staging.56
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,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/layer-common": patch
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Draw the camera wall from the gated wall endpoint, and show the server's own
|
|
6
|
+
reason on a tile.
|
|
7
|
+
|
|
8
|
+
`CameraWall` fetched its cameras from `GET /api/site-cameras` - the Settings
|
|
9
|
+
panel's paginated CRUD list, which took its site from an optional query
|
|
10
|
+
parameter and checked nothing about the caller. Any signed-in user from any
|
|
11
|
+
organisation could read every camera in the estate through it, `host` and
|
|
12
|
+
`username` included.
|
|
13
|
+
|
|
14
|
+
It now calls `GET /api/site-cameras/site/:siteId/wall`, which already exists,
|
|
15
|
+
which the React Native monitoring app was built against, and which is
|
|
16
|
+
authorised server-side: the caller must be a member of the site, of its owning
|
|
17
|
+
organisation, or work for an organisation actively engaged to serve it. That
|
|
18
|
+
endpoint also returns `type: "ip"` cameras only and each camera's capability
|
|
19
|
+
descriptor, so a wall cannot be handed an ANPR unit and a tile can explain
|
|
20
|
+
itself.
|
|
21
|
+
|
|
22
|
+
Two consequences worth stating:
|
|
23
|
+
|
|
24
|
+
- **The pager is gone.** The wall endpoint returns the site's cameras in one
|
|
25
|
+
answer. Paging a video wall was an artefact of borrowing the CRUD list.
|
|
26
|
+
- **A tile now prefers the server's `unavailableReason`** over the reason it
|
|
27
|
+
used to work out itself. The server knows things the browser cannot - chiefly
|
|
28
|
+
"No recorder is configured for this camera's relay", which is the true answer
|
|
29
|
+
for every camera in the estate until the API host is configured, and which the
|
|
30
|
+
wall used to replace with its own guess. Offline still beats everything, since
|
|
31
|
+
it explains every tile at once.
|
|
32
|
+
|
|
33
|
+
No permission gate was invented here. Each application still gates its own menu
|
|
34
|
+
entry; this component draws what it is given, and the server decides.
|
|
35
|
+
|
|
36
|
+
Two smaller things in the same area:
|
|
37
|
+
|
|
38
|
+
- **A refused wall now says so.** The catch-all message told every failure to
|
|
39
|
+
"check your connection", which sends somebody who simply may not see that
|
|
40
|
+
site off to debug their wifi. A 401/403/404 now reads "You do not have access
|
|
41
|
+
to this site's cameras." The server answers "not yours" and "does not exist"
|
|
42
|
+
identically, so this wording does not distinguish them either.
|
|
43
|
+
- **`middleware/member.ts` is removed.** It read a cookie into an unused
|
|
44
|
+
variable and did nothing else - a file named like a membership gate that was
|
|
45
|
+
not one. No page in any of the eleven web apps referenced it. The real check
|
|
46
|
+
is `plugins/secure-member.client.ts`, driven by `memberOnly` page meta, which
|
|
47
|
+
is untouched.
|
|
@@ -40,16 +40,6 @@
|
|
|
40
40
|
|
|
41
41
|
<div class="vms__spacer" />
|
|
42
42
|
|
|
43
|
-
<div v-if="pages > 1" class="vms__pager">
|
|
44
|
-
<button type="button" class="vms__tool" :disabled="page === 1" @click="page--">
|
|
45
|
-
Prev
|
|
46
|
-
</button>
|
|
47
|
-
<span class="vms__range">{{ pageRange || `Page ${page} of ${pages}` }}</span>
|
|
48
|
-
<button type="button" class="vms__tool" :disabled="page >= pages" @click="page++">
|
|
49
|
-
Next
|
|
50
|
-
</button>
|
|
51
|
-
</div>
|
|
52
|
-
|
|
53
43
|
<button
|
|
54
44
|
type="button"
|
|
55
45
|
class="vms__tool"
|
|
@@ -225,12 +215,9 @@ const props = defineProps<{ site: string }>();
|
|
|
225
215
|
|
|
226
216
|
/* ---------------------------------------------------------------- the data */
|
|
227
217
|
|
|
228
|
-
const {
|
|
218
|
+
const { getSiteWall } = useSiteSettings();
|
|
229
219
|
|
|
230
220
|
const cameras = ref<TWallCamera[]>([]);
|
|
231
|
-
const page = ref(1);
|
|
232
|
-
const pages = ref(0);
|
|
233
|
-
const pageRange = ref("");
|
|
234
221
|
const loading = ref(true);
|
|
235
222
|
const failed = ref("");
|
|
236
223
|
|
|
@@ -238,32 +225,32 @@ async function load() {
|
|
|
238
225
|
loading.value = true;
|
|
239
226
|
failed.value = "";
|
|
240
227
|
try {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
//
|
|
254
|
-
// and the raw message is often a stack trace. Say what is true and offer
|
|
255
|
-
// the one useful action.
|
|
228
|
+
// The site's wall, from the endpoint built for it. `wallCameras` still
|
|
229
|
+
// filters on the way in — the server already returns `type: "ip"` only, and
|
|
230
|
+
// an ANPR unit must never reach a monitoring wall even if that ever changes.
|
|
231
|
+
const res = await getSiteWall(props.site);
|
|
232
|
+
cameras.value = wallCameras(res?.cameras);
|
|
233
|
+
} catch (error: any) {
|
|
234
|
+
// The raw message is often a stack trace and rarely something an operator
|
|
235
|
+
// can act on — but "you may not see this site" and "your connection
|
|
236
|
+
// dropped" are genuinely different problems with different next steps, and
|
|
237
|
+
// telling a refused caller to check their connection sends them off to
|
|
238
|
+
// debug their wifi. The server answers a site that is not the caller's the
|
|
239
|
+
// same way as a site that does not exist, on purpose, so this wording does
|
|
240
|
+
// not distinguish them either.
|
|
256
241
|
cameras.value = [];
|
|
257
|
-
|
|
242
|
+
const status = Number(error?.statusCode ?? error?.response?.status ?? 0);
|
|
243
|
+
failed.value =
|
|
244
|
+
status === 401 || status === 403 || status === 404
|
|
245
|
+
? "You do not have access to this site's cameras."
|
|
246
|
+
: "The list of cameras did not load. Check your connection and try again.";
|
|
258
247
|
} finally {
|
|
259
248
|
loading.value = false;
|
|
260
249
|
}
|
|
261
250
|
}
|
|
262
251
|
|
|
263
252
|
onMounted(load);
|
|
264
|
-
watch(page, load);
|
|
265
253
|
watch(() => props.site, () => {
|
|
266
|
-
page.value = 1;
|
|
267
254
|
selectedIds.value = [];
|
|
268
255
|
load();
|
|
269
256
|
});
|
|
@@ -433,18 +420,6 @@ async function toggleFullscreen() {
|
|
|
433
420
|
color: var(--vms-text-dim);
|
|
434
421
|
}
|
|
435
422
|
|
|
436
|
-
.vms__pager {
|
|
437
|
-
display: flex;
|
|
438
|
-
align-items: center;
|
|
439
|
-
gap: 8px;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
.vms__range {
|
|
443
|
-
font-size: 12px;
|
|
444
|
-
color: var(--vms-text-dim);
|
|
445
|
-
white-space: nowrap;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
423
|
.vms__body {
|
|
449
424
|
display: flex;
|
|
450
425
|
min-height: 0;
|
|
@@ -99,6 +99,33 @@ export default function () {
|
|
|
99
99
|
});
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* The monitoring wall for one site.
|
|
104
|
+
*
|
|
105
|
+
* A different endpoint from `getAllSiteCameras`, and the difference is the
|
|
106
|
+
* point. That one is the Settings panel's paginated CRUD list; this one is
|
|
107
|
+
* purpose-built for a wall and is what the React Native app already uses:
|
|
108
|
+
*
|
|
109
|
+
* - it is **site-scoped and authorised server-side** — the caller must be a
|
|
110
|
+
* member of the site, of its owning organisation, or work for an
|
|
111
|
+
* organisation engaged to serve it;
|
|
112
|
+
* - it returns `type: "ip"` cameras only, so an ANPR unit can never land on a
|
|
113
|
+
* monitoring wall;
|
|
114
|
+
* - it returns each camera's **capability descriptor** and the server's own
|
|
115
|
+
* `unavailableReason`, so a tile explains itself instead of showing a black
|
|
116
|
+
* rectangle;
|
|
117
|
+
* - it never returns a credential, and it contacts no device.
|
|
118
|
+
*
|
|
119
|
+
* Unpaginated by design: a wall shows a site's cameras, and paging them was
|
|
120
|
+
* an artefact of borrowing the CRUD list.
|
|
121
|
+
*/
|
|
122
|
+
async function getSiteWall(siteId: string) {
|
|
123
|
+
return await useNuxtApp().$api<Record<string, any>>(
|
|
124
|
+
`/api/site-cameras/site/${siteId}/wall`,
|
|
125
|
+
{ method: "GET" }
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
102
129
|
async function updateSiteInformation(
|
|
103
130
|
siteId: string,
|
|
104
131
|
payload: { bgImage: string; description: string; docs: { id: string; name: string }[] }
|
|
@@ -175,6 +202,7 @@ export default function () {
|
|
|
175
202
|
updateSite,
|
|
176
203
|
addCamera,
|
|
177
204
|
getAllSiteCameras,
|
|
205
|
+
getSiteWall,
|
|
178
206
|
setSiteGuardPosts,
|
|
179
207
|
updateSiteCamera,
|
|
180
208
|
deleteSiteCameraById,
|
package/package.json
CHANGED
|
@@ -119,3 +119,31 @@ test("a server that says live video is unsupported is believed", () => {
|
|
|
119
119
|
assert.equal(v.showPlayer, false);
|
|
120
120
|
assert.equal(v.detail, "This camera is not active.");
|
|
121
121
|
});
|
|
122
|
+
|
|
123
|
+
/* ------------------------------------------- the server's reason wins */
|
|
124
|
+
|
|
125
|
+
test("the server's own refusal is shown verbatim, not replaced with a guess", () => {
|
|
126
|
+
// The one that matters: the browser cannot know this, and until the API host
|
|
127
|
+
// is configured it is the true answer for every camera in the estate.
|
|
128
|
+
const noRecorder = "No recorder is configured for this camera's relay.";
|
|
129
|
+
const view = tileView(ip({ unavailableReason: noRecorder }));
|
|
130
|
+
assert.equal(view.detail, noRecorder);
|
|
131
|
+
assert.equal(view.state, "unavailable");
|
|
132
|
+
assert.equal(view.showPlayer, false);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("the server's reason beats the reasons the wall could work out itself", () => {
|
|
136
|
+
const reason = "This camera has no address configured.";
|
|
137
|
+
assert.equal(tileView(ip({ status: "inactive", unavailableReason: reason })).detail, reason);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("an absent or blank reason changes nothing", () => {
|
|
141
|
+
assert.equal(tileView(ip({ unavailableReason: null })).state, "connecting");
|
|
142
|
+
assert.equal(tileView(ip({ unavailableReason: " " })).state, "connecting");
|
|
143
|
+
assert.equal(tileView(ip()).state, "connecting");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("being offline still explains every tile at once, ahead of the server's reason", () => {
|
|
147
|
+
const view = tileView(ip({ unavailableReason: "Something server-side." }), { online: false });
|
|
148
|
+
assert.equal(view.state, "offline");
|
|
149
|
+
});
|
package/utils/camera-wall.ts
CHANGED
|
@@ -51,6 +51,17 @@ export type TWallCamera = {
|
|
|
51
51
|
type?: string;
|
|
52
52
|
status?: string;
|
|
53
53
|
capabilities?: Record<string, TWallCapability>;
|
|
54
|
+
/**
|
|
55
|
+
* The server's own sentence for why this camera cannot show a picture, or
|
|
56
|
+
* `null`/absent when it can. Answered from the record before anything is
|
|
57
|
+
* requested, so it costs no device traffic.
|
|
58
|
+
*
|
|
59
|
+
* **Preferred over anything worked out here.** The server knows things the
|
|
60
|
+
* browser cannot — that no recorder is configured for this camera's relay, for
|
|
61
|
+
* instance, which is the true answer for every camera until the API host is
|
|
62
|
+
* configured. Replacing it with our own wording hides that.
|
|
63
|
+
*/
|
|
64
|
+
unavailableReason?: string | null;
|
|
54
65
|
};
|
|
55
66
|
|
|
56
67
|
/* -------------------------------------------------------------------------- */
|
|
@@ -251,6 +262,13 @@ export function tileView(
|
|
|
251
262
|
if (!camera) return dead("unavailable", "No camera in this position.");
|
|
252
263
|
if (!online)
|
|
253
264
|
return dead("offline", "This browser has no internet connection.");
|
|
265
|
+
// The server's refusal wins over anything reconstructed here. It covers the
|
|
266
|
+
// same two cases below AND the ones only it can know — chiefly "no recorder is
|
|
267
|
+
// configured for this camera's relay", which is the honest answer for every
|
|
268
|
+
// camera until the API host is configured, and which used to be replaced by
|
|
269
|
+
// our own guess.
|
|
270
|
+
const reason = camera.unavailableReason?.trim();
|
|
271
|
+
if (reason) return dead("unavailable", reason);
|
|
254
272
|
if (camera.status && camera.status !== "active")
|
|
255
273
|
return dead("unavailable", "This camera is not active.");
|
|
256
274
|
if (!camera.host) return dead("unavailable", "This camera has no address configured.");
|
package/middleware/member.ts
DELETED