@7365admin1/layer-common 3.1.4-staging.53 → 3.1.4-staging.55
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.
- package/components/CameraWall.vue +650 -0
- package/components/CameraWallTile.vue +333 -0
- package/components/DashboardMain.vue +10 -33
- package/composables/useEquipmentManagementPermission.ts +1 -1
- package/composables/useEquipmentPermission.ts +1 -1
- package/package.json +1 -1
- package/utils/camera-wall.test.ts +121 -0
- package/utils/camera-wall.ts +281 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE CAMERA WALL'S RULES, WITH NO MARKUP IN THEM.
|
|
3
|
+
*
|
|
4
|
+
* `CameraWall.vue` draws the wall; this file decides what it is allowed to draw.
|
|
5
|
+
* Keeping the two apart is not tidiness — every rule below is one somebody can
|
|
6
|
+
* get wrong in a way nobody notices until a supervisor is looking at the wrong
|
|
7
|
+
* picture, so each one is stated once, here, and pinned by `camera-wall.test.ts`.
|
|
8
|
+
*
|
|
9
|
+
* ## The four rules
|
|
10
|
+
*
|
|
11
|
+
* 1. **Only `type: "ip"` cameras go on a wall.** An ANPR unit belongs to visitor
|
|
12
|
+
* and vehicle management: it is pointed at a number plate, it is wired to a
|
|
13
|
+
* barrier, and its record carries a real device address and a real credential.
|
|
14
|
+
* Putting one on a monitoring wall is a defect, not a feature. The request
|
|
15
|
+
* already asks the server for `type=ip`; `wallCameras()` filters again on the
|
|
16
|
+
* way in, because "the caller passed the right query" is an assumption and
|
|
17
|
+
* this is the rule.
|
|
18
|
+
* 2. **A capability we have not been told about is NOT available.** An absent or
|
|
19
|
+
* malformed descriptor reads as `unknown`, and `unknown` counts as no — so a
|
|
20
|
+
* browser talking to an older API cannot draw a control that would do nothing.
|
|
21
|
+
* 3. **A control that is off is drawn, dimmed, and says why** — never hidden. The
|
|
22
|
+
* sentence shown is the SERVER'S own sentence, never rewritten here, so a
|
|
23
|
+
* camera cannot explain itself two different ways on two screens.
|
|
24
|
+
* 4. **"Live" means a picture is arriving**, not "we asked for one". The tile
|
|
25
|
+
* says `Connecting…` until the player page is actually up, and `No signal`
|
|
26
|
+
* when it never comes.
|
|
27
|
+
*
|
|
28
|
+
* These match the iSecure365 mobile monitoring build one for one, deliberately:
|
|
29
|
+
* the same product should not answer the same question differently on a phone
|
|
30
|
+
* and on a laptop.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/** One capability's answer, as the API returns it (core's `CameraCapabilityEntry`). */
|
|
34
|
+
export type TWallCapability = {
|
|
35
|
+
state?: "supported" | "unsupported" | "unknown";
|
|
36
|
+
transport?: string | null;
|
|
37
|
+
reason?: string | null;
|
|
38
|
+
/** The server's own sentence. Displayed verbatim; never rewritten here. */
|
|
39
|
+
detail?: string | null;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** A camera as the wall needs it. Extra fields on the record are ignored. */
|
|
43
|
+
export type TWallCamera = {
|
|
44
|
+
_id: string;
|
|
45
|
+
name?: string;
|
|
46
|
+
/**
|
|
47
|
+
* The stored address. For a `type: "ip"` camera this is a complete, directly
|
|
48
|
+
* renderable live-video page — it is the picture, not a fragment to resolve.
|
|
49
|
+
*/
|
|
50
|
+
host?: string;
|
|
51
|
+
type?: string;
|
|
52
|
+
status?: string;
|
|
53
|
+
capabilities?: Record<string, TWallCapability>;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/* -------------------------------------------------------------------------- */
|
|
57
|
+
/* Layouts */
|
|
58
|
+
/* -------------------------------------------------------------------------- */
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The three wall sizes.
|
|
62
|
+
*
|
|
63
|
+
* Square only, and only three of them. A VMS layout picker exists so a
|
|
64
|
+
* supervisor can change what they are watching in one click; a list of eleven
|
|
65
|
+
* arrangements is a settings screen wearing a toolbar's clothes.
|
|
66
|
+
*/
|
|
67
|
+
export const WALL_LAYOUTS = [
|
|
68
|
+
{ key: "1x1", columns: 1, tiles: 1, label: "Single" },
|
|
69
|
+
{ key: "2x2", columns: 2, tiles: 4, label: "Four" },
|
|
70
|
+
{ key: "3x3", columns: 3, tiles: 9, label: "Nine" },
|
|
71
|
+
] as const;
|
|
72
|
+
|
|
73
|
+
export type TWallLayoutKey = (typeof WALL_LAYOUTS)[number]["key"];
|
|
74
|
+
|
|
75
|
+
/** Four tiles: enough to be a wall, small enough to still be readable. */
|
|
76
|
+
export const DEFAULT_WALL_LAYOUT: TWallLayoutKey = "2x2";
|
|
77
|
+
|
|
78
|
+
export function wallLayout(key: string | undefined) {
|
|
79
|
+
return WALL_LAYOUTS.find((l) => l.key === key) ?? WALL_LAYOUTS[1];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* At three columns a tile is small enough that the chrome starts eating the
|
|
84
|
+
* picture, so the tile drops its status WORD and keeps the dot.
|
|
85
|
+
*/
|
|
86
|
+
export function isDenseLayout(key: string | undefined) {
|
|
87
|
+
return wallLayout(key).columns >= 3;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* -------------------------------------------------------------------------- */
|
|
91
|
+
/* Which cameras, and in what order */
|
|
92
|
+
/* -------------------------------------------------------------------------- */
|
|
93
|
+
|
|
94
|
+
/** Rule 1. Only `type: "ip"`. See the note at the top of this file. */
|
|
95
|
+
export function wallCameras(items: unknown): TWallCamera[] {
|
|
96
|
+
if (!Array.isArray(items)) return [];
|
|
97
|
+
return items.filter(
|
|
98
|
+
(c): c is TWallCamera => !!c && typeof c === "object" && (c as TWallCamera).type === "ip"
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The tiles, in order, padded with `null` to the layout's size.
|
|
104
|
+
*
|
|
105
|
+
* An empty selection is **not** "no cameras" — it is "nobody has chosen yet",
|
|
106
|
+
* and the wall falls back to the site's own order. A supervisor who clears the
|
|
107
|
+
* picker gets their site back, not a black screen.
|
|
108
|
+
*/
|
|
109
|
+
export function wallTiles(
|
|
110
|
+
cameras: TWallCamera[],
|
|
111
|
+
selectedIds: string[] | undefined,
|
|
112
|
+
layoutKey: string | undefined
|
|
113
|
+
): Array<TWallCamera | null> {
|
|
114
|
+
const { tiles } = wallLayout(layoutKey);
|
|
115
|
+
const byId = new Map(cameras.map((c) => [c._id, c]));
|
|
116
|
+
const chosen =
|
|
117
|
+
selectedIds && selectedIds.length
|
|
118
|
+
? selectedIds.map((id) => byId.get(id)).filter((c): c is TWallCamera => !!c)
|
|
119
|
+
: cameras;
|
|
120
|
+
|
|
121
|
+
const out: Array<TWallCamera | null> = chosen.slice(0, tiles);
|
|
122
|
+
while (out.length < tiles) out.push(null);
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* -------------------------------------------------------------------------- */
|
|
127
|
+
/* Capabilities */
|
|
128
|
+
/* -------------------------------------------------------------------------- */
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The two words a switched-off control wears.
|
|
132
|
+
*
|
|
133
|
+
* Lifted unchanged from the mobile build, and the "yet" is the whole point: it
|
|
134
|
+
* is the difference between a feature that is broken and a feature that is
|
|
135
|
+
* waiting on something somebody has to switch on.
|
|
136
|
+
*/
|
|
137
|
+
export const CAPABILITY_TAGS = {
|
|
138
|
+
unsupported: "Not set up yet",
|
|
139
|
+
unknown: "Not checked yet",
|
|
140
|
+
} as const;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* What a client with no descriptor at all should say. This is the server's own
|
|
144
|
+
* `device-not-probed` sentence, reused rather than reinvented, so an older API
|
|
145
|
+
* that sends no descriptor still explains itself in the product's own words.
|
|
146
|
+
*/
|
|
147
|
+
const NOT_PROBED = "This camera has not been asked what it can do yet.";
|
|
148
|
+
|
|
149
|
+
/** The four capabilities the wall draws a control for, and their plain names. */
|
|
150
|
+
export const WALL_CAPABILITY_CONTROLS = [
|
|
151
|
+
{ key: "playback", title: "Recorded footage" },
|
|
152
|
+
{ key: "events", title: "Events and alarms" },
|
|
153
|
+
{ key: "ptz", title: "Move camera" },
|
|
154
|
+
{ key: "presets", title: "Stored positions" },
|
|
155
|
+
] as const;
|
|
156
|
+
|
|
157
|
+
export type TCapabilityView = {
|
|
158
|
+
state: "supported" | "unsupported" | "unknown";
|
|
159
|
+
available: boolean;
|
|
160
|
+
/** `null` when the capability works — a working control needs no tag. */
|
|
161
|
+
tag: string | null;
|
|
162
|
+
/** The server's sentence, or the not-probed one. Never empty. */
|
|
163
|
+
detail: string;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/** Rule 2 and rule 3, in one place. */
|
|
167
|
+
export function capabilityView(
|
|
168
|
+
camera: TWallCamera | null | undefined,
|
|
169
|
+
key: string
|
|
170
|
+
): TCapabilityView {
|
|
171
|
+
const entry = camera?.capabilities?.[key];
|
|
172
|
+
const state =
|
|
173
|
+
entry?.state === "supported" || entry?.state === "unsupported"
|
|
174
|
+
? entry.state
|
|
175
|
+
: "unknown";
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
state,
|
|
179
|
+
available: state === "supported",
|
|
180
|
+
tag: state === "supported" ? null : CAPABILITY_TAGS[state],
|
|
181
|
+
detail: (entry?.detail || "").trim() || NOT_PROBED,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* -------------------------------------------------------------------------- */
|
|
186
|
+
/* Tile state */
|
|
187
|
+
/* -------------------------------------------------------------------------- */
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* How long a player page may take to load before the tile stops claiming it is
|
|
191
|
+
* coming. Deliberately generous — lifted from the mobile build, where the
|
|
192
|
+
* argument is that reporting a working video service as dead is the worse of
|
|
193
|
+
* the two mistakes.
|
|
194
|
+
*/
|
|
195
|
+
export const LIVE_CONNECT_TIMEOUT_MS = 15_000;
|
|
196
|
+
|
|
197
|
+
export type TTileState =
|
|
198
|
+
| "live"
|
|
199
|
+
| "connecting"
|
|
200
|
+
| "no-signal"
|
|
201
|
+
| "offline"
|
|
202
|
+
| "unavailable";
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The four words a tile may say about itself.
|
|
206
|
+
*
|
|
207
|
+
* `offline` and `no-signal` are two different facts and used to share one word:
|
|
208
|
+
* "Offline" on every tile at once means the BROWSER lost the network; "No
|
|
209
|
+
* signal" on one tile means that camera is not answering. A supervisor needs to
|
|
210
|
+
* know which of those they are looking at.
|
|
211
|
+
*/
|
|
212
|
+
export const TILE_LABELS = {
|
|
213
|
+
live: "Live",
|
|
214
|
+
connecting: "Connecting…",
|
|
215
|
+
"no-signal": "No signal",
|
|
216
|
+
offline: "No connection",
|
|
217
|
+
unavailable: "Not set up yet",
|
|
218
|
+
} as const;
|
|
219
|
+
|
|
220
|
+
/** What the browser knows about the embedded player page. */
|
|
221
|
+
export type TPlayerState = "loading" | "ready" | "timeout";
|
|
222
|
+
|
|
223
|
+
export type TTileView = {
|
|
224
|
+
state: TTileState;
|
|
225
|
+
label: string;
|
|
226
|
+
/** A sentence for the states that need explaining; `null` when live. */
|
|
227
|
+
detail: string | null;
|
|
228
|
+
/** Whether the player page may be embedded at all. */
|
|
229
|
+
showPlayer: boolean;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Rule 4, plus every reason a tile may have nothing to show.
|
|
234
|
+
*
|
|
235
|
+
* Order matters: the browser being offline beats everything (it explains every
|
|
236
|
+
* tile at once), then the record's own problems, then the server's verdict on
|
|
237
|
+
* live video, and only then what the embedded page is actually doing.
|
|
238
|
+
*/
|
|
239
|
+
export function tileView(
|
|
240
|
+
camera: TWallCamera | null | undefined,
|
|
241
|
+
opts: { online?: boolean; player?: TPlayerState } = {}
|
|
242
|
+
): TTileView {
|
|
243
|
+
const online = opts.online !== false;
|
|
244
|
+
const dead = (state: TTileState, detail: string | null): TTileView => ({
|
|
245
|
+
state,
|
|
246
|
+
label: TILE_LABELS[state],
|
|
247
|
+
detail,
|
|
248
|
+
showPlayer: false,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
if (!camera) return dead("unavailable", "No camera in this position.");
|
|
252
|
+
if (!online)
|
|
253
|
+
return dead("offline", "This browser has no internet connection.");
|
|
254
|
+
if (camera.status && camera.status !== "active")
|
|
255
|
+
return dead("unavailable", "This camera is not active.");
|
|
256
|
+
if (!camera.host) return dead("unavailable", "This camera has no address configured.");
|
|
257
|
+
|
|
258
|
+
const live = capabilityView(camera, "liveVideo");
|
|
259
|
+
if (live.state === "unsupported") return dead("unavailable", live.detail);
|
|
260
|
+
|
|
261
|
+
const player = opts.player ?? "loading";
|
|
262
|
+
if (player === "timeout")
|
|
263
|
+
return {
|
|
264
|
+
state: "no-signal",
|
|
265
|
+
label: TILE_LABELS["no-signal"],
|
|
266
|
+
// Invented: only the browser can know the page never loaded, so the
|
|
267
|
+
// server has no reason code for it. Worded as a fact, not a diagnosis —
|
|
268
|
+
// we genuinely cannot tell a dead camera from a dead video service.
|
|
269
|
+
detail: "The live view did not load. The camera or the video service may be down.",
|
|
270
|
+
showPlayer: true,
|
|
271
|
+
};
|
|
272
|
+
if (player === "loading")
|
|
273
|
+
return {
|
|
274
|
+
state: "connecting",
|
|
275
|
+
label: TILE_LABELS.connecting,
|
|
276
|
+
detail: null,
|
|
277
|
+
showPlayer: true,
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
return { state: "live", label: TILE_LABELS.live, detail: null, showPlayer: true };
|
|
281
|
+
}
|