@openinc/parse-server-opendash 4.1.8 → 4.2.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/dist/features/openservice/init.js +4 -0
- package/dist/features/openservice/schedules/calendarSync/types/Event.d.ts +2 -0
- package/dist/features/openservice/serviceSchedules/acl/assignmentAcl.d.ts +30 -0
- package/dist/features/openservice/serviceSchedules/acl/assignmentAcl.js +85 -0
- package/dist/features/openservice/serviceSchedules/acl/index.d.ts +1 -0
- package/dist/features/openservice/serviceSchedules/acl/index.js +6 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/functions/eventToPlan.d.ts +50 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/functions/eventToPlan.js +115 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/functions/outlookRecurrence.d.ts +59 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/functions/outlookRecurrence.js +353 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/functions/planToEvent.d.ts +27 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/functions/planToEvent.js +145 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/index.d.ts +8 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/index.js +28 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/service/ServiceCalendarSyncPoller.d.ts +54 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/service/ServiceCalendarSyncPoller.js +248 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/service/serviceCalendarMeta.d.ts +55 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/service/serviceCalendarMeta.js +109 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/service/syncPlanToCalendar.d.ts +26 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/service/syncPlanToCalendar.js +122 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/types/ServiceSyncTypes.d.ts +55 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/types/ServiceSyncTypes.js +8 -0
- package/dist/features/openservice/serviceSchedules/initServiceSchedulesFeature.d.ts +10 -0
- package/dist/features/openservice/serviceSchedules/initServiceSchedulesFeature.js +39 -0
- package/dist/features/openservice/serviceTickets/alarmActions/functions/alarmTicketFields.d.ts +35 -0
- package/dist/features/openservice/serviceTickets/alarmActions/functions/alarmTicketFields.js +103 -0
- package/dist/features/openservice/serviceTickets/alarmActions/index.d.ts +7 -0
- package/dist/features/openservice/serviceTickets/alarmActions/index.js +16 -0
- package/dist/features/openservice/serviceTickets/alarmActions/service/createAlarmTicket.d.ts +33 -0
- package/dist/features/openservice/serviceTickets/alarmActions/service/createAlarmTicket.js +179 -0
- package/dist/features/openservice/serviceTickets/alarmActions/service/ensureAlarmTicketAction.d.ts +22 -0
- package/dist/features/openservice/serviceTickets/alarmActions/service/ensureAlarmTicketAction.js +127 -0
- package/dist/features/openservice/serviceTickets/alarmActions/types/AlarmTicketTypes.d.ts +80 -0
- package/dist/features/openservice/serviceTickets/alarmActions/types/AlarmTicketTypes.js +28 -0
- package/dist/features/permissions/types/Permissions.d.ts +1 -2
- package/dist/features/permissions/types/Permissions.js +8 -2
- package/dist/functions/openinc-openservice-alarm-create-ticket.d.ts +25 -0
- package/dist/functions/openinc-openservice-alarm-create-ticket.js +63 -0
- package/dist/functions/openinc-openservice-notify-contacts.d.ts +16 -0
- package/dist/functions/openinc-openservice-notify-contacts.js +82 -0
- package/dist/functions/openinc-openservice-schedule-calendar-sync.d.ts +15 -0
- package/dist/functions/openinc-openservice-schedule-calendar-sync.js +60 -0
- package/dist/hooks/Service_Schedule.js +89 -16
- package/dist/hooks/Service_Schedule_Template.js +28 -7
- package/dist/hooks/Service_Ticket.js +2 -2
- package/dist/types/Service_Schedule.d.ts +12 -0
- package/dist/types/Service_Schedule.js +21 -0
- package/dist/types/Service_Schedule_Execution.d.ts +6 -3
- package/dist/types/Service_Schedule_Execution.js +6 -0
- package/dist/types/Service_Schedule_Template.d.ts +7 -0
- package/dist/types/Service_Schedule_Template.js +12 -0
- package/package.json +5 -4
- package/schema/Service_Schedule.json +17 -0
- package/schema/Service_Schedule_Execution.json +5 -0
- package/schema/Service_Schedule_Template.json +9 -0
- package/dist/features/ruleset/applyRuleSet.d.ts +0 -41
- package/dist/features/ruleset/applyRuleSet.js +0 -81
package/dist/features/openservice/serviceTickets/alarmActions/functions/alarmTicketFields.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type AlarmEvent, type AlarmTicketParams } from "../types/AlarmTicketTypes.js";
|
|
2
|
+
/**
|
|
3
|
+
* Alarm + action parameters → the text fields of a ticket.
|
|
4
|
+
*
|
|
5
|
+
* Pure, so the wording and — more importantly — the `origin` marker can be
|
|
6
|
+
* checked without a database. The marker is what makes the whole thing
|
|
7
|
+
* idempotent: it identifies the alarm, and one open ticket per marker is the
|
|
8
|
+
* default.
|
|
9
|
+
*/
|
|
10
|
+
/** Title, description and origin of the ticket a fired alarm produces. */
|
|
11
|
+
export interface AlarmTicketFields {
|
|
12
|
+
title: string;
|
|
13
|
+
description: string;
|
|
14
|
+
origin: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function alarmTicketFields(event: AlarmEvent, params?: AlarmTicketParams): AlarmTicketFields;
|
|
17
|
+
/**
|
|
18
|
+
* The `origin` marker: `alarm:<key>`.
|
|
19
|
+
*
|
|
20
|
+
* Same idea as the schedule plugin's `schedule:<id>:<occurrence>` — a
|
|
21
|
+
* free-form, greppable string that says where a ticket came from and can be
|
|
22
|
+
* queried back (`equalTo("origin", …)`). The key is the alarm's id when the
|
|
23
|
+
* executor knows it, otherwise source and sensor, which stay stable across
|
|
24
|
+
* firings of the same alarm.
|
|
25
|
+
*/
|
|
26
|
+
export declare function alarmOrigin(event: AlarmEvent): string;
|
|
27
|
+
/**
|
|
28
|
+
* A stable identity for the alarm — the one thing a payload must carry.
|
|
29
|
+
*
|
|
30
|
+
* Returns `undefined` when the executor reports neither an alarm id nor a
|
|
31
|
+
* sensor: such a call cannot be deduplicated and is rejected rather than
|
|
32
|
+
* silently creating a ticket on every firing.
|
|
33
|
+
*/
|
|
34
|
+
export declare function alarmKey(event: AlarmEvent): string | undefined;
|
|
35
|
+
export declare function firedAt(event: AlarmEvent): Date;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.alarmTicketFields = alarmTicketFields;
|
|
4
|
+
exports.alarmOrigin = alarmOrigin;
|
|
5
|
+
exports.alarmKey = alarmKey;
|
|
6
|
+
exports.firedAt = firedAt;
|
|
7
|
+
const AlarmTicketTypes_js_1 = require("../types/AlarmTicketTypes.js");
|
|
8
|
+
function alarmTicketFields(event, params = {}) {
|
|
9
|
+
return {
|
|
10
|
+
title: renderTitle(event, params),
|
|
11
|
+
description: renderDescription(event, params),
|
|
12
|
+
origin: alarmOrigin(event),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The `origin` marker: `alarm:<key>`.
|
|
17
|
+
*
|
|
18
|
+
* Same idea as the schedule plugin's `schedule:<id>:<occurrence>` — a
|
|
19
|
+
* free-form, greppable string that says where a ticket came from and can be
|
|
20
|
+
* queried back (`equalTo("origin", …)`). The key is the alarm's id when the
|
|
21
|
+
* executor knows it, otherwise source and sensor, which stay stable across
|
|
22
|
+
* firings of the same alarm.
|
|
23
|
+
*/
|
|
24
|
+
function alarmOrigin(event) {
|
|
25
|
+
return `alarm:${alarmKey(event)}`;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A stable identity for the alarm — the one thing a payload must carry.
|
|
29
|
+
*
|
|
30
|
+
* Returns `undefined` when the executor reports neither an alarm id nor a
|
|
31
|
+
* sensor: such a call cannot be deduplicated and is rejected rather than
|
|
32
|
+
* silently creating a ticket on every firing.
|
|
33
|
+
*/
|
|
34
|
+
function alarmKey(event) {
|
|
35
|
+
if (event.alarmId)
|
|
36
|
+
return event.alarmId;
|
|
37
|
+
const sensor = [event.item_source, event.item_id].filter(Boolean).join("/");
|
|
38
|
+
if (!sensor)
|
|
39
|
+
return undefined;
|
|
40
|
+
return event.item_dimension != null
|
|
41
|
+
? `${sensor}#${event.item_dimension}`
|
|
42
|
+
: sensor;
|
|
43
|
+
}
|
|
44
|
+
/** `{{alarm}}` / `{{sensor}}` / `{{value}}` in the configured title. */
|
|
45
|
+
function renderTitle(event, params) {
|
|
46
|
+
const template = params.title?.trim();
|
|
47
|
+
const sensor = sensorLabel(event);
|
|
48
|
+
if (!template) {
|
|
49
|
+
// No template: the alarm's own name is the most recognisable thing there
|
|
50
|
+
// is, and a ticket must have a title.
|
|
51
|
+
return event.name || (sensor ? `Alarm: ${sensor}` : "Alarm");
|
|
52
|
+
}
|
|
53
|
+
return template
|
|
54
|
+
.replace(/\{\{\s*alarm\s*\}\}/gi, event.name ?? "")
|
|
55
|
+
.replace(/\{\{\s*sensor\s*\}\}/gi, sensor)
|
|
56
|
+
.replace(/\{\{\s*value\s*\}\}/gi, formatValue(event.value))
|
|
57
|
+
.replace(/\s+/g, " ")
|
|
58
|
+
.trim();
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The configured note plus what the alarm actually reported.
|
|
62
|
+
*
|
|
63
|
+
* The details are appended rather than templated, because the person handling
|
|
64
|
+
* the ticket needs to know which sensor, which value and when — and forgetting
|
|
65
|
+
* a placeholder in the form should not lose that.
|
|
66
|
+
*/
|
|
67
|
+
function renderDescription(event, params) {
|
|
68
|
+
const lines = [];
|
|
69
|
+
const note = params.description?.trim();
|
|
70
|
+
if (note)
|
|
71
|
+
lines.push(note, "");
|
|
72
|
+
const sensor = sensorLabel(event);
|
|
73
|
+
if (event.name)
|
|
74
|
+
lines.push(`Alarm: ${event.name}`);
|
|
75
|
+
if (sensor)
|
|
76
|
+
lines.push(`Sensor: ${sensor}`);
|
|
77
|
+
if (event.value !== undefined)
|
|
78
|
+
lines.push(`Wert: ${formatValue(event.value)}`);
|
|
79
|
+
if (event.condition)
|
|
80
|
+
lines.push(`Regel: ${event.condition}`);
|
|
81
|
+
lines.push(`Ausgelöst: ${firedAt(event).toISOString()}`);
|
|
82
|
+
lines.push(`Quelle: ${AlarmTicketTypes_js_1.ALARM_TICKET_ACTION_ID}`);
|
|
83
|
+
return lines.join("\n");
|
|
84
|
+
}
|
|
85
|
+
function firedAt(event) {
|
|
86
|
+
if (event.firedAt) {
|
|
87
|
+
const parsed = new Date(event.firedAt);
|
|
88
|
+
if (!Number.isNaN(parsed.getTime()))
|
|
89
|
+
return parsed;
|
|
90
|
+
}
|
|
91
|
+
return new Date();
|
|
92
|
+
}
|
|
93
|
+
function sensorLabel(event) {
|
|
94
|
+
const sensor = [event.item_source, event.item_id].filter(Boolean).join("/");
|
|
95
|
+
return event.item_dimension != null && sensor
|
|
96
|
+
? `${sensor} [${event.item_dimension}]`
|
|
97
|
+
: sensor;
|
|
98
|
+
}
|
|
99
|
+
function formatValue(value) {
|
|
100
|
+
if (value === undefined || value === null)
|
|
101
|
+
return "";
|
|
102
|
+
return String(value);
|
|
103
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { alarmKey, alarmOrigin, alarmTicketFields, firedAt, } from "./functions/alarmTicketFields.js";
|
|
2
|
+
export type { AlarmTicketFields } from "./functions/alarmTicketFields.js";
|
|
3
|
+
export { createAlarmTicket, tenantForSecret, } from "./service/createAlarmTicket.js";
|
|
4
|
+
export type { AlarmTicketResult } from "./service/createAlarmTicket.js";
|
|
5
|
+
export { ensureAlarmTicketAction } from "./service/ensureAlarmTicketAction.js";
|
|
6
|
+
export { ALARM_TICKET_ACTION_ID, ALARM_TICKET_SECRET_CONFIG_KEY, } from "./types/AlarmTicketTypes.js";
|
|
7
|
+
export type { AlarmEvent, AlarmTicketParams, } from "./types/AlarmTicketTypes.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ALARM_TICKET_SECRET_CONFIG_KEY = exports.ALARM_TICKET_ACTION_ID = exports.ensureAlarmTicketAction = exports.tenantForSecret = exports.createAlarmTicket = exports.firedAt = exports.alarmTicketFields = exports.alarmOrigin = exports.alarmKey = void 0;
|
|
4
|
+
var alarmTicketFields_js_1 = require("./functions/alarmTicketFields.js");
|
|
5
|
+
Object.defineProperty(exports, "alarmKey", { enumerable: true, get: function () { return alarmTicketFields_js_1.alarmKey; } });
|
|
6
|
+
Object.defineProperty(exports, "alarmOrigin", { enumerable: true, get: function () { return alarmTicketFields_js_1.alarmOrigin; } });
|
|
7
|
+
Object.defineProperty(exports, "alarmTicketFields", { enumerable: true, get: function () { return alarmTicketFields_js_1.alarmTicketFields; } });
|
|
8
|
+
Object.defineProperty(exports, "firedAt", { enumerable: true, get: function () { return alarmTicketFields_js_1.firedAt; } });
|
|
9
|
+
var createAlarmTicket_js_1 = require("./service/createAlarmTicket.js");
|
|
10
|
+
Object.defineProperty(exports, "createAlarmTicket", { enumerable: true, get: function () { return createAlarmTicket_js_1.createAlarmTicket; } });
|
|
11
|
+
Object.defineProperty(exports, "tenantForSecret", { enumerable: true, get: function () { return createAlarmTicket_js_1.tenantForSecret; } });
|
|
12
|
+
var ensureAlarmTicketAction_js_1 = require("./service/ensureAlarmTicketAction.js");
|
|
13
|
+
Object.defineProperty(exports, "ensureAlarmTicketAction", { enumerable: true, get: function () { return ensureAlarmTicketAction_js_1.ensureAlarmTicketAction; } });
|
|
14
|
+
var AlarmTicketTypes_js_1 = require("./types/AlarmTicketTypes.js");
|
|
15
|
+
Object.defineProperty(exports, "ALARM_TICKET_ACTION_ID", { enumerable: true, get: function () { return AlarmTicketTypes_js_1.ALARM_TICKET_ACTION_ID; } });
|
|
16
|
+
Object.defineProperty(exports, "ALARM_TICKET_SECRET_CONFIG_KEY", { enumerable: true, get: function () { return AlarmTicketTypes_js_1.ALARM_TICKET_SECRET_CONFIG_KEY; } });
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Tenant } from "../../../../../types/index.js";
|
|
2
|
+
import { type AlarmEvent, type AlarmTicketParams } from "../types/AlarmTicketTypes.js";
|
|
3
|
+
/** Outcome of one fired alarm. */
|
|
4
|
+
export interface AlarmTicketResult {
|
|
5
|
+
ticketId: string;
|
|
6
|
+
/** `false` when an open ticket for this alarm already existed. */
|
|
7
|
+
created: boolean;
|
|
8
|
+
origin: string;
|
|
9
|
+
equipmentId?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Create the ticket a fired alarm asks for — or return the one that is already
|
|
13
|
+
* open for it.
|
|
14
|
+
*
|
|
15
|
+
* **One open ticket per alarm** is the default (`params.duplicate` opts out).
|
|
16
|
+
* A sensor that crosses its threshold every few minutes would otherwise bury
|
|
17
|
+
* the inbox, and the person on shift needs one work item, not a log. Closing
|
|
18
|
+
* the ticket arms the alarm again, which is the behaviour people expect from
|
|
19
|
+
* every other alerting tool.
|
|
20
|
+
*
|
|
21
|
+
* Deliberately thin: number, ACL, assignment notifications and the tenant's
|
|
22
|
+
* watcher rules all happen in the `Service_Ticket` hooks, on every write path.
|
|
23
|
+
*/
|
|
24
|
+
export declare function createAlarmTicket(tenant: Tenant, event: AlarmEvent, params?: AlarmTicketParams): Promise<AlarmTicketResult>;
|
|
25
|
+
/**
|
|
26
|
+
* The tenant a call belongs to, identified by its shared secret.
|
|
27
|
+
*
|
|
28
|
+
* The executor lives outside this server and is not trusted to name a tenant —
|
|
29
|
+
* so the secret does it. One `OD3_Config` row per tenant
|
|
30
|
+
* (`OPENSERVICE_ALARM_TICKET_SECRET`); no row means the tenant has not switched
|
|
31
|
+
* the endpoint on, and the call is refused.
|
|
32
|
+
*/
|
|
33
|
+
export declare function tenantForSecret(secret: string): Promise<Tenant | undefined>;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createAlarmTicket = createAlarmTicket;
|
|
7
|
+
exports.tenantForSecret = tenantForSecret;
|
|
8
|
+
const node_1 = __importDefault(require("parse/node"));
|
|
9
|
+
const index_js_1 = require("../../../../../types/index.js");
|
|
10
|
+
const alarmTicketFields_js_1 = require("../functions/alarmTicketFields.js");
|
|
11
|
+
const AlarmTicketTypes_js_1 = require("../types/AlarmTicketTypes.js");
|
|
12
|
+
/**
|
|
13
|
+
* Create the ticket a fired alarm asks for — or return the one that is already
|
|
14
|
+
* open for it.
|
|
15
|
+
*
|
|
16
|
+
* **One open ticket per alarm** is the default (`params.duplicate` opts out).
|
|
17
|
+
* A sensor that crosses its threshold every few minutes would otherwise bury
|
|
18
|
+
* the inbox, and the person on shift needs one work item, not a log. Closing
|
|
19
|
+
* the ticket arms the alarm again, which is the behaviour people expect from
|
|
20
|
+
* every other alerting tool.
|
|
21
|
+
*
|
|
22
|
+
* Deliberately thin: number, ACL, assignment notifications and the tenant's
|
|
23
|
+
* watcher rules all happen in the `Service_Ticket` hooks, on every write path.
|
|
24
|
+
*/
|
|
25
|
+
async function createAlarmTicket(tenant, event, params = {}) {
|
|
26
|
+
if (!(0, alarmTicketFields_js_1.alarmKey)(event)) {
|
|
27
|
+
throw new node_1.default.Error(node_1.default.Error.INVALID_QUERY, "The alarm payload identifies no alarm: pass alarmId, or item_source/item_id.");
|
|
28
|
+
}
|
|
29
|
+
const fields = (0, alarmTicketFields_js_1.alarmTicketFields)(event, params);
|
|
30
|
+
if (!params.duplicate) {
|
|
31
|
+
const open = await findOpenTicket(tenant, fields.origin);
|
|
32
|
+
if (open) {
|
|
33
|
+
return {
|
|
34
|
+
ticketId: open.id,
|
|
35
|
+
created: false,
|
|
36
|
+
origin: fields.origin,
|
|
37
|
+
equipmentId: open.get("equipment")?.id,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const status = await resolveDefaultStatus(tenant);
|
|
42
|
+
if (!status) {
|
|
43
|
+
throw new node_1.default.Error(node_1.default.Error.INVALID_QUERY, `No "${AlarmTicketTypes_js_1.DEFAULT_STATUS_CATEGORY}" status configured for this tenant — cannot create a ticket.`);
|
|
44
|
+
}
|
|
45
|
+
const equipment = await resolveEquipment(tenant, event, params);
|
|
46
|
+
const ticket = new index_js_1.Service_Ticket();
|
|
47
|
+
ticket.set("deleted", false);
|
|
48
|
+
ticket.set("tenant", tenant);
|
|
49
|
+
ticket.set("status", status);
|
|
50
|
+
ticket.set("title", fields.title);
|
|
51
|
+
ticket.set("description", fields.description);
|
|
52
|
+
ticket.set("origin", fields.origin);
|
|
53
|
+
// The moment the alarm fired, not the moment we got the call — a queued or
|
|
54
|
+
// retried webhook must not move the incident.
|
|
55
|
+
ticket.set("startDate", (0, alarmTicketFields_js_1.firedAt)(event));
|
|
56
|
+
if (equipment)
|
|
57
|
+
ticket.set("equipment", equipment);
|
|
58
|
+
if (params.priority != null)
|
|
59
|
+
ticket.set("priority", params.priority);
|
|
60
|
+
if (params.projectId) {
|
|
61
|
+
const project = new index_js_1.Service_Project();
|
|
62
|
+
project.id = params.projectId;
|
|
63
|
+
ticket.set("project", project);
|
|
64
|
+
}
|
|
65
|
+
// Assignment arrays hold plain ids in this schema (see the `pointerIds`
|
|
66
|
+
// helper in the ticket hook, which accepts both).
|
|
67
|
+
if (params.assignedUserIds?.length) {
|
|
68
|
+
ticket.set("assignedUsers", params.assignedUserIds);
|
|
69
|
+
}
|
|
70
|
+
if (params.assignedRoleIds?.length) {
|
|
71
|
+
ticket.set("assignedRoles", params.assignedRoleIds);
|
|
72
|
+
}
|
|
73
|
+
if (params.tagIds?.length)
|
|
74
|
+
ticket.set("tags", params.tagIds);
|
|
75
|
+
await ticket.save(null, { useMasterKey: true });
|
|
76
|
+
return {
|
|
77
|
+
ticketId: ticket.id,
|
|
78
|
+
created: true,
|
|
79
|
+
origin: fields.origin,
|
|
80
|
+
equipmentId: equipment?.id,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* An unfinished ticket of this alarm, if there is one.
|
|
85
|
+
*
|
|
86
|
+
* "Unfinished" is decided by the **status category**, not by `closedAt`: a
|
|
87
|
+
* tenant can name its statuses whatever it likes, the category is the contract
|
|
88
|
+
* (`done` / `cancelled` — the same list core uses).
|
|
89
|
+
*/
|
|
90
|
+
async function findOpenTicket(tenant, origin) {
|
|
91
|
+
const tickets = await new node_1.default.Query(index_js_1.Service_Ticket)
|
|
92
|
+
.equalTo("tenant", tenant)
|
|
93
|
+
.equalTo("origin", origin)
|
|
94
|
+
.equalTo("deleted", false)
|
|
95
|
+
.include("status")
|
|
96
|
+
.descending("createdAt")
|
|
97
|
+
.limit(20)
|
|
98
|
+
.find({ useMasterKey: true });
|
|
99
|
+
return tickets.find((ticket) => {
|
|
100
|
+
const category = ticket.get("status")?.get("category");
|
|
101
|
+
return !AlarmTicketTypes_js_1.DONE_STATUS_CATEGORIES.includes(category);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/** The tenant's inbox status, else a global one — never another tenant's. */
|
|
105
|
+
async function resolveDefaultStatus(tenant) {
|
|
106
|
+
const statuses = await new node_1.default.Query(index_js_1.Service_Status)
|
|
107
|
+
.equalTo("category", AlarmTicketTypes_js_1.DEFAULT_STATUS_CATEGORY)
|
|
108
|
+
.equalTo("deleted", false)
|
|
109
|
+
.ascending("order")
|
|
110
|
+
.find({ useMasterKey: true });
|
|
111
|
+
return (statuses.find((status) => status.get("tenant")?.id === tenant.id) ?? statuses.find((status) => !status.get("tenant")));
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The machine the ticket belongs to.
|
|
115
|
+
*
|
|
116
|
+
* 1. what was configured on the alarm — the only reliable source, because
|
|
117
|
+
* **nothing in the data model links a sensor to a machine**;
|
|
118
|
+
* 2. otherwise a machine whose `sourceTags` name the sensor or its source.
|
|
119
|
+
* That is a convention, not a relation, so it is a fallback: `sourceTags`
|
|
120
|
+
* was meant for source scoping, and a site-level tag can match several
|
|
121
|
+
* machines — the first one wins, which is a guess.
|
|
122
|
+
*
|
|
123
|
+
* No machine is not an error: `equipment` is optional on a ticket, and an
|
|
124
|
+
* unassigned alarm ticket in the inbox is better than one on the wrong machine.
|
|
125
|
+
*/
|
|
126
|
+
async function resolveEquipment(tenant, event, params) {
|
|
127
|
+
if (params.equipmentId) {
|
|
128
|
+
const explicit = await new node_1.default.Query(index_js_1.Service_Equipment)
|
|
129
|
+
.equalTo("objectId", params.equipmentId)
|
|
130
|
+
.first({ useMasterKey: true });
|
|
131
|
+
if (explicit)
|
|
132
|
+
return explicit;
|
|
133
|
+
console.warn(`[AlarmTicket] Configured machine ${params.equipmentId} does not exist`);
|
|
134
|
+
}
|
|
135
|
+
const candidates = [event.item_id, event.item_source].filter((value) => !!value);
|
|
136
|
+
if (candidates.length === 0)
|
|
137
|
+
return undefined;
|
|
138
|
+
const query = new node_1.default.Query(index_js_1.Service_Equipment)
|
|
139
|
+
.equalTo("tenant", tenant)
|
|
140
|
+
.notEqualTo("deleted", true)
|
|
141
|
+
// `sourceTags` is an Array column: Mongo reads "contained in" as "the array
|
|
142
|
+
// holds one of these". The generated type only knows `any[]`, hence the cast.
|
|
143
|
+
.containedIn("sourceTags", candidates);
|
|
144
|
+
return (await query.first({ useMasterKey: true })) ?? undefined;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The tenant a call belongs to, identified by its shared secret.
|
|
148
|
+
*
|
|
149
|
+
* The executor lives outside this server and is not trusted to name a tenant —
|
|
150
|
+
* so the secret does it. One `OD3_Config` row per tenant
|
|
151
|
+
* (`OPENSERVICE_ALARM_TICKET_SECRET`); no row means the tenant has not switched
|
|
152
|
+
* the endpoint on, and the call is refused.
|
|
153
|
+
*/
|
|
154
|
+
async function tenantForSecret(secret) {
|
|
155
|
+
if (!secret)
|
|
156
|
+
return undefined;
|
|
157
|
+
const configs = await new node_1.default.Query(index_js_1.Config)
|
|
158
|
+
.equalTo("key", AlarmTicketTypes_js_1.ALARM_TICKET_SECRET_CONFIG_KEY)
|
|
159
|
+
.include("tenant")
|
|
160
|
+
.find({ useMasterKey: true });
|
|
161
|
+
const match = configs.find((config) => {
|
|
162
|
+
const value = config.get("value");
|
|
163
|
+
return typeof value === "string" && constantTimeEqual(value.trim(), secret);
|
|
164
|
+
});
|
|
165
|
+
return match?.get("tenant") ?? undefined;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Compare without leaking the answer through timing — the caller controls one
|
|
169
|
+
* side of this comparison and may try many.
|
|
170
|
+
*/
|
|
171
|
+
function constantTimeEqual(a, b) {
|
|
172
|
+
if (a.length !== b.length)
|
|
173
|
+
return false;
|
|
174
|
+
let diff = 0;
|
|
175
|
+
for (let i = 0; i < a.length; i++) {
|
|
176
|
+
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
177
|
+
}
|
|
178
|
+
return diff === 0;
|
|
179
|
+
}
|
package/dist/features/openservice/serviceTickets/alarmActions/service/ensureAlarmTicketAction.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Make "create a ticket" selectable in the alarm dialog.
|
|
3
|
+
*
|
|
4
|
+
* Alarm actions are **data**, not code: `OD3_AlarmAction` rows that the
|
|
5
|
+
* monitoring adapter loads into the frontend, where the chosen row's
|
|
6
|
+
* `formFields` are rendered as the action's settings form (see
|
|
7
|
+
* `plugins/monitoring/src/components/AlarmCreate.tsx`). There is no plugin-side
|
|
8
|
+
* `registerAlarmAction`, so the row is seeded here — find-or-create by `topic`,
|
|
9
|
+
* like the other open.SERVICE bootstraps.
|
|
10
|
+
*
|
|
11
|
+
* The row is **global** (no tenant): the action makes sense for every tenant,
|
|
12
|
+
* and the tenant of a fired alarm is established by its secret, not by this
|
|
13
|
+
* row.
|
|
14
|
+
*
|
|
15
|
+
* ⚠️ `type` / `target` / `payload` are the executor's half of the contract —
|
|
16
|
+
* the component that evaluates alarms lives outside this server. The values
|
|
17
|
+
* below assume a webhook-style executor that POSTs `payload` (placeholders
|
|
18
|
+
* filled from the alarm) to `target`. If the executor expects something else,
|
|
19
|
+
* this row is where it is corrected; the receiving endpoint accepts any caller
|
|
20
|
+
* that brings the secret.
|
|
21
|
+
*/
|
|
22
|
+
export declare function ensureAlarmTicketAction(): Promise<void>;
|
package/dist/features/openservice/serviceTickets/alarmActions/service/ensureAlarmTicketAction.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ensureAlarmTicketAction = ensureAlarmTicketAction;
|
|
7
|
+
const node_1 = __importDefault(require("parse/node"));
|
|
8
|
+
const index_js_1 = require("../../../../../types/index.js");
|
|
9
|
+
const AlarmTicketTypes_js_1 = require("../types/AlarmTicketTypes.js");
|
|
10
|
+
/**
|
|
11
|
+
* Make "create a ticket" selectable in the alarm dialog.
|
|
12
|
+
*
|
|
13
|
+
* Alarm actions are **data**, not code: `OD3_AlarmAction` rows that the
|
|
14
|
+
* monitoring adapter loads into the frontend, where the chosen row's
|
|
15
|
+
* `formFields` are rendered as the action's settings form (see
|
|
16
|
+
* `plugins/monitoring/src/components/AlarmCreate.tsx`). There is no plugin-side
|
|
17
|
+
* `registerAlarmAction`, so the row is seeded here — find-or-create by `topic`,
|
|
18
|
+
* like the other open.SERVICE bootstraps.
|
|
19
|
+
*
|
|
20
|
+
* The row is **global** (no tenant): the action makes sense for every tenant,
|
|
21
|
+
* and the tenant of a fired alarm is established by its secret, not by this
|
|
22
|
+
* row.
|
|
23
|
+
*
|
|
24
|
+
* ⚠️ `type` / `target` / `payload` are the executor's half of the contract —
|
|
25
|
+
* the component that evaluates alarms lives outside this server. The values
|
|
26
|
+
* below assume a webhook-style executor that POSTs `payload` (placeholders
|
|
27
|
+
* filled from the alarm) to `target`. If the executor expects something else,
|
|
28
|
+
* this row is where it is corrected; the receiving endpoint accepts any caller
|
|
29
|
+
* that brings the secret.
|
|
30
|
+
*/
|
|
31
|
+
async function ensureAlarmTicketAction() {
|
|
32
|
+
const existing = await new node_1.default.Query(index_js_1.AlarmAction)
|
|
33
|
+
.equalTo("topic", AlarmTicketTypes_js_1.ALARM_TICKET_ACTION_ID)
|
|
34
|
+
.first({ useMasterKey: true });
|
|
35
|
+
if (existing)
|
|
36
|
+
return;
|
|
37
|
+
const action = new index_js_1.AlarmAction();
|
|
38
|
+
action.set("label", "Ticket erstellen (open.SERVICE)");
|
|
39
|
+
action.set("type", "webhook");
|
|
40
|
+
action.set("topic", AlarmTicketTypes_js_1.ALARM_TICKET_ACTION_ID);
|
|
41
|
+
action.set("target", "/parse/functions/openinc-openservice-alarm-create-ticket");
|
|
42
|
+
action.set("templateType", "json");
|
|
43
|
+
action.set("payload", JSON.stringify({
|
|
44
|
+
secret: "{{secret}}",
|
|
45
|
+
event: {
|
|
46
|
+
alarmId: "{{alarm.id}}",
|
|
47
|
+
name: "{{alarm.name}}",
|
|
48
|
+
item_id: "{{item.id}}",
|
|
49
|
+
item_source: "{{item.source}}",
|
|
50
|
+
item_dimension: "{{item.dimension}}",
|
|
51
|
+
value: "{{value}}",
|
|
52
|
+
condition: "{{condition}}",
|
|
53
|
+
firedAt: "{{timestamp}}",
|
|
54
|
+
},
|
|
55
|
+
params: "{{action}}",
|
|
56
|
+
}));
|
|
57
|
+
// Every value type can breach a rule, so every type may raise a ticket.
|
|
58
|
+
action.set("supportedTypes", [
|
|
59
|
+
"Number",
|
|
60
|
+
"String",
|
|
61
|
+
"Boolean",
|
|
62
|
+
"Geo",
|
|
63
|
+
"Object",
|
|
64
|
+
]);
|
|
65
|
+
action.set("formFields", FORM_FIELDS);
|
|
66
|
+
await action.save(null, { useMasterKey: true });
|
|
67
|
+
console.log(`[AlarmTicket] Registered alarm action "${AlarmTicketTypes_js_1.ALARM_TICKET_ACTION_ID}"`);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* What the person configuring an alarm gets to fill in.
|
|
71
|
+
*
|
|
72
|
+
* Field types are the ones `FormGenerator` (in `@opendash/core`) understands;
|
|
73
|
+
* `select.parse` renders an object picker for a Parse class. Keys match
|
|
74
|
+
* `AlarmTicketParams` — the executor hands the form's values back to us as
|
|
75
|
+
* `params`.
|
|
76
|
+
*
|
|
77
|
+
* The machine is a plain picker rather than something derived from the sensor,
|
|
78
|
+
* because nothing in the data model links the two. Whoever sets the alarm up
|
|
79
|
+
* knows which machine it watches; guessing it later is worse than asking now.
|
|
80
|
+
*/
|
|
81
|
+
const FORM_FIELDS = [
|
|
82
|
+
{
|
|
83
|
+
key: "title",
|
|
84
|
+
label: "Titel des Tickets",
|
|
85
|
+
type: "input",
|
|
86
|
+
hint: "Platzhalter: {{alarm}}, {{sensor}}, {{value}}. Leer = Name des Alarms.",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
key: "description",
|
|
90
|
+
label: "Notiz",
|
|
91
|
+
type: "textarea",
|
|
92
|
+
hint: "Wird dem Ticket vorangestellt; Sensor, Wert, Regel und Zeitpunkt kommen automatisch dazu.",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
key: "equipmentId",
|
|
96
|
+
label: "Maschine",
|
|
97
|
+
type: "select.parse",
|
|
98
|
+
settings: {
|
|
99
|
+
className: "OD3_Service_Equipment",
|
|
100
|
+
displayField: "label",
|
|
101
|
+
mode: "single",
|
|
102
|
+
},
|
|
103
|
+
hint: "Ohne Angabe wird über die Quellen-Tags der Maschine geraten — oder das Ticket bleibt ohne Maschine.",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
key: "projectId",
|
|
107
|
+
label: "Projekt",
|
|
108
|
+
type: "select.parse",
|
|
109
|
+
settings: {
|
|
110
|
+
className: "OD3_Service_Project",
|
|
111
|
+
displayField: "label",
|
|
112
|
+
mode: "single",
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
key: "priority",
|
|
117
|
+
label: "Priorität",
|
|
118
|
+
type: "input.number",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
key: "duplicate",
|
|
122
|
+
label: "Bei jedem Auslösen ein neues Ticket",
|
|
123
|
+
type: "switch",
|
|
124
|
+
defaultValue: false,
|
|
125
|
+
hint: "Aus (Standard): solange das Ticket offen ist, entsteht kein zweites.",
|
|
126
|
+
},
|
|
127
|
+
];
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The alarm action "create a ticket".
|
|
3
|
+
*
|
|
4
|
+
* An open.DASH alarm watches a sensor value against a rule; when the rule
|
|
5
|
+
* breaks, its configured **action** runs. The actions are rows in
|
|
6
|
+
* `OD3_AlarmAction` (loaded into the frontend by the monitoring adapter), and
|
|
7
|
+
* the executor — the component that evaluates alarms, outside this server —
|
|
8
|
+
* carries them out. This module is the receiving end: it turns one fired alarm
|
|
9
|
+
* into an `OD3_Service_Ticket`.
|
|
10
|
+
*/
|
|
11
|
+
/** Id of the action row, and the marker in a ticket's `origin`. */
|
|
12
|
+
export declare const ALARM_TICKET_ACTION_ID = "openservice-create-ticket";
|
|
13
|
+
/**
|
|
14
|
+
* Config key of the shared secret an executor authenticates with.
|
|
15
|
+
*
|
|
16
|
+
* One row per tenant: `key` = this, `value` = the secret, `tenant` = the
|
|
17
|
+
* tenant's pointer. The secret is what tells us **which tenant** a call belongs
|
|
18
|
+
* to — an alarm executor is not trusted to name it. Without a row, the endpoint
|
|
19
|
+
* only answers to the master key.
|
|
20
|
+
*/
|
|
21
|
+
export declare const ALARM_TICKET_SECRET_CONFIG_KEY = "OPENSERVICE_ALARM_TICKET_SECRET";
|
|
22
|
+
/** Status category a fresh ticket lands in — mirrors the ticket plugin's port. */
|
|
23
|
+
export declare const DEFAULT_STATUS_CATEGORY = "inbox";
|
|
24
|
+
/** Categories in which a ticket counts as finished (mirrors core's `DONE_CATEGORIES`). */
|
|
25
|
+
export declare const DONE_STATUS_CATEGORIES: string[];
|
|
26
|
+
/**
|
|
27
|
+
* What the executor reports about the alarm that fired.
|
|
28
|
+
*
|
|
29
|
+
* Field names follow `OD3_Alarm` so a payload can be assembled from the alarm
|
|
30
|
+
* row without renaming: `item_id` / `item_source` / `item_dimension` identify
|
|
31
|
+
* the sensor, the rest is context for the ticket text. Everything is optional
|
|
32
|
+
* except that *something* must identify the alarm — see `alarmKey`.
|
|
33
|
+
*/
|
|
34
|
+
export interface AlarmEvent {
|
|
35
|
+
/** `OD3_Alarm.objectId`, if the executor knows it. */
|
|
36
|
+
alarmId?: string;
|
|
37
|
+
/** The alarm's name, used in the ticket title when no template is given. */
|
|
38
|
+
name?: string;
|
|
39
|
+
/** Sensor id, e.g. `cnc04.spindle`. */
|
|
40
|
+
item_id?: string;
|
|
41
|
+
/** Source (site/tenant hierarchy) the sensor belongs to. */
|
|
42
|
+
item_source?: string;
|
|
43
|
+
/** Which dimension of the sensor tripped. */
|
|
44
|
+
item_dimension?: number;
|
|
45
|
+
/** The value that broke the rule. */
|
|
46
|
+
value?: number | string | boolean;
|
|
47
|
+
/** Human-readable rule, e.g. `> 80`. */
|
|
48
|
+
condition?: string;
|
|
49
|
+
/** When it fired (ISO). Defaults to now. */
|
|
50
|
+
firedAt?: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* What the person configuring the alarm filled into the action's form
|
|
54
|
+
* (`OD3_AlarmAction.formFields`).
|
|
55
|
+
*/
|
|
56
|
+
export interface AlarmTicketParams {
|
|
57
|
+
/** Ticket title; `{{alarm}}`, `{{sensor}}` and `{{value}}` are replaced. */
|
|
58
|
+
title?: string;
|
|
59
|
+
/** Prepended to the generated alarm details. */
|
|
60
|
+
description?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Machine the ticket belongs to.
|
|
63
|
+
*
|
|
64
|
+
* Set explicitly when configuring the alarm — that is the reliable way,
|
|
65
|
+
* because nothing in the data model links a sensor to a machine. Without it,
|
|
66
|
+
* `Equipment.sourceTags` is tried (see `resolveEquipment`).
|
|
67
|
+
*/
|
|
68
|
+
equipmentId?: string;
|
|
69
|
+
projectId?: string;
|
|
70
|
+
priority?: number;
|
|
71
|
+
assignedUserIds?: string[];
|
|
72
|
+
assignedRoleIds?: string[];
|
|
73
|
+
tagIds?: string[];
|
|
74
|
+
/**
|
|
75
|
+
* Reopen behaviour. `false` (default) keeps **one open ticket per alarm**: a
|
|
76
|
+
* flapping sensor does not produce a hundred tickets, and closing the ticket
|
|
77
|
+
* arms the alarm again. `true` creates one per firing.
|
|
78
|
+
*/
|
|
79
|
+
duplicate?: boolean;
|
|
80
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The alarm action "create a ticket".
|
|
4
|
+
*
|
|
5
|
+
* An open.DASH alarm watches a sensor value against a rule; when the rule
|
|
6
|
+
* breaks, its configured **action** runs. The actions are rows in
|
|
7
|
+
* `OD3_AlarmAction` (loaded into the frontend by the monitoring adapter), and
|
|
8
|
+
* the executor — the component that evaluates alarms, outside this server —
|
|
9
|
+
* carries them out. This module is the receiving end: it turns one fired alarm
|
|
10
|
+
* into an `OD3_Service_Ticket`.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.DONE_STATUS_CATEGORIES = exports.DEFAULT_STATUS_CATEGORY = exports.ALARM_TICKET_SECRET_CONFIG_KEY = exports.ALARM_TICKET_ACTION_ID = void 0;
|
|
14
|
+
/** Id of the action row, and the marker in a ticket's `origin`. */
|
|
15
|
+
exports.ALARM_TICKET_ACTION_ID = "openservice-create-ticket";
|
|
16
|
+
/**
|
|
17
|
+
* Config key of the shared secret an executor authenticates with.
|
|
18
|
+
*
|
|
19
|
+
* One row per tenant: `key` = this, `value` = the secret, `tenant` = the
|
|
20
|
+
* tenant's pointer. The secret is what tells us **which tenant** a call belongs
|
|
21
|
+
* to — an alarm executor is not trusted to name it. Without a row, the endpoint
|
|
22
|
+
* only answers to the master key.
|
|
23
|
+
*/
|
|
24
|
+
exports.ALARM_TICKET_SECRET_CONFIG_KEY = "OPENSERVICE_ALARM_TICKET_SECRET";
|
|
25
|
+
/** Status category a fresh ticket lands in — mirrors the ticket plugin's port. */
|
|
26
|
+
exports.DEFAULT_STATUS_CATEGORY = "inbox";
|
|
27
|
+
/** Categories in which a ticket counts as finished (mirrors core's `DONE_CATEGORIES`). */
|
|
28
|
+
exports.DONE_STATUS_CATEGORIES = ["done", "cancelled"];
|
|
@@ -24,9 +24,8 @@ export declare namespace Permissions {
|
|
|
24
24
|
ticket_edit = "service:can-edit-ticket",
|
|
25
25
|
ticket_delete = "service:can-delete-ticket",
|
|
26
26
|
ticket_assign = "service:can-assign-ticket",
|
|
27
|
-
|
|
27
|
+
schedule_access = "service:can-access-schedule-plugin",
|
|
28
28
|
schedule_create = "service:can-create-schedule",
|
|
29
|
-
schedule_edit = "service:can-edit-schedule",
|
|
30
29
|
schedule_delete = "service:can-delete-schedule",
|
|
31
30
|
execution_perform = "service:can-perform-execution",
|
|
32
31
|
template_manage = "service:can-manage-template",
|
|
@@ -37,9 +37,15 @@ var Permissions;
|
|
|
37
37
|
SERVICE["ticket_delete"] = "service:can-delete-ticket";
|
|
38
38
|
SERVICE["ticket_assign"] = "service:can-assign-ticket";
|
|
39
39
|
// Schedules (Wartungspläne)
|
|
40
|
-
|
|
40
|
+
//
|
|
41
|
+
// No `read` or `edit` here on purpose: both are decided per record by the
|
|
42
|
+
// ACL — creator, assigned roles and users — and a capability that says
|
|
43
|
+
// "may edit" while the ACL says "not this one" only adds a second answer to
|
|
44
|
+
// a question that already has one. `create` stays a capability because
|
|
45
|
+
// there is no record yet whose ACL could be asked, and a class-level
|
|
46
|
+
// permission cannot express it per tenant.
|
|
47
|
+
SERVICE["schedule_access"] = "service:can-access-schedule-plugin";
|
|
41
48
|
SERVICE["schedule_create"] = "service:can-create-schedule";
|
|
42
|
-
SERVICE["schedule_edit"] = "service:can-edit-schedule";
|
|
43
49
|
SERVICE["schedule_delete"] = "service:can-delete-schedule";
|
|
44
50
|
// Executions (Durchführungen)
|
|
45
51
|
SERVICE["execution_perform"] = "service:can-perform-execution";
|