@7365admin1/layer-common 4.1.3-staging.253 → 4.1.3-staging.255
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.
|
@@ -15,7 +15,67 @@
|
|
|
15
15
|
placeholder="Enter CCTV camera name"
|
|
16
16
|
/>
|
|
17
17
|
</v-col>
|
|
18
|
-
<v-col cols="12">
|
|
18
|
+
<v-col v-if="isCCTV" cols="12" class="mb-2">
|
|
19
|
+
<InputLabel
|
|
20
|
+
class="text-capitalize"
|
|
21
|
+
title="How do you have this camera's address?"
|
|
22
|
+
/>
|
|
23
|
+
<v-radio-group
|
|
24
|
+
v-model="addressMode"
|
|
25
|
+
inline
|
|
26
|
+
hide-details
|
|
27
|
+
density="comfortable"
|
|
28
|
+
>
|
|
29
|
+
<v-radio label="From the recorder" value="recorder" />
|
|
30
|
+
<v-radio label="I already have a link" value="url" />
|
|
31
|
+
</v-radio-group>
|
|
32
|
+
</v-col>
|
|
33
|
+
|
|
34
|
+
<template v-if="showRecorderFields">
|
|
35
|
+
<v-col cols="12">
|
|
36
|
+
<InputLabel
|
|
37
|
+
class="text-capitalize"
|
|
38
|
+
title="Recorder address"
|
|
39
|
+
required
|
|
40
|
+
/>
|
|
41
|
+
<v-text-field
|
|
42
|
+
v-model.trim="recorder.host"
|
|
43
|
+
density="comfortable"
|
|
44
|
+
:rules="[requiredRule]"
|
|
45
|
+
placeholder="Enter the recorder's address"
|
|
46
|
+
hint="The address of the recorder (NVR) this camera is plugged into, on the site network."
|
|
47
|
+
persistent-hint
|
|
48
|
+
/>
|
|
49
|
+
</v-col>
|
|
50
|
+
|
|
51
|
+
<v-col cols="5" class="pr-2">
|
|
52
|
+
<InputLabel class="text-capitalize" title="Port" />
|
|
53
|
+
<v-text-field
|
|
54
|
+
v-model.trim="recorder.port"
|
|
55
|
+
density="comfortable"
|
|
56
|
+
:rules="[portRule]"
|
|
57
|
+
placeholder="Leave blank if unsure"
|
|
58
|
+
/>
|
|
59
|
+
</v-col>
|
|
60
|
+
|
|
61
|
+
<v-col cols="7">
|
|
62
|
+
<InputLabel
|
|
63
|
+
class="text-capitalize"
|
|
64
|
+
title="Channel number"
|
|
65
|
+
required
|
|
66
|
+
/>
|
|
67
|
+
<v-text-field
|
|
68
|
+
v-model.trim="recorder.channel"
|
|
69
|
+
density="comfortable"
|
|
70
|
+
:rules="[requiredRule, channelRule]"
|
|
71
|
+
placeholder="e.g. 6"
|
|
72
|
+
hint="The channel this camera sits on in the recorder's own list."
|
|
73
|
+
persistent-hint
|
|
74
|
+
/>
|
|
75
|
+
</v-col>
|
|
76
|
+
</template>
|
|
77
|
+
|
|
78
|
+
<v-col v-if="showUrlField" cols="12">
|
|
19
79
|
<InputLabel class="text-capitalize" title="URL" required />
|
|
20
80
|
<v-text-field
|
|
21
81
|
v-model.trim="camera.host"
|
|
@@ -26,6 +86,26 @@
|
|
|
26
86
|
/>
|
|
27
87
|
</v-col>
|
|
28
88
|
|
|
89
|
+
<v-col v-if="isCCTV" cols="12" class="pt-2">
|
|
90
|
+
<v-btn
|
|
91
|
+
variant="outlined"
|
|
92
|
+
class="text-none"
|
|
93
|
+
:disabled="!canTest || disable"
|
|
94
|
+
:loading="testing"
|
|
95
|
+
@click="testCamera"
|
|
96
|
+
text="Test this camera"
|
|
97
|
+
/>
|
|
98
|
+
|
|
99
|
+
<v-alert
|
|
100
|
+
v-if="testResult"
|
|
101
|
+
class="mt-3"
|
|
102
|
+
variant="tonal"
|
|
103
|
+
density="compact"
|
|
104
|
+
:type="testResult.ok ? 'success' : 'warning'"
|
|
105
|
+
:text="testResult.message"
|
|
106
|
+
/>
|
|
107
|
+
</v-col>
|
|
108
|
+
|
|
29
109
|
<!-- <v-col v-if="isANPR" cols="12">
|
|
30
110
|
<InputLabel class="text-capitalize" title="Category" required />
|
|
31
111
|
<v-select
|
|
@@ -232,6 +312,108 @@ const message = ref("");
|
|
|
232
312
|
const isANPR = computed(() => prop.type === "anpr");
|
|
233
313
|
const isCCTV = computed(() => prop.type === "ip");
|
|
234
314
|
|
|
315
|
+
const { addCamera, updateSiteCamera, testSiteCamera } = useSiteSettings();
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Two ways to say where a camera is, and the guided one is the default.
|
|
319
|
+
*
|
|
320
|
+
* A CCTV camera's stored address is a relay player page. Nothing printed on a
|
|
321
|
+
* camera, and nothing in a recorder's own screens, will ever tell somebody what
|
|
322
|
+
* it is — until now it had to be requested from whoever runs the video service
|
|
323
|
+
* and pasted in. `recorder` collects what a person can genuinely find out and
|
|
324
|
+
* the server derives the address; `url` is the old field, kept because cameras
|
|
325
|
+
* configured that way must keep editing exactly as they do today and because
|
|
326
|
+
* some people do legitimately have a ready-made link.
|
|
327
|
+
*
|
|
328
|
+
* Editing an existing camera opens on `url`, so nothing about an already-working
|
|
329
|
+
* record changes shape under the person looking at it.
|
|
330
|
+
*/
|
|
331
|
+
const addressMode = ref<"recorder" | "url">(
|
|
332
|
+
prop.type === "ip" && prop.mode !== "edit" ? "recorder" : "url"
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
const recorder = ref({ host: "", port: "", channel: "" });
|
|
336
|
+
|
|
337
|
+
const showRecorderFields = computed(
|
|
338
|
+
() => isCCTV.value && addressMode.value === "recorder"
|
|
339
|
+
);
|
|
340
|
+
const showUrlField = computed(
|
|
341
|
+
() => !isCCTV.value || addressMode.value === "url"
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
const portRule = (v: string | any) => {
|
|
345
|
+
if (!v) return true;
|
|
346
|
+
const port = Number(v);
|
|
347
|
+
return (Number.isInteger(port) && port >= 1 && port <= 65535) || "Enter a port number between 1 and 65535, or leave it blank.";
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const channelRule = (v: string | any) => {
|
|
351
|
+
if (!v) return true;
|
|
352
|
+
const channel = Number(v);
|
|
353
|
+
return (
|
|
354
|
+
(Number.isInteger(channel) && channel >= 1 && channel <= 256) ||
|
|
355
|
+
"Enter the channel number, from 1 to 256."
|
|
356
|
+
);
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
/* ---------------------------- test before saving --------------------------- */
|
|
360
|
+
|
|
361
|
+
const testing = ref(false);
|
|
362
|
+
const testResult = ref<{ ok: boolean; message: string } | null>(null);
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* What the address fields currently say, in the shape the API takes.
|
|
366
|
+
*
|
|
367
|
+
* The same object is used for the test and for the save, so a green test is a
|
|
368
|
+
* test of the thing that gets saved and not of something near it.
|
|
369
|
+
*/
|
|
370
|
+
function addressPayload() {
|
|
371
|
+
if (showRecorderFields.value) {
|
|
372
|
+
return {
|
|
373
|
+
host: "",
|
|
374
|
+
recorderHost: recorder.value.host,
|
|
375
|
+
recorderPort: recorder.value.port ? Number(recorder.value.port) : undefined,
|
|
376
|
+
channel: recorder.value.channel ? Number(recorder.value.channel) : undefined,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return { host: camera.value.host };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const canTest = computed(() => {
|
|
384
|
+
if (testing.value) return false;
|
|
385
|
+
if (showRecorderFields.value) {
|
|
386
|
+
return Boolean(recorder.value.host && recorder.value.channel);
|
|
387
|
+
}
|
|
388
|
+
return Boolean(camera.value.host);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// A verdict belongs to the address that produced it. Change the address and the
|
|
392
|
+
// old verdict is no longer about anything.
|
|
393
|
+
watch(
|
|
394
|
+
() => JSON.stringify(addressPayload()),
|
|
395
|
+
() => {
|
|
396
|
+
testResult.value = null;
|
|
397
|
+
}
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
async function testCamera() {
|
|
401
|
+
testing.value = true;
|
|
402
|
+
testResult.value = null;
|
|
403
|
+
|
|
404
|
+
try {
|
|
405
|
+
const result = await testSiteCamera(prop.site, addressPayload());
|
|
406
|
+
testResult.value = { ok: result.ok, message: result.message };
|
|
407
|
+
} catch (error: any) {
|
|
408
|
+
testResult.value = {
|
|
409
|
+
ok: false,
|
|
410
|
+
message: cameraErrorConverter(error, prop.type),
|
|
411
|
+
};
|
|
412
|
+
} finally {
|
|
413
|
+
testing.value = false;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
235
417
|
const anprCategoryOptions = computed(() => [
|
|
236
418
|
{ title: "Resident", value: "resident" },
|
|
237
419
|
{ title: "Visitor", value: "visitor" },
|
|
@@ -265,7 +447,10 @@ const hasChanges = computed(() => {
|
|
|
265
447
|
camera.value.category !== prop.camera.category ||
|
|
266
448
|
camera.value.guardPost !== prop.camera.guardPost ||
|
|
267
449
|
camera.value.direction !== prop.camera.direction ||
|
|
268
|
-
camera.value.name !== prop.camera.name
|
|
450
|
+
camera.value.name !== prop.camera.name ||
|
|
451
|
+
// Switching an existing camera over to the recorder fields is a change
|
|
452
|
+
// even though its stored URL has not been retyped.
|
|
453
|
+
showRecorderFields.value
|
|
269
454
|
);
|
|
270
455
|
}
|
|
271
456
|
|
|
@@ -273,20 +458,18 @@ const hasChanges = computed(() => {
|
|
|
273
458
|
return true;
|
|
274
459
|
});
|
|
275
460
|
|
|
276
|
-
const { addCamera, updateSiteCamera } = useSiteSettings();
|
|
277
|
-
|
|
278
461
|
async function submit() {
|
|
279
462
|
disable.value = true;
|
|
280
463
|
camera.value.guardPost = camera.value.guardPost ?? 0;
|
|
281
464
|
|
|
282
465
|
try {
|
|
283
466
|
if (prop.mode === "add") {
|
|
284
|
-
await addCamera(camera.value);
|
|
467
|
+
await addCamera({ ...camera.value, ...addressPayload() });
|
|
285
468
|
}
|
|
286
469
|
|
|
287
470
|
if (prop.mode === "edit") {
|
|
288
471
|
await updateSiteCamera(camera.value._id ?? "", {
|
|
289
|
-
|
|
472
|
+
...addressPayload(),
|
|
290
473
|
username: camera.value.username,
|
|
291
474
|
password: camera.value.password,
|
|
292
475
|
category: camera.value.category,
|
|
@@ -48,13 +48,56 @@ export default function () {
|
|
|
48
48
|
}
|
|
49
49
|
);
|
|
50
50
|
}
|
|
51
|
-
async function addCamera(camera: TSiteCamera) {
|
|
51
|
+
async function addCamera(camera: TSiteCamera & TCameraByRecorder) {
|
|
52
52
|
return await useNuxtApp().$api<Record<string, any>>(`/api/site-cameras`, {
|
|
53
53
|
method: "POST",
|
|
54
54
|
body: camera,
|
|
55
55
|
});
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Describe a camera by the recorder it is on instead of by a finished URL.
|
|
60
|
+
*
|
|
61
|
+
* The stored address is a relay player page, which nobody outside the video
|
|
62
|
+
* service can work out. These three fields are what a person can actually
|
|
63
|
+
* read off a recorder, and the server turns them into that address. Optional
|
|
64
|
+
* everywhere: sending `host` still works exactly as it always has.
|
|
65
|
+
*/
|
|
66
|
+
type TCameraByRecorder = {
|
|
67
|
+
recorderHost?: string;
|
|
68
|
+
recorderPort?: number;
|
|
69
|
+
channel?: number;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** What a "Test this camera" press comes back with. Never an address. */
|
|
73
|
+
type TCameraTestResult = {
|
|
74
|
+
ok: boolean;
|
|
75
|
+
status:
|
|
76
|
+
| "working"
|
|
77
|
+
| "no-video"
|
|
78
|
+
| "credential-refused"
|
|
79
|
+
| "unreachable"
|
|
80
|
+
| "cannot-test";
|
|
81
|
+
message: string;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Try an address before saving it. Read-only, and it runs on the server —
|
|
86
|
+
* the browser never touches a recorder and never sees a credential.
|
|
87
|
+
*/
|
|
88
|
+
async function testSiteCamera(
|
|
89
|
+
siteId: string,
|
|
90
|
+
payload: { host?: string } & TCameraByRecorder
|
|
91
|
+
) {
|
|
92
|
+
return await useNuxtApp().$api<TCameraTestResult>(
|
|
93
|
+
`/api/site-cameras/site/${siteId}/test`,
|
|
94
|
+
{
|
|
95
|
+
method: "POST",
|
|
96
|
+
body: payload,
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
58
101
|
async function updateSiteCamera(
|
|
59
102
|
id: string,
|
|
60
103
|
value: Partial<
|
|
@@ -68,7 +111,8 @@ export default function () {
|
|
|
68
111
|
| "direction"
|
|
69
112
|
| "status"
|
|
70
113
|
>
|
|
71
|
-
>
|
|
114
|
+
> &
|
|
115
|
+
TCameraByRecorder
|
|
72
116
|
) {
|
|
73
117
|
return await useNuxtApp().$api<Record<string, any>>(
|
|
74
118
|
`/api/site-cameras/id/${id}`,
|
|
@@ -258,6 +302,7 @@ export default function () {
|
|
|
258
302
|
getSiteUnits,
|
|
259
303
|
updateSite,
|
|
260
304
|
addCamera,
|
|
305
|
+
testSiteCamera,
|
|
261
306
|
getAllSiteCameras,
|
|
262
307
|
bulkImportSiteCameras,
|
|
263
308
|
getSiteWall,
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@7365admin1/layer-common",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "4.1.3-staging.
|
|
5
|
+
"version": "4.1.3-staging.255",
|
|
6
6
|
"author": "7365admin1",
|
|
7
7
|
"main": "./nuxt.config.ts",
|
|
8
8
|
"//files": "What a consumer extending this layer actually loads. Without this npm ships the whole working tree - the changesets, the CI workflows, the render harness in tools/ and any scratch directory that happened to exist at publish time. Nuxt resolves a layer by directory, so every runtime directory below has to stay listed; adding a new top-level runtime directory means adding it here too.",
|