@7365admin1/layer-common 3.2.1 → 3.2.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,57 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 3.2.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 55160ca: Stop crashing on a null session in comments, feedback and visitor management
8
+
9
+ Several call sites read `currentUser.value._id` (and `.serviceProvider`,
10
+ `.type`, `.givenName`, `.surname`) with no guard. When the session is briefly
11
+ absent — during sign-out, on an expired session, or before the user has
12
+ resolved on first paint — `currentUser.value` is null and the property read
13
+ throws, taking the page down rather than the one action.
14
+
15
+ Those reads are now optional. Purely defensive: where a user exists the
16
+ behaviour is identical, and where one does not the field goes undefined instead
17
+ of throwing.
18
+
19
+ Covers the feedback list and its accept/submit handlers, the visitor
20
+ check-in/check-out path, and comment loading.
21
+
22
+ ## 3.2.2
23
+
24
+ ### Patch Changes
25
+
26
+ - d81dc2e: Camera-fault alerts get their own snackbar instead of riding the visitor one
27
+
28
+ Camera health was being sent through `showTransientMessage`, the same path as
29
+ unregistered-visitor plate alerts. That path returns early when the site has
30
+ the unregistered-visitor snackbar switched off, so **two of the five ANPR sites
31
+ were being told nothing at all when a camera went down** — the setting that
32
+ silenced them has nothing to do with cameras. The old snackbar was also hidden
33
+ whenever the operator was on the Unregistered tab.
34
+
35
+ Camera health is now its own alert, gated only on the permission to view
36
+ visitor data. It cannot be silenced by the unregistered-visitor switch and is
37
+ not hidden on that tab.
38
+
39
+ Several cameras failing at once collapse into a single line — "N cameras at
40
+ this site are not responding … Open Site Settings > Cameras to see which" —
41
+ rather than one snackbar per camera. Faults are keyed by camera id, so a repeat
42
+ for the same camera replaces its line instead of stacking another.
43
+
44
+ When a camera comes back, the operator who was told it was down is told it
45
+ recovered; the alert clears on its own.
46
+
47
+ Reads the optional `event` (`camera-fault` / `camera-recovered`) and `camera`
48
+ fields added to the socket payload in `@7365admin1/core`. Both are optional —
49
+ against an older API-core that sends a bare `message`, this behaves as it does
50
+ today and treats it as a fault.
51
+
52
+ **Expect an alert burst on first release** from the sites that were silent.
53
+ Warn the client before shipping.
54
+
3
55
  ## 3.2.1
4
56
 
5
57
  ### Patch Changes
@@ -721,9 +721,9 @@ const {
721
721
  _getFeedbacks({
722
722
  page: page.value,
723
723
  site: route.params.site as string,
724
- provider: currentUser.value.serviceProvider,
725
- ...(currentUser.value.type != "site" && {
726
- userId: currentUser.value._id,
724
+ provider: currentUser.value?.serviceProvider,
725
+ ...(currentUser.value?.type != "site" && {
726
+ userId: currentUser.value?._id,
727
727
  }),
728
728
  service: "Security",
729
729
  dateFrom: moment(startDate.value, "DD/MM/YYYY").startOf("day"),
@@ -998,9 +998,9 @@ async function handleAccept() {
998
998
  _id: feedbackData.value._id,
999
999
  statusUpdate: {
1000
1000
  status: "In-Progress",
1001
- updatedById: currentUser.value._id,
1002
- updatedByName: `${currentUser.value.givenName} ${currentUser.value.surname}`,
1003
- assignee: currentUser.value._id,
1001
+ updatedById: currentUser.value?._id,
1002
+ updatedByName: `${currentUser.value?.givenName} ${currentUser.value?.surname}`,
1003
+ assignee: currentUser.value?._id,
1004
1004
  provider: currentUser.value?.serviceProvider,
1005
1005
  },
1006
1006
  };
@@ -1158,7 +1158,7 @@ async function submit() {
1158
1158
  isSubmitting.value = true;
1159
1159
 
1160
1160
  const result = await createFeedback({
1161
- createdBy: currentUser.value._id,
1161
+ createdBy: currentUser.value?._id,
1162
1162
  description: _feedback.value.description,
1163
1163
  attachments: _feedback.value.attachments,
1164
1164
  site: route.params.site as string,
@@ -1708,7 +1708,7 @@ async function handleVisitorDataFromScannedQRCodeCheckInOut(
1708
1708
  site: visitorData.site ?? siteId,
1709
1709
  },
1710
1710
  update: {
1711
- updatedBy: currentUser.value._id,
1711
+ updatedBy: currentUser.value?._id,
1712
1712
  visitorPass: visitorPass.find(
1713
1713
  (i: Record<string, any>) => i.keyId === null
1714
1714
  )
@@ -66,6 +66,29 @@
66
66
  </template>
67
67
  </v-snackbar>
68
68
 
69
+ <!--
70
+ Camera health is its own alert, deliberately separate from the plate
71
+ snackbar above: it is not a visitor event, it must not be silenced by the
72
+ unregistered-visitor switch, and it must not be hidden on the unregistered
73
+ tab. Several failing cameras collapse into one line rather than one
74
+ snackbar each.
75
+ -->
76
+ <v-snackbar
77
+ v-if="canViewVisitor"
78
+ v-model="cameraAlert.modal"
79
+ :color="cameraFaults.size ? 'warning' : 'success'"
80
+ :timeout="10000"
81
+ location="top right"
82
+ class="rounded-xl"
83
+ multi-line
84
+ :z-index="100000000"
85
+ >
86
+ <span class="text-1-4rem">{{ cameraAlertText }}</span>
87
+ <template #actions>
88
+ <v-btn icon="mdi-close" variant="text" @click="cameraAlert.modal = false" />
89
+ </template>
90
+ </v-snackbar>
91
+
69
92
  <v-dialog v-model="permanentMessageDialog.modal" max-width="480" persistent>
70
93
  <v-card>
71
94
  <v-card-title class="text-h6">Notice</v-card-title>
@@ -132,6 +155,36 @@ const permanentMessageDialog = reactive({
132
155
  text: "",
133
156
  });
134
157
 
158
+ // Outstanding camera faults for this site, keyed by camera id so a repeat for
159
+ // the same camera replaces its line instead of adding another.
160
+ const cameraFaults = reactive(new Map<string, string>());
161
+ const cameraRecovery = ref("");
162
+ const cameraAlert = reactive({ modal: false });
163
+
164
+ const cameraAlertText = computed(
165
+ () => cameraAlertMessage([...cameraFaults.values()]) || cameraRecovery.value
166
+ );
167
+
168
+ const handleCameraHealth = (data: TVisitorSocketData) => {
169
+ // Older API-core builds send the fault text with no `event`, so a bare
170
+ // message is a fault.
171
+ if (data?.event === "camera-recovered") {
172
+ // Without a camera id there is no way to tell which fault cleared.
173
+ if (data?.camera) cameraFaults.delete(data.camera);
174
+ else cameraFaults.clear();
175
+ // Alerts are now infrequent, so the operator is told it came back —
176
+ // otherwise the only signal a camera recovered is silence.
177
+ cameraRecovery.value = cameraFaults.size ? "" : data?.message || "";
178
+ cameraAlert.modal = Boolean(cameraAlertText.value);
179
+ return;
180
+ }
181
+
182
+ if (!data?.message) return;
183
+ cameraRecovery.value = "";
184
+ cameraFaults.set(data?.camera || data.message, data.message);
185
+ cameraAlert.modal = true;
186
+ };
187
+
135
188
  watch(
136
189
  () => props.siteId,
137
190
  async (siteId) => {
@@ -233,8 +286,9 @@ const connectSocket = () => {
233
286
  }
234
287
 
235
288
 
236
- if (data?.message) {
237
- showTransientMessage(data.message, "error");
289
+ // Camera health, not a visitor event: its own alert, its own gate.
290
+ if (data?.message || data?.event) {
291
+ handleCameraHealth(data);
238
292
  }
239
293
 
240
294
  if (data?.messagePermanent) {
@@ -89,13 +89,13 @@ export default function useComment() {
89
89
 
90
90
  comments.value = _comments.items
91
91
  .map((comment: any) => {
92
- if (comment.createdBy === currentUser.value._id) {
92
+ if (comment.createdBy === currentUser.value?._id) {
93
93
  comment.justify = "end";
94
94
  } else {
95
95
  comment.justify = "start";
96
96
  if (
97
97
  ((Array.isArray(comment.seenBy) &&
98
- !comment.seenBy.includes(currentUser.value._id)) ||
98
+ !comment.seenBy.includes(currentUser.value?._id)) ||
99
99
  !comment?.seenBy) &&
100
100
  comment?._id
101
101
  ) {
@@ -111,7 +111,7 @@ export default function useComment() {
111
111
  );
112
112
 
113
113
  if (Array.isArray(updateSeenIds) && updateSeenIds.length > 0) {
114
- const seenBy = await updateSeenBy(updateSeenIds, currentUser.value._id);
114
+ const seenBy = await updateSeenBy(updateSeenIds, currentUser.value?._id);
115
115
  }
116
116
  } catch (error) {
117
117
  console.log("error :", error);
@@ -5,6 +5,32 @@ export type TVisitorSocketData = {
5
5
  message?: string;
6
6
  messagePermanent?: string;
7
7
  reload?: boolean;
8
+ /**
9
+ * Camera health. Sent alongside `message` by API-core so the client can group
10
+ * or clear an alert per camera instead of matching on the message text.
11
+ * Absent on older API-core builds — treat a bare `message` as a fault.
12
+ */
13
+ event?: "camera-fault" | "camera-recovered";
14
+ /** The camera the `event` is about. */
15
+ camera?: string;
16
+ };
17
+
18
+ /**
19
+ * One line for however many cameras are currently down.
20
+ *
21
+ * A site with several ANPR cameras could otherwise raise one alert per camera
22
+ * at the same moment. The single-camera case shows the server's own message,
23
+ * which names the camera; beyond that, naming them all in a snackbar is worse
24
+ * than sending the operator to the page that lists them.
25
+ */
26
+ export const cameraAlertMessage = (messages: string[]): string => {
27
+ if (!messages.length) return "";
28
+ if (messages.length === 1) return messages[0] as string;
29
+ return (
30
+ `${messages.length} cameras at this site are not responding. Plate reads ` +
31
+ `and automatic barrier opening are stopped for them. Open Site Settings > ` +
32
+ `Cameras to see which.`
33
+ );
8
34
  };
9
35
 
10
36
  export const useVisitorSocket = () => {
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",
5
+ "version": "3.2.3",
6
6
  "author": "7365admin1",
7
7
  "main": "./nuxt.config.ts",
8
8
  "publishConfig": {
@@ -14,6 +14,7 @@
14
14
  "build": "nuxt build .playground",
15
15
  "generate": "nuxt generate .playground",
16
16
  "preview": "nuxt preview .playground",
17
+ "test": "esbuild composables/useVisitorSocket.ts --format=esm --outfile=test/.build/useVisitorSocket.mjs --log-level=error && node --test \"test/*.test.mjs\"",
17
18
  "release": "yarn run build && changeset publish"
18
19
  },
19
20
  "devDependencies": {
@@ -0,0 +1,36 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+
4
+ import { cameraAlertMessage } from "./.build/useVisitorSocket.mjs";
5
+
6
+ /*
7
+ * The reported symptom was one camera raising a red toast every 10 seconds on
8
+ * an unrelated page. API-core now paces the alert; this side has to make sure
9
+ * several cameras failing at once still cannot become several snackbars.
10
+ */
11
+
12
+ test("nothing is shown when no camera is down", () => {
13
+ assert.equal(cameraAlertMessage([]), "");
14
+ });
15
+
16
+ test("one failing camera shows the server's own message, which names it", () => {
17
+ const text = "The entry and exit ANPR camera at Seventh Condominium is not responding.";
18
+ assert.equal(cameraAlertMessage([text]), text);
19
+ });
20
+
21
+ test("several failing cameras collapse into a single line", () => {
22
+ const many = ["camera one is down", "camera two is down", "camera three is down"];
23
+ const text = cameraAlertMessage(many);
24
+ assert.match(text, /^3 cameras at this site are not responding/);
25
+ for (const message of many) assert.doesNotMatch(text, new RegExp(message));
26
+ });
27
+
28
+ test("the collapsed line points at the page that lists them", () => {
29
+ assert.match(cameraAlertMessage(["a", "b"]), /Site Settings > Cameras/);
30
+ });
31
+
32
+ test("no camera message tells the operator to deactivate anything", () => {
33
+ for (const messages of [["a camera is down"], ["a", "b"]]) {
34
+ assert.doesNotMatch(cameraAlertMessage(messages), /inactive|deactivat/i);
35
+ }
36
+ });