@7365admin1/layer-common 3.2.3 → 3.2.5
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 +251 -0
- package/components/CameraWall.vue +1034 -0
- package/components/CameraWallTile.vue +818 -0
- package/components/Layout/NavigationDrawer.vue +43 -2
- package/components/NavigationItem.vue +9 -0
- package/composables/useSiteSettings.ts +50 -0
- package/package.json +3 -1
- package/plugins/vuetify.ts +37 -9
- package/utils/camera-wall.test.ts +571 -0
- package/utils/camera-wall.ts +926 -0
- package/utils/theme.test.ts +201 -0
- package/utils/theme.ts +136 -0
- package/middleware/member.ts +0 -4
|
@@ -1,9 +1,41 @@
|
|
|
1
1
|
<!-- LayoutNavigationDrawer.vue -->
|
|
2
2
|
<template>
|
|
3
|
-
|
|
3
|
+
<!--
|
|
4
|
+
`bg-brand-surface`, not `bg-primary`: `primary` is a foreground colour on a
|
|
5
|
+
dark page (it has to carry `class="text-primary"` sentences at 4.5:1), and a
|
|
6
|
+
fill that carries a label cannot also be that. `brand-surface` is a real
|
|
7
|
+
surface in each theme - see `utils/theme.ts` - so the drawer belongs to the
|
|
8
|
+
application it is in rather than being the one panel the theme switch never
|
|
9
|
+
reached. Its label reads 15.43:1 light, 13.21:1 dark.
|
|
10
|
+
|
|
11
|
+
NO HARDCODED LABEL COLOUR ANYWHERE IN HERE. Vuetify paints the drawer with
|
|
12
|
+
`on-brand-surface` and the list inside it inherits that through
|
|
13
|
+
`bg-transparent`, which the drawer supplies to every `v-list` it contains.
|
|
14
|
+
A `text-white` in here (or in an app's `#action` / `#services` slot) is
|
|
15
|
+
white on a light rail at 1.13:1.
|
|
16
|
+
|
|
17
|
+
`floating` dropped so the drawer draws its own edge, and the three
|
|
18
|
+
opacities below are raised on this element only:
|
|
19
|
+
|
|
20
|
+
- `border-opacity` 0.45. A surface is a low-contrast panel by design
|
|
21
|
+
(1.13:1 light, 1.19:1 dark against the page), so the edge is what carries
|
|
22
|
+
the boundary: 3.27:1 light, 4.33:1 dark. The theme-wide value stays where
|
|
23
|
+
it is; raising that would redraw every table and card edge in eleven
|
|
24
|
+
applications.
|
|
25
|
+
- `hover-opacity` 0.08. Material's 0.04 is 1.11:1 on this rail, which is
|
|
26
|
+
not feedback. 0.08 is 1.17:1 light, 1.24:1 dark.
|
|
27
|
+
- `activated-opacity` 0.18. The active item is `primary` text on a
|
|
28
|
+
primary-tinted fill (see `NavigationItem.vue`); 0.12 left the fill at
|
|
29
|
+
1.15:1, 0.18 is 1.44:1 light, 1.33:1 dark.
|
|
30
|
+
-->
|
|
31
|
+
<v-navigation-drawer
|
|
32
|
+
v-model="drawer"
|
|
33
|
+
permanent
|
|
34
|
+
class="bg-brand-surface brand-drawer"
|
|
35
|
+
>
|
|
4
36
|
<v-list>
|
|
5
37
|
<v-list-item>
|
|
6
|
-
<v-list-item-title class="text-h6
|
|
38
|
+
<v-list-item-title class="text-h6">
|
|
7
39
|
{{ props.title || APP_NAME }}
|
|
8
40
|
</v-list-item-title>
|
|
9
41
|
</v-list-item>
|
|
@@ -91,3 +123,12 @@ const handleClick = (item: any) => {
|
|
|
91
123
|
emit("navigate", item);
|
|
92
124
|
};
|
|
93
125
|
</script>
|
|
126
|
+
|
|
127
|
+
<style scoped>
|
|
128
|
+
/* Scoped to the drawer: none of these reach the rest of the application. */
|
|
129
|
+
.brand-drawer {
|
|
130
|
+
--v-border-opacity: 0.45;
|
|
131
|
+
--v-hover-opacity: 0.08;
|
|
132
|
+
--v-activated-opacity: 0.18;
|
|
133
|
+
}
|
|
134
|
+
</style>
|
|
@@ -1,9 +1,16 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
`color="primary"` is applied by Vuetify only while the item is active, so the
|
|
3
|
+
page you are on is named by colour and not just by a 1.15:1 tint you cannot
|
|
4
|
+
see. `primary` reads 14.56:1 on the light rail and 5.38:1 on the dark one,
|
|
5
|
+
and it is the one brand colour that is a foreground in both themes.
|
|
6
|
+
-->
|
|
1
7
|
<template>
|
|
2
8
|
<v-list-group v-if="children && children.length">
|
|
3
9
|
<template #activator="{ props: groupProps }">
|
|
4
10
|
<v-list-item
|
|
5
11
|
v-bind="groupProps"
|
|
6
12
|
:prepend-icon="icon"
|
|
13
|
+
color="primary"
|
|
7
14
|
class="text-subtitle-2"
|
|
8
15
|
@click.stop="onParentClick"
|
|
9
16
|
>
|
|
@@ -25,6 +32,7 @@
|
|
|
25
32
|
v-else-if="routeTo"
|
|
26
33
|
:prepend-icon="icon"
|
|
27
34
|
:to="routeTo"
|
|
35
|
+
color="primary"
|
|
28
36
|
class="text-subtitle-2"
|
|
29
37
|
>
|
|
30
38
|
{{ title }}
|
|
@@ -34,6 +42,7 @@
|
|
|
34
42
|
v-else-if="props.link"
|
|
35
43
|
:prepend-icon="icon"
|
|
36
44
|
:href="props.link"
|
|
45
|
+
color="primary"
|
|
37
46
|
class="text-subtitle-2"
|
|
38
47
|
>
|
|
39
48
|
{{ title }}
|
|
@@ -99,6 +99,54 @@ 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
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Health for the cameras currently on screen.
|
|
131
|
+
*
|
|
132
|
+
* Same site scoping as the wall, and the ids are narrowed to the visible
|
|
133
|
+
* tiles on purpose: this is the one camera call that can reach a device, and
|
|
134
|
+
* asking about cameras nobody is looking at spends a real budget for nothing.
|
|
135
|
+
*
|
|
136
|
+
* **Reachability is answered per RECORDER and cached there**, so a wall of
|
|
137
|
+
* twelve channels on one device is one connect rather than twelve, and
|
|
138
|
+
* several supervisors watching the same wall is still one. That is what makes
|
|
139
|
+
* it safe to ask on a timer at all — but only a slow one: the server caches
|
|
140
|
+
* for `healthCacheSeconds`, and asking faster than the cache buys nothing and
|
|
141
|
+
* costs round trips.
|
|
142
|
+
*/
|
|
143
|
+
async function getSiteHealth(siteId: string, cameraIds: string[]) {
|
|
144
|
+
return await useNuxtApp().$api<Record<string, any>>(
|
|
145
|
+
`/api/site-cameras/site/${siteId}/health`,
|
|
146
|
+
{ method: "GET", query: { ids: cameraIds.join(",") } }
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
102
150
|
async function updateSiteInformation(
|
|
103
151
|
siteId: string,
|
|
104
152
|
payload: { bgImage: string; description: string; docs: { id: string; name: string }[] }
|
|
@@ -175,6 +223,8 @@ export default function () {
|
|
|
175
223
|
updateSite,
|
|
176
224
|
addCamera,
|
|
177
225
|
getAllSiteCameras,
|
|
226
|
+
getSiteWall,
|
|
227
|
+
getSiteHealth,
|
|
178
228
|
setSiteGuardPosts,
|
|
179
229
|
updateSiteCamera,
|
|
180
230
|
deleteSiteCameraById,
|
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.
|
|
5
|
+
"version": "3.2.5",
|
|
6
6
|
"author": "7365admin1",
|
|
7
7
|
"main": "./nuxt.config.ts",
|
|
8
8
|
"publishConfig": {
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
"generate": "nuxt generate .playground",
|
|
16
16
|
"preview": "nuxt preview .playground",
|
|
17
17
|
"test": "esbuild composables/useVisitorSocket.ts --format=esm --outfile=test/.build/useVisitorSocket.mjs --log-level=error && node --test \"test/*.test.mjs\"",
|
|
18
|
+
"test:theme": "node --test \"utils/theme.test.ts\"",
|
|
19
|
+
"test:camera-wall": "node --test \"utils/camera-wall.test.ts\"",
|
|
18
20
|
"release": "yarn run build && changeset publish"
|
|
19
21
|
},
|
|
20
22
|
"devDependencies": {
|
package/plugins/vuetify.ts
CHANGED
|
@@ -42,6 +42,8 @@ import {
|
|
|
42
42
|
} from "vuetify-pro-tiptap";
|
|
43
43
|
import "vuetify-pro-tiptap/style.css";
|
|
44
44
|
|
|
45
|
+
import { LIGHT_THEME, DARK_THEME } from "../utils/theme";
|
|
46
|
+
|
|
45
47
|
export default defineNuxtPlugin((app) => {
|
|
46
48
|
const vuetify = createVuetify({
|
|
47
49
|
defaults: {
|
|
@@ -69,17 +71,43 @@ export default defineNuxtPlugin((app) => {
|
|
|
69
71
|
// hint: "This field is required",
|
|
70
72
|
// },
|
|
71
73
|
},
|
|
74
|
+
/**
|
|
75
|
+
* THE PRODUCT HAS TWO THEMES, AND UNTIL NOW ONLY ONE OF THEM WAS OURS.
|
|
76
|
+
*
|
|
77
|
+
* Every app shows a light/dark toggle in the app bar (Layout/Header.vue),
|
|
78
|
+
* and it sets Vuetify's built-in "light" and "dark" themes. Those were left
|
|
79
|
+
* at Vuetify's factory values, so one click on any screen in any of the
|
|
80
|
+
* eleven apps threw the brand away: `primary` stopped being the Seven365
|
|
81
|
+
* navy and became Material blue #2196F3 - and `primary` is the navigation
|
|
82
|
+
* drawer's fill - while `primary-button` and `text-primary`, used in around
|
|
83
|
+
* eighty places across the layer and the apps, do not exist in a stock
|
|
84
|
+
* theme at all, so those components lost their colour to an undefined CSS
|
|
85
|
+
* variable. There was also no way back to the brand: the toggle only ever
|
|
86
|
+
* flipped between the two stock themes.
|
|
87
|
+
*
|
|
88
|
+
* The fix is to make the stock names OURS rather than to add new names.
|
|
89
|
+
* Several components already branch on `theme.global.name === "dark"` (this
|
|
90
|
+
* layer's FormDialog, property-management's facility icons), so a
|
|
91
|
+
* differently-named dark theme would have left every one of them silently
|
|
92
|
+
* on their light branch inside a dark app. Overriding `light` and `dark`
|
|
93
|
+
* means the toggle, `v-theme-provider theme="light"`, the `plain-dark`
|
|
94
|
+
* layout and all of those comparisons keep working, and start being right.
|
|
95
|
+
*
|
|
96
|
+
* The colours themselves, and every ratio they were chosen for, are in
|
|
97
|
+
* `utils/theme.ts`, which `utils/theme.test.ts` measures on every run.
|
|
98
|
+
*/
|
|
72
99
|
theme: {
|
|
73
|
-
defaultTheme: "
|
|
100
|
+
defaultTheme: "light",
|
|
74
101
|
themes: {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
102
|
+
light: LIGHT_THEME,
|
|
103
|
+
dark: DARK_THEME,
|
|
104
|
+
/**
|
|
105
|
+
* The name this product booted on before the two above carried the
|
|
106
|
+
* brand. Kept, and identical to `light`, so anything that asks for it
|
|
107
|
+
* by name - in an app that is not in this repository - still gets a
|
|
108
|
+
* theme rather than a blank one.
|
|
109
|
+
*/
|
|
110
|
+
iservice365: LIGHT_THEME,
|
|
83
111
|
},
|
|
84
112
|
},
|
|
85
113
|
icons: {
|