@7365admin1/layer-common 4.117.0 → 4.118.1
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,21 @@
|
|
|
1
1
|
# @iservice365/layer-common
|
|
2
2
|
|
|
3
|
+
## 4.118.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 83b3d6d: Incident Report PDF: the header now carries the report status (new optional
|
|
8
|
+
`status` prop on `IncidentInformationDownload`), cells use one consistent 1px
|
|
9
|
+
border with no half-row page splits, and the dates print as DD/MM/YYYY in
|
|
10
|
+
Singapore time. They printed as MM/DD/YYYY, and from a UTC server an 8 Oct date
|
|
11
|
+
showed as 07/10. No field removed; NRIC masking unchanged.
|
|
12
|
+
|
|
13
|
+
## 4.118.0
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- 42930fa: Robot screens: Add robot, Edit, Remove and the building-map editor show only for Seven365 Super Admin. Commands on a HERO Fleet Core robot show only for Seven365 Super Admin; other users see status and position (view only). Matches the server rule that Fleet Core and robot setup are Super Admin only.
|
|
18
|
+
|
|
3
19
|
## 4.117.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
|
@@ -3117,11 +3133,11 @@ Monthly report: a month that had patrols opens the list of that month's patrol r
|
|
|
3117
3133
|
`utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
|
|
3118
3134
|
own rule from two endpoints the console already calls, unprojected:
|
|
3119
3135
|
|
|
3120
|
-
|
|
3121
|
-
|
|
3136
|
+
GET /api/members/user/:user/app/admin the Seven365 staff membership
|
|
3137
|
+
GET /api/roles/id/:role that membership's role document
|
|
3122
3138
|
|
|
3123
|
-
|
|
3124
|
-
|
|
3139
|
+
owner = member.type === "admin" && role.type === "admin" && role.default === true
|
|
3140
|
+
staff = member.type === "admin" && role.type === "admin"
|
|
3125
3141
|
|
|
3126
3142
|
`role.default` is the marker because it is the only property of a platform
|
|
3127
3143
|
staff role no API caller can set - `role.controller.ts` validates create and
|
|
@@ -1,42 +1,40 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
<!--
|
|
4
|
-
<
|
|
5
|
-
class="text-uppercase text-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
</
|
|
10
|
-
<v-row no-gutters class="
|
|
11
|
-
<p
|
|
12
|
-
class="w-100 text-h6 pa-2 text-center bg-warning text-on-warning"
|
|
13
|
-
>
|
|
2
|
+
<div class="ir-doc px-5 pt-4">
|
|
3
|
+
<!-- Report header: title with the status inside it, so the two never float apart. -->
|
|
4
|
+
<div class="ir-title bg-error text-on-error">
|
|
5
|
+
<p class="text-uppercase text-h6 ma-0">Incident Report</p>
|
|
6
|
+
<p v-if="status" class="ir-status ma-0 mt-2" :class="`ir-status--${statusKey}`">
|
|
7
|
+
Status: {{ String(status).toUpperCase() }}
|
|
8
|
+
</p>
|
|
9
|
+
</div>
|
|
10
|
+
<v-row no-gutters class="ir-box mt-5">
|
|
11
|
+
<p class="ir-section w-100 text-h6 pa-2 text-center bg-warning text-on-warning">
|
|
14
12
|
Incident Information
|
|
15
13
|
</p>
|
|
16
14
|
|
|
17
|
-
<v-col cols="6" class="pa-3
|
|
15
|
+
<v-col cols="6" class="pa-3 ir-cell ir-e">
|
|
18
16
|
<p class="font-weight-bold">Site</p>
|
|
19
17
|
<p>{{ siteName || "-" }}</p>
|
|
20
18
|
</v-col>
|
|
21
|
-
<v-col cols="6" class="pa-3
|
|
19
|
+
<v-col cols="6" class="pa-3 ir-cell">
|
|
22
20
|
<p class="font-weight-bold">Reference</p>
|
|
23
21
|
<p>{{ incidentInformation?.siteInfo?.irNumber || "-" }}</p>
|
|
24
22
|
</v-col>
|
|
25
23
|
|
|
26
|
-
<v-col cols="6" class="pa-3
|
|
24
|
+
<v-col cols="6" class="pa-3 ir-cell ir-e">
|
|
27
25
|
<p class="font-weight-bold">Place of Incident</p>
|
|
28
26
|
<p>
|
|
29
27
|
{{ incidentInformation?.placeOfIncident?.incidentLocation || "-" }}
|
|
30
28
|
</p>
|
|
31
29
|
</v-col>
|
|
32
|
-
<v-col cols="6" class="pa-3
|
|
30
|
+
<v-col cols="6" class="pa-3 ir-cell">
|
|
33
31
|
<p class="font-weight-bold">Type of Incident</p>
|
|
34
32
|
<p>
|
|
35
33
|
{{ incidentInformation?.incidentTypeAndTime?.typeOfIncident || "-" }}
|
|
36
34
|
</p>
|
|
37
35
|
</v-col>
|
|
38
36
|
|
|
39
|
-
<v-col cols="6" class="pa-3
|
|
37
|
+
<v-col cols="6" class="pa-3 ir-cell ir-e">
|
|
40
38
|
<p class="font-weight-bold">Date of Incident</p>
|
|
41
39
|
<p>
|
|
42
40
|
{{
|
|
@@ -46,55 +44,58 @@
|
|
|
46
44
|
}}
|
|
47
45
|
</p>
|
|
48
46
|
</v-col>
|
|
49
|
-
<v-col cols="6" class="pa-3
|
|
47
|
+
<v-col cols="6" class="pa-3 ir-cell">
|
|
50
48
|
<p class="font-weight-bold">Incident Start Time</p>
|
|
51
49
|
<p>
|
|
52
50
|
{{ incidentInformation?.incidentTypeAndTime?.incidentStart || "-" }}
|
|
53
51
|
</p>
|
|
54
52
|
</v-col>
|
|
55
53
|
|
|
56
|
-
<v-col cols="6" class="pa-3
|
|
54
|
+
<v-col cols="6" class="pa-3 ir-cell ir-e">
|
|
57
55
|
<p class="font-weight-bold">Time of Response (From 1st Contact)</p>
|
|
58
56
|
<p>
|
|
59
57
|
{{ incidentInformation?.incidentTypeAndTime?.timeOfResponse || "-" }}
|
|
60
58
|
</p>
|
|
61
59
|
</v-col>
|
|
62
|
-
<v-col cols="6" class="pa-3
|
|
60
|
+
<v-col cols="6" class="pa-3 ir-cell">
|
|
63
61
|
<p class="font-weight-bold">Incident End Time</p>
|
|
64
62
|
<p>
|
|
65
63
|
{{ incidentInformation?.incidentTypeAndTime?.incidentEnd || "-" }}
|
|
66
64
|
</p>
|
|
67
65
|
</v-col>
|
|
68
66
|
|
|
69
|
-
<v-col cols="6" class="pa-3
|
|
67
|
+
<v-col cols="6" class="pa-3 ir-cell ir-e">
|
|
70
68
|
<p class="font-weight-bold">Date of Report Submission</p>
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
<p>
|
|
70
|
+
{{
|
|
71
|
+
toLocalDate(incidentInformation?.submissionForm?.dateOfReport) ||
|
|
72
|
+
"NA"
|
|
73
|
+
}}
|
|
74
|
+
</p>
|
|
74
75
|
</v-col>
|
|
75
|
-
<v-col cols="6" class="pa-3
|
|
76
|
+
<v-col cols="6" class="pa-3 ir-cell">
|
|
76
77
|
<p class="font-weight-bold">Time of Report Submission</p>
|
|
77
78
|
<p>{{ incidentInformation?.submissionForm?.time || "-" }}</p>
|
|
78
79
|
</v-col>
|
|
79
80
|
|
|
80
|
-
<v-col cols="6" class="pa-3
|
|
81
|
+
<v-col cols="6" class="pa-3 ir-cell ir-e">
|
|
81
82
|
<p class="font-weight-bold">Report Submitted By</p>
|
|
82
83
|
<p>{{ incidentInformation?.siteInfo?.submittedBy || "-" }}</p>
|
|
83
84
|
</v-col>
|
|
84
|
-
<v-col cols="6" class="pa-3
|
|
85
|
+
<v-col cols="6" class="pa-3 ir-cell">
|
|
85
86
|
<p class="font-weight-bold">Designation</p>
|
|
86
87
|
<p>{{ incidentInformation?.siteInfo?.designation || "-" }}</p>
|
|
87
88
|
</v-col>
|
|
88
89
|
|
|
89
|
-
<v-col cols="4" class="pa-3
|
|
90
|
+
<v-col cols="4" class="pa-3 ir-cell ir-e">
|
|
90
91
|
<p class="font-weight-bold">Name of Informant/Complainant</p>
|
|
91
92
|
<p>{{ incidentInformation?.complaintInfo?.complainant || "-" }}</p>
|
|
92
93
|
</v-col>
|
|
93
|
-
<v-col cols="4" class="pa-3
|
|
94
|
+
<v-col cols="4" class="pa-3 ir-cell ir-e">
|
|
94
95
|
<p class="font-weight-bold">Contact No.</p>
|
|
95
96
|
<p>{{ incidentInformation?.complaintInfo?.contact || "-" }}</p>
|
|
96
97
|
</v-col>
|
|
97
|
-
<v-col cols="4" class="pa-3
|
|
98
|
+
<v-col cols="4" class="pa-3 ir-cell">
|
|
98
99
|
<p class="font-weight-bold">NRIC/WP No.</p>
|
|
99
100
|
<p>
|
|
100
101
|
{{
|
|
@@ -105,17 +106,17 @@
|
|
|
105
106
|
</p>
|
|
106
107
|
</v-col>
|
|
107
108
|
|
|
108
|
-
<v-col cols="4" class="pa-3
|
|
109
|
+
<v-col cols="4" class="pa-3 ir-cell ir-e">
|
|
109
110
|
<p class="font-weight-bold">
|
|
110
111
|
Name of Recipient of Complaint (1st Contact)
|
|
111
112
|
</p>
|
|
112
113
|
<p>{{ incidentInformation?.recipientOfComplaint?.recipient || "-" }}</p>
|
|
113
114
|
</v-col>
|
|
114
|
-
<v-col cols="4" class="pa-3
|
|
115
|
+
<v-col cols="4" class="pa-3 ir-cell ir-e">
|
|
115
116
|
<p class="font-weight-bold">Contact No.</p>
|
|
116
117
|
<p>{{ incidentInformation?.recipientOfComplaint?.contact || "-" }}</p>
|
|
117
118
|
</v-col>
|
|
118
|
-
<v-col cols="4" class="pa-3
|
|
119
|
+
<v-col cols="4" class="pa-3 ir-cell">
|
|
119
120
|
<p class="font-weight-bold">NRIC/WP No.</p>
|
|
120
121
|
<p>
|
|
121
122
|
{{
|
|
@@ -126,7 +127,7 @@
|
|
|
126
127
|
</p>
|
|
127
128
|
</v-col>
|
|
128
129
|
|
|
129
|
-
<v-col cols="12" class="pa-3
|
|
130
|
+
<v-col cols="12" class="pa-3 ir-cell">
|
|
130
131
|
<p class="font-weight-bold">Complaint Received through</p>
|
|
131
132
|
<p
|
|
132
133
|
v-if="
|
|
@@ -142,7 +143,7 @@
|
|
|
142
143
|
}}
|
|
143
144
|
</p>
|
|
144
145
|
</v-col>
|
|
145
|
-
<v-col cols="12" class="pa-3">
|
|
146
|
+
<v-col cols="12" class="pa-3 ir-cell ir-last">
|
|
146
147
|
<p class="font-weight-bold">Brief Description</p>
|
|
147
148
|
<p>
|
|
148
149
|
{{
|
|
@@ -151,7 +152,7 @@
|
|
|
151
152
|
</p>
|
|
152
153
|
</v-col>
|
|
153
154
|
</v-row>
|
|
154
|
-
</
|
|
155
|
+
</div>
|
|
155
156
|
</template>
|
|
156
157
|
|
|
157
158
|
<script lang="ts" setup>
|
|
@@ -162,8 +163,15 @@ const props = defineProps({
|
|
|
162
163
|
type: Object as PropType<Record<string, any> | null>,
|
|
163
164
|
required: true,
|
|
164
165
|
},
|
|
166
|
+
// Optional report status (pending/approved/rejected), printed inside the header.
|
|
167
|
+
status: {
|
|
168
|
+
type: String,
|
|
169
|
+
default: "",
|
|
170
|
+
},
|
|
165
171
|
});
|
|
166
172
|
|
|
173
|
+
const statusKey = computed(() => String(props.status || "").toLowerCase());
|
|
174
|
+
|
|
167
175
|
// utilities
|
|
168
176
|
const { getSiteById } = useSiteSettings();
|
|
169
177
|
const route = useRoute();
|
|
@@ -204,11 +212,16 @@ watch(
|
|
|
204
212
|
{ immediate: true }
|
|
205
213
|
);
|
|
206
214
|
|
|
215
|
+
// DD/MM/YYYY in Singapore time. The PDF is rendered on a server whose clock is
|
|
216
|
+
// UTC, so without the zone an 8 Oct (SGT) date prints as 07/10.
|
|
207
217
|
function toLocalDate(utcString: string) {
|
|
208
|
-
|
|
218
|
+
const d = new Date(utcString);
|
|
219
|
+
if (!utcString || isNaN(d.getTime())) return "";
|
|
220
|
+
return d.toLocaleDateString("en-GB", {
|
|
209
221
|
year: "numeric",
|
|
210
222
|
month: "2-digit",
|
|
211
223
|
day: "2-digit",
|
|
224
|
+
timeZone: "Asia/Singapore",
|
|
212
225
|
});
|
|
213
226
|
}
|
|
214
227
|
|
|
@@ -218,3 +231,48 @@ const maskNRIC = (value: any) => {
|
|
|
218
231
|
return value.replace(/.(?=.{3})/g, "*");
|
|
219
232
|
};
|
|
220
233
|
</script>
|
|
234
|
+
|
|
235
|
+
<style scoped>
|
|
236
|
+
/* One consistent 1px grey rule for every cell edge; rows never split across pages. */
|
|
237
|
+
.ir-title {
|
|
238
|
+
text-align: center;
|
|
239
|
+
padding: 10px;
|
|
240
|
+
}
|
|
241
|
+
.ir-status {
|
|
242
|
+
display: inline-block;
|
|
243
|
+
background: #fff;
|
|
244
|
+
color: #b26a00;
|
|
245
|
+
border: 1px solid currentColor;
|
|
246
|
+
border-radius: 4px;
|
|
247
|
+
padding: 2px 14px;
|
|
248
|
+
font-size: 0.95rem;
|
|
249
|
+
font-weight: 700;
|
|
250
|
+
letter-spacing: 0.03em;
|
|
251
|
+
}
|
|
252
|
+
.ir-status--approved { color: #2e7d32; }
|
|
253
|
+
.ir-status--rejected { color: #c62828; }
|
|
254
|
+
.ir-box {
|
|
255
|
+
border: 1px solid #8a8a8a;
|
|
256
|
+
}
|
|
257
|
+
.ir-cell {
|
|
258
|
+
border-bottom: 1px solid #8a8a8a;
|
|
259
|
+
break-inside: avoid;
|
|
260
|
+
page-break-inside: avoid;
|
|
261
|
+
}
|
|
262
|
+
.ir-e {
|
|
263
|
+
border-right: 1px solid #8a8a8a;
|
|
264
|
+
}
|
|
265
|
+
.ir-last {
|
|
266
|
+
border-bottom: 0;
|
|
267
|
+
/* The long free-text answer may flow onto the next page, but never leave
|
|
268
|
+
one or two stray lines behind. */
|
|
269
|
+
break-inside: auto;
|
|
270
|
+
page-break-inside: auto;
|
|
271
|
+
orphans: 3;
|
|
272
|
+
widows: 3;
|
|
273
|
+
}
|
|
274
|
+
.ir-section {
|
|
275
|
+
break-after: avoid;
|
|
276
|
+
border-bottom: 1px solid #8a8a8a;
|
|
277
|
+
}
|
|
278
|
+
</style>
|
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
<div v-if="commands.length" class="hero-detail__commands">
|
|
139
139
|
<RobotCommandBar
|
|
140
140
|
:capabilities="caps"
|
|
141
|
-
:can-command="
|
|
141
|
+
:can-command="cmdOk"
|
|
142
142
|
:online="live?.ok === true && live.online === true"
|
|
143
143
|
:mode="statusMode"
|
|
144
144
|
:type="robotType"
|
|
@@ -151,7 +151,7 @@
|
|
|
151
151
|
{{ commandResult.text }}
|
|
152
152
|
</div>
|
|
153
153
|
</div>
|
|
154
|
-
<RobotViewOnlyNotice v-if="!
|
|
154
|
+
<RobotViewOnlyNotice v-if="!cmdOk" />
|
|
155
155
|
</section>
|
|
156
156
|
</div>
|
|
157
157
|
|
|
@@ -369,11 +369,13 @@ const props = withDefaults(
|
|
|
369
369
|
canEdit?: boolean;
|
|
370
370
|
canCommand?: boolean;
|
|
371
371
|
canDelete?: boolean;
|
|
372
|
+
/** Seven365 Super Admin: the only one who may command a HERO Fleet Core robot (owner, 12 Oct 2026). */
|
|
373
|
+
superAdmin?: boolean;
|
|
372
374
|
/** The robot types this app presents - the edit dialog shows Type only with both. */
|
|
373
375
|
types?: Array<"cleaning" | "security">;
|
|
374
376
|
tickMs?: number;
|
|
375
377
|
}>(),
|
|
376
|
-
{ backLabel: "", canEdit: false, canCommand: false, canDelete: false, types: () => [], tickMs: 1200 },
|
|
378
|
+
{ backLabel: "", canEdit: false, canCommand: false, canDelete: false, superAdmin: false, types: () => [], tickMs: 1200 },
|
|
377
379
|
);
|
|
378
380
|
const emit = defineEmits<{
|
|
379
381
|
back: [];
|
|
@@ -398,6 +400,7 @@ const {
|
|
|
398
400
|
} = useRobots();
|
|
399
401
|
|
|
400
402
|
const record = ref<Record<string, any> | null>(null);
|
|
403
|
+
const cmdOk = computed(() => props.canCommand && (props.superAdmin || record.value?.brand !== "fleet-core"));
|
|
401
404
|
const topEl = ref<HTMLElement | null>(null);
|
|
402
405
|
const brands = ref<Array<Record<string, any>>>([]);
|
|
403
406
|
const error = ref("");
|
|
@@ -478,7 +481,7 @@ const makerApp = computed(() => (live.value?.ok ? live.value.profile?.makerApp :
|
|
|
478
481
|
|
|
479
482
|
const online = computed(() => live.value?.ok === true && live.value.online === true);
|
|
480
483
|
const commands = computed(() =>
|
|
481
|
-
visibleCommands({ capabilities: caps.value, canCommand:
|
|
484
|
+
visibleCommands({ capabilities: caps.value, canCommand: cmdOk.value, online: online.value, mode: statusMode.value, type: robotType.value, resumable: resumable.value }),
|
|
482
485
|
);
|
|
483
486
|
|
|
484
487
|
/* ---- the plan ---- */
|
|
@@ -506,7 +509,7 @@ watch(
|
|
|
506
509
|
const readout = computed(() => `${positionText(live.value, caps.value, now.value)} · ${speedText(prevPose.value, nextPose.value, statusMode.value)}`);
|
|
507
510
|
|
|
508
511
|
const spot = ref<Array<{ x: number; y: number }>>([]);
|
|
509
|
-
const spotOn = computed(() => showSpotClean({ canCommand:
|
|
512
|
+
const spotOn = computed(() => showSpotClean({ canCommand: cmdOk.value, capabilities: caps.value, online: online.value, type: robotType.value }));
|
|
510
513
|
|
|
511
514
|
// The route drawn on the plan: the shown map's first route, else the first with placed checkpoints (the rest are listed by name).
|
|
512
515
|
const route = computed<TPatrolRouteView | null>(() =>
|
|
@@ -839,9 +842,9 @@ function onPick(p: TSavedPoint) {
|
|
|
839
842
|
const livePanel = ref<{ show(): void } | null>(null);
|
|
840
843
|
const hasCamera = computed(() => caps.value?.liveVideo === true || canDrive.value);
|
|
841
844
|
const driveOpen = ref(false);
|
|
842
|
-
const canDrive = computed(() =>
|
|
845
|
+
const canDrive = computed(() => cmdOk.value && caps.value?.manualDrive === true && online.value);
|
|
843
846
|
const canResolve = computed(
|
|
844
|
-
() =>
|
|
847
|
+
() => cmdOk.value && caps.value?.resolveAlerts === true && faultBadge(fault.value) === "ALARM" && typeof fault.value?.code === "string",
|
|
845
848
|
);
|
|
846
849
|
const resolving = ref(false);
|
|
847
850
|
async function resolveFault() {
|
|
@@ -26,9 +26,10 @@
|
|
|
26
26
|
:key="robotId"
|
|
27
27
|
:robot-id="robotId"
|
|
28
28
|
:back-label="siteName || names.siteName ? `${siteName || names.siteName} fleet` : ''"
|
|
29
|
-
:can-edit="canEdit"
|
|
29
|
+
:can-edit="canEdit && superAdmin"
|
|
30
30
|
:can-command="canCommand"
|
|
31
|
-
:
|
|
31
|
+
:super-admin="superAdmin"
|
|
32
|
+
:can-delete="canDelete && superAdmin"
|
|
32
33
|
:types="types"
|
|
33
34
|
@back="emit('back')"
|
|
34
35
|
@removed="onRemoved"
|
|
@@ -42,7 +43,7 @@
|
|
|
42
43
|
:site-name="siteName"
|
|
43
44
|
:client-name="clientName"
|
|
44
45
|
:type="type"
|
|
45
|
-
:can-edit-map="canSetup && canEdit"
|
|
46
|
+
:can-edit-map="canSetup && canEdit && superAdmin"
|
|
46
47
|
:back-to-network="backToNetwork"
|
|
47
48
|
@open="(id) => emit('open', id)"
|
|
48
49
|
@network="emit('network')"
|
|
@@ -57,7 +58,7 @@
|
|
|
57
58
|
:client-name="clientName"
|
|
58
59
|
:type="type"
|
|
59
60
|
:can-command="canCommand"
|
|
60
|
-
:can-edit-map="canSetup && canEdit"
|
|
61
|
+
:can-edit-map="canSetup && canEdit && superAdmin"
|
|
61
62
|
:back-to-network="backToNetwork"
|
|
62
63
|
@open="(id) => emit('open', id)"
|
|
63
64
|
@network="emit('network')"
|
|
@@ -90,7 +91,7 @@
|
|
|
90
91
|
:site-name="siteName"
|
|
91
92
|
:client-name="clientName"
|
|
92
93
|
:type="type"
|
|
93
|
-
:can-setup="canSetup"
|
|
94
|
+
:can-setup="canSetup && superAdmin"
|
|
94
95
|
:back-to-network="backToNetwork"
|
|
95
96
|
:reload-key="reloadKey"
|
|
96
97
|
@open="(r) => emit('open', String(r._id))"
|
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.
|
|
5
|
+
"version": "4.118.1",
|
|
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.",
|