iobroker.time-tracker 0.7.13 → 0.7.15
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/README.md +48 -16
- package/build/lib/services/firstAdministrator.js +84 -0
- package/build/lib/services/firstAdministrator.js.map +7 -0
- package/build/main.js +42 -35
- package/build/main.js.map +2 -2
- package/io-package.json +46 -27
- package/package.json +1 -1
- package/www/sw.js.map +1 -1
package/README.md
CHANGED
|
@@ -41,9 +41,21 @@ PIN. All data stays on your own ioBroker host: no cloud, no subscription.
|
|
|
41
41
|
**The instance `time-tracker.0` is created automatically** — `iobroker add time-tracker` is only needed if you want
|
|
42
42
|
the instance on its own.
|
|
43
43
|
2. Start the instance and open the web app on the port of the instance settings (default **8092**):
|
|
44
|
-
`http://<ioBroker host>:8092
|
|
44
|
+
`http://<ioBroker host>:8092/`. The instance tile in the admin carries the same way in as a symbol (see
|
|
45
|
+
[The link in the instance list](#the-link-in-the-instance-list)), so host and port do not have to be typed.
|
|
45
46
|
3. Log in with the start password (see [First start](#first-start)) and create your employees.
|
|
46
47
|
|
|
48
|
+
### The link in the instance list
|
|
49
|
+
|
|
50
|
+
The adapter reports its own way into the web interface (`common.localLinks`), and the admin renders a symbol out of it:
|
|
51
|
+
the tile of `time-tracker.0` in **Instances** offers *Open the time tracking app*, the overview page does the same — one
|
|
52
|
+
click and the login screen shows up, without typing host and port.
|
|
53
|
+
|
|
54
|
+
The address is **not** written into the adapter; the admin puts it together from the instance settings — the bind
|
|
55
|
+
address gives the host, the port the port. With the default `127.0.0.1` the link leads to the ioBroker machine itself
|
|
56
|
+
(that is where the app answers), with `0.0.0.0` it takes the address you reached the admin with, so phone and PC use the
|
|
57
|
+
same link. A reverse proxy rule for `time-tracker.0` replaces the link with its own path.
|
|
58
|
+
|
|
47
59
|
## First start
|
|
48
60
|
|
|
49
61
|
The first start creates the database, the roles and the settings — and **one administrator account**, because
|
|
@@ -61,6 +73,22 @@ The start password appears **once in the ioBroker log**: open `Logs` in the admi
|
|
|
61
73
|
`administrator "admin" created with the start password "…" - change it at the first login`. The instance settings
|
|
62
74
|
show the same hint — enter your own password there and nothing has to be searched.
|
|
63
75
|
|
|
76
|
+
**Entering the password later works too.** As long as the account still carries its unchanged start password (the
|
|
77
|
+
forced password change was never completed), the adapter applies the configured start password on **every** start. A
|
|
78
|
+
value filled in after the first start — or after restoring a database — therefore works right away. Every start says
|
|
79
|
+
in the log which of these four cases it is:
|
|
80
|
+
|
|
81
|
+
- `administrator "admin" created with the start password "Zf-…" - change it at the first login` — no administrator
|
|
82
|
+
existed and the field was empty; this is the generated password.
|
|
83
|
+
- `administrator "admin" created with the configured start password - it has to be changed at the first login` — no
|
|
84
|
+
administrator existed, the value of the instance settings was used.
|
|
85
|
+
- `administrator "admin" exists already and still has its start password - it was set from the instance settings now`
|
|
86
|
+
— the account existed, the configured start password took over (the entry in the audit trail is
|
|
87
|
+
`start password from the instance settings`).
|
|
88
|
+
- `the configured start password is not used: administrator "admin" changed his password already` — the account
|
|
89
|
+
changed its password in the app, so the setting has no effect any more: clear the field (the password belongs to
|
|
90
|
+
the profile page of that account) or restore a database that still carries the start password.
|
|
91
|
+
|
|
64
92
|
After that the usual order is: create employees (**Administration → Employees**), set their working time
|
|
65
93
|
(**Arbeitsprofil** button of the row: employment level, weekly hours, working days, overtime model, vacation,
|
|
66
94
|
carryover, paid break minutes, own break rules), and — if a tablet is used — create a terminal and switch the kiosk on
|
|
@@ -429,6 +457,25 @@ local SQLite file, access is role-based, and every correction is written to an a
|
|
|
429
457
|
|
|
430
458
|
### **WORK IN PROGRESS**
|
|
431
459
|
|
|
460
|
+
### 0.7.15 (2026-10-01)
|
|
461
|
+
|
|
462
|
+
- (Alex) new: the instance tile in the admin carries a **symbol that opens the app** now. The adapter announces the way
|
|
463
|
+
into its own web interface as `common.localLinks`, so the admin shows *Open the time tracking app* next to the
|
|
464
|
+
instance — one click and the login screen shows up, without typing host and port. The link uses the bind address and
|
|
465
|
+
the port of the instance settings; with the default `127.0.0.1` it leads to the ioBroker machine itself, with
|
|
466
|
+
`0.0.0.0` to the address the browser reached the admin with.
|
|
467
|
+
|
|
468
|
+
### 0.7.14 (2026-10-01)
|
|
469
|
+
|
|
470
|
+
- (Alex) fix: a start password that is entered **after** the first start opens the installation now. The first
|
|
471
|
+
administrator was only ever created once, so a later value of the instance setting was ignored in silence — no log
|
|
472
|
+
line and no way in. As long as the account still carries its unchanged start password, the adapter applies the
|
|
473
|
+
configured value on every start, names the account in the log and notes it in the audit trail; once the password
|
|
474
|
+
was changed in the app the setting stays out of play and the log says so. That is exactly the “I set a password and
|
|
475
|
+
nothing appears in the log” case from the field report. The decision sits in
|
|
476
|
+
`src/lib/services/firstAdministrator.ts` next to its unit test; `README.md` (“First start”), `docs/erste-schritte.md`,
|
|
477
|
+
`docs/testplan.md` and D15 of `docs/entscheidungen.md` describe the four log lines
|
|
478
|
+
|
|
432
479
|
### 0.7.13 (2026-09-27)
|
|
433
480
|
|
|
434
481
|
- (Alex) docs: the file server of a `web` instance is a documented way to the calendar file again — under the new mount
|
|
@@ -452,21 +499,6 @@ local SQLite file, access is role-based, and every correction is written to an a
|
|
|
452
499
|
`files/time-tracker.<n>/` is known to the file manager and exists from the first start, because a small `INFO.txt`
|
|
453
500
|
keeps it in place; the state `calendar.feedFile` still carries the path for the `ical` adapter
|
|
454
501
|
|
|
455
|
-
### 0.7.10 (2026-09-26)
|
|
456
|
-
|
|
457
|
-
- (Alex) change: the state `calendar.feedFile` is called “Path of the calendar file” now — in all eleven languages.
|
|
458
|
-
The old name said “calendar file” and let the value look like a download, while it is the **path** that the `ical`
|
|
459
|
-
adapter reads; browsers and calendar apps use the link `calendar.feedUrl`
|
|
460
|
-
|
|
461
|
-
### 0.7.9 (2026-09-26)
|
|
462
|
-
|
|
463
|
-
- (Alex) fix: “Feiertag hinzufügen” stayed grey while the day had to be typed as text — it comes from the date field of
|
|
464
|
-
the browser now, and the label no longer names a format. A holiday entered without a region belongs to the country of
|
|
465
|
-
the instance instead of always `DE`, the list shows the days of **every** region of the year, and a day of the next
|
|
466
|
-
year takes the shown year along
|
|
467
|
-
- (Alex) fix: “Abbrechen” closes the form of an absence while it is being changed as well — it cleared only the flag of
|
|
468
|
-
the new absence and left the dialog of the change on the screen
|
|
469
|
-
|
|
470
502
|
Older entries are kept in [`CHANGELOG_OLD.md`](CHANGELOG_OLD.md).
|
|
471
503
|
|
|
472
504
|
## Provenance
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var firstAdministrator_exports = {};
|
|
20
|
+
__export(firstAdministrator_exports, {
|
|
21
|
+
DEFAULT_ADMIN_LOGIN: () => DEFAULT_ADMIN_LOGIN,
|
|
22
|
+
DEFAULT_TIMEZONE: () => DEFAULT_TIMEZONE,
|
|
23
|
+
ensureFirstAdministrator: () => ensureFirstAdministrator,
|
|
24
|
+
generateStartPassword: () => generateStartPassword
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(firstAdministrator_exports);
|
|
27
|
+
var import_node_crypto = require("node:crypto");
|
|
28
|
+
var import_auth = require("./auth");
|
|
29
|
+
const ADMIN_ROLE = "admin";
|
|
30
|
+
const DEFAULT_ADMIN_LOGIN = "admin";
|
|
31
|
+
const DEFAULT_TIMEZONE = "Europe/Berlin";
|
|
32
|
+
const RESET_REASON = "start password from the instance settings";
|
|
33
|
+
function generateStartPassword() {
|
|
34
|
+
return `Zf-${(0, import_node_crypto.randomBytes)(9).toString("base64url")}-7`;
|
|
35
|
+
}
|
|
36
|
+
function ensureFirstAdministrator(input) {
|
|
37
|
+
var _a, _b, _c, _d;
|
|
38
|
+
const login = ((_a = input.login) != null ? _a : "").trim() || DEFAULT_ADMIN_LOGIN;
|
|
39
|
+
const configured = ((_b = input.password) != null ? _b : "").trim();
|
|
40
|
+
const administrators = input.users.list().filter((user) => input.users.roles(user.id).includes(ADMIN_ROLE));
|
|
41
|
+
if (administrators.length > 0) {
|
|
42
|
+
const target = (_c = administrators.find((user) => user.login.toLowerCase() === login.toLowerCase())) != null ? _c : [...administrators].sort((left, right) => left.id - right.id)[0];
|
|
43
|
+
if (!configured) {
|
|
44
|
+
return { action: "existing", login: target.login };
|
|
45
|
+
}
|
|
46
|
+
if (!target.mustChangePw) {
|
|
47
|
+
return { action: "password_changed", login: target.login };
|
|
48
|
+
}
|
|
49
|
+
if ((0, import_auth.verifyPassword)(configured, target.passwordHash)) {
|
|
50
|
+
return { action: "start_password_unchanged", login: target.login };
|
|
51
|
+
}
|
|
52
|
+
input.users.update({
|
|
53
|
+
id: target.id,
|
|
54
|
+
patch: { passwordHash: (0, import_auth.hashPassword)(configured), mustChangePw: true },
|
|
55
|
+
reason: RESET_REASON,
|
|
56
|
+
actorId: target.id,
|
|
57
|
+
now: input.now
|
|
58
|
+
});
|
|
59
|
+
return { action: "start_password_reset", login: target.login };
|
|
60
|
+
}
|
|
61
|
+
const password = configured || generateStartPassword();
|
|
62
|
+
try {
|
|
63
|
+
const user = input.users.create({
|
|
64
|
+
login,
|
|
65
|
+
displayName: login,
|
|
66
|
+
passwordHash: (0, import_auth.hashPassword)(password),
|
|
67
|
+
mustChangePw: true,
|
|
68
|
+
timezone: ((_d = input.timezone) != null ? _d : "").trim() || DEFAULT_TIMEZONE,
|
|
69
|
+
roleKeys: [ADMIN_ROLE],
|
|
70
|
+
now: input.now
|
|
71
|
+
});
|
|
72
|
+
return { action: "created", login: user.login, password, generated: !configured };
|
|
73
|
+
} catch (error) {
|
|
74
|
+
return { action: "failed", login, reason: error instanceof Error ? error.message : String(error) };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
78
|
+
0 && (module.exports = {
|
|
79
|
+
DEFAULT_ADMIN_LOGIN,
|
|
80
|
+
DEFAULT_TIMEZONE,
|
|
81
|
+
ensureFirstAdministrator,
|
|
82
|
+
generateStartPassword
|
|
83
|
+
});
|
|
84
|
+
//# sourceMappingURL=firstAdministrator.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/services/firstAdministrator.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * First administrator of the instance.\n *\n * Without an account nobody can log in, so the first start creates one: the login and the start password come from\n * the instance settings, and without a configured password a random one is generated (it is written to the adapter\n * log, because there is no other way to hand it to the operator).\n *\n * The account always carries \u201Cchange the password\u201D, so the start password opens the door exactly once. As long as\n * the administrator still has that unchanged start password, the configured value is applied again on every start \u2014\n * an operator who sets the field *after* the first start (or restores a database) therefore reaches the\n * installation. That override is deliberately limited to the unchange start password: once the password was changed\n * in the web app, a forgotten field in the instance settings can never reset it, and the caller says in the log why\n * the configured value is out of play.\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport type { UsersRepository } from \"../db/repositories/users\";\nimport { hashPassword, verifyPassword } from \"./auth\";\n\n/** Role key that marks an administrator. */\nconst ADMIN_ROLE = \"admin\";\n\n/** Login used when the instance settings leave the field empty. */\nexport const DEFAULT_ADMIN_LOGIN = \"admin\";\n\n/** Time zone used when the instance settings leave the field empty (the web app falls back to it as well). */\nexport const DEFAULT_TIMEZONE = \"Europe/Berlin\";\n\n/** Reason written to the audit trail when the start password is taken from the instance settings. */\nconst RESET_REASON = \"start password from the instance settings\";\n\n/**\n * Generates a start password.\n *\n * @returns random password that satisfies the password policy (letters, digits and at least ten characters)\n */\nexport function generateStartPassword(): string {\n\treturn `Zf-${randomBytes(9).toString(\"base64url\")}-7`;\n}\n\n/** What {@link ensureFirstAdministrator} did. */\nexport type FirstAdministratorOutcome =\n\t/** No administrator existed, the account was created (`password` is the start password, `generated` says why) */\n\t| { action: \"created\"; login: string; password: string; generated: boolean }\n\t/** The unchange start password of the existing administrator was set to the configured value */\n\t| { action: \"start_password_reset\"; login: string }\n\t/** The existing administrator carries the configured start password already (nothing was written) */\n\t| { action: \"start_password_unchanged\"; login: string }\n\t/** The administrator changed his password, so the configured start password is not used */\n\t| { action: \"password_changed\"; login: string }\n\t/** An administrator exists and the instance settings carry no start password */\n\t| { action: \"existing\"; login: string }\n\t/** The account could not be created (a taken login, a broken database) */\n\t| { action: \"failed\"; login: string; reason: string };\n\n/**\n * Creates the first administrator when the instance has none, and keeps the configured start password working while\n * that account still carries its unchange start password.\n *\n * @param input - user storage and the instance settings\n * @param input.users - user repository of the open database\n * @param input.login - configured login of the first administrator (empty = {@link DEFAULT_ADMIN_LOGIN})\n * @param input.password - configured start password (empty = a random one is generated)\n * @param input.timezone - time zone of the new account (empty = {@link DEFAULT_TIMEZONE})\n * @param input.now - instant of the change, defaults to now\n * @returns what happened, so the caller can log it\n */\nexport function ensureFirstAdministrator(input: {\n\tusers: UsersRepository;\n\tlogin?: string | null;\n\tpassword?: string | null;\n\ttimezone?: string | null;\n\tnow?: number;\n}): FirstAdministratorOutcome {\n\tconst login = (input.login ?? \"\").trim() || DEFAULT_ADMIN_LOGIN;\n\tconst configured = (input.password ?? \"\").trim();\n\tconst administrators = input.users.list().filter(user => input.users.roles(user.id).includes(ADMIN_ROLE));\n\n\tif (administrators.length > 0) {\n\t\t// Several administrators are possible: the configured login wins, otherwise the oldest account is the one\n\t\t// the instance settings describe (\u201Cthe first administrator\u201D).\n\t\tconst target =\n\t\t\tadministrators.find(user => user.login.toLowerCase() === login.toLowerCase()) ??\n\t\t\t[...administrators].sort((left, right) => left.id - right.id)[0];\n\n\t\tif (!configured) {\n\t\t\treturn { action: \"existing\", login: target.login };\n\t\t}\n\t\tif (!target.mustChangePw) {\n\t\t\treturn { action: \"password_changed\", login: target.login };\n\t\t}\n\t\tif (verifyPassword(configured, target.passwordHash)) {\n\t\t\treturn { action: \"start_password_unchanged\", login: target.login };\n\t\t}\n\n\t\tinput.users.update({\n\t\t\tid: target.id,\n\t\t\tpatch: { passwordHash: hashPassword(configured), mustChangePw: true },\n\t\t\treason: RESET_REASON,\n\t\t\tactorId: target.id,\n\t\t\tnow: input.now,\n\t\t});\n\t\treturn { action: \"start_password_reset\", login: target.login };\n\t}\n\n\tconst password = configured || generateStartPassword();\n\ttry {\n\t\tconst user = input.users.create({\n\t\t\tlogin,\n\t\t\tdisplayName: login,\n\t\t\tpasswordHash: hashPassword(password),\n\t\t\tmustChangePw: true,\n\t\t\ttimezone: (input.timezone ?? \"\").trim() || DEFAULT_TIMEZONE,\n\t\t\troleKeys: [ADMIN_ROLE],\n\t\t\tnow: input.now,\n\t\t});\n\t\treturn { action: \"created\", login: user.login, password, generated: !configured };\n\t} catch (error) {\n\t\treturn { action: \"failed\", login, reason: error instanceof Error ? error.message : String(error) };\n\t}\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAeA,yBAA4B;AAE5B,kBAA6C;AAG7C,MAAM,aAAa;AAGZ,MAAM,sBAAsB;AAG5B,MAAM,mBAAmB;AAGhC,MAAM,eAAe;AAOd,SAAS,wBAAgC;AAC/C,SAAO,UAAM,gCAAY,CAAC,EAAE,SAAS,WAAW,CAAC;AAClD;AA6BO,SAAS,yBAAyB,OAMX;AAzE9B;AA0EC,QAAM,UAAS,WAAM,UAAN,YAAe,IAAI,KAAK,KAAK;AAC5C,QAAM,eAAc,WAAM,aAAN,YAAkB,IAAI,KAAK;AAC/C,QAAM,iBAAiB,MAAM,MAAM,KAAK,EAAE,OAAO,UAAQ,MAAM,MAAM,MAAM,KAAK,EAAE,EAAE,SAAS,UAAU,CAAC;AAExG,MAAI,eAAe,SAAS,GAAG;AAG9B,UAAM,UACL,oBAAe,KAAK,UAAQ,KAAK,MAAM,YAAY,MAAM,MAAM,YAAY,CAAC,MAA5E,YACA,CAAC,GAAG,cAAc,EAAE,KAAK,CAAC,MAAM,UAAU,KAAK,KAAK,MAAM,EAAE,EAAE,CAAC;AAEhE,QAAI,CAAC,YAAY;AAChB,aAAO,EAAE,QAAQ,YAAY,OAAO,OAAO,MAAM;AAAA,IAClD;AACA,QAAI,CAAC,OAAO,cAAc;AACzB,aAAO,EAAE,QAAQ,oBAAoB,OAAO,OAAO,MAAM;AAAA,IAC1D;AACA,YAAI,4BAAe,YAAY,OAAO,YAAY,GAAG;AACpD,aAAO,EAAE,QAAQ,4BAA4B,OAAO,OAAO,MAAM;AAAA,IAClE;AAEA,UAAM,MAAM,OAAO;AAAA,MAClB,IAAI,OAAO;AAAA,MACX,OAAO,EAAE,kBAAc,0BAAa,UAAU,GAAG,cAAc,KAAK;AAAA,MACpE,QAAQ;AAAA,MACR,SAAS,OAAO;AAAA,MAChB,KAAK,MAAM;AAAA,IACZ,CAAC;AACD,WAAO,EAAE,QAAQ,wBAAwB,OAAO,OAAO,MAAM;AAAA,EAC9D;AAEA,QAAM,WAAW,cAAc,sBAAsB;AACrD,MAAI;AACH,UAAM,OAAO,MAAM,MAAM,OAAO;AAAA,MAC/B;AAAA,MACA,aAAa;AAAA,MACb,kBAAc,0BAAa,QAAQ;AAAA,MACnC,cAAc;AAAA,MACd,YAAW,WAAM,aAAN,YAAkB,IAAI,KAAK,KAAK;AAAA,MAC3C,UAAU,CAAC,UAAU;AAAA,MACrB,KAAK,MAAM;AAAA,IACZ,CAAC;AACD,WAAO,EAAE,QAAQ,WAAW,OAAO,KAAK,OAAO,UAAU,WAAW,CAAC,WAAW;AAAA,EACjF,SAAS,OAAO;AACf,WAAO,EAAE,QAAQ,UAAU,OAAO,QAAQ,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE;AAAA,EAClG;AACD;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/build/main.js
CHANGED
|
@@ -21,7 +21,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
21
21
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
22
|
mod
|
|
23
23
|
));
|
|
24
|
-
var import_node_crypto = require("node:crypto");
|
|
25
24
|
var fs = __toESM(require("node:fs"));
|
|
26
25
|
var path = __toESM(require("node:path"));
|
|
27
26
|
var import_node_os = require("node:os");
|
|
@@ -42,6 +41,7 @@ var import_rfid = require("./lib/db/repositories/rfid");
|
|
|
42
41
|
var import_triggers = require("./lib/db/repositories/triggers");
|
|
43
42
|
var import_aggregation = require("./lib/services/aggregation");
|
|
44
43
|
var import_auth = require("./lib/services/auth");
|
|
44
|
+
var import_firstAdministrator = require("./lib/services/firstAdministrator");
|
|
45
45
|
var import_sessionSecret = require("./lib/services/sessionSecret");
|
|
46
46
|
var import_closing = require("./lib/services/closing");
|
|
47
47
|
var import_backup = require("./lib/services/backup");
|
|
@@ -195,44 +195,51 @@ class TimeTracker extends utils.Adapter {
|
|
|
195
195
|
/**
|
|
196
196
|
* Creates the first administrator when the instance has none.
|
|
197
197
|
*
|
|
198
|
-
* The
|
|
199
|
-
*
|
|
200
|
-
*
|
|
198
|
+
* The decision itself sits in `services/firstAdministrator.ts` (it is unit tested there); this method only
|
|
199
|
+
* reports the result. Every start says what happened, so “I set a start password and nothing appears in the
|
|
200
|
+
* log” cannot happen again — see `docs/entscheidungen.md`, D15.
|
|
201
201
|
*
|
|
202
202
|
* @param db - open database handle
|
|
203
203
|
*/
|
|
204
204
|
ensureAdministrator(db) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
205
|
+
const outcome = (0, import_firstAdministrator.ensureFirstAdministrator)({
|
|
206
|
+
users: (0, import_users.createUsersRepository)(db),
|
|
207
|
+
login: this.config.adminLogin,
|
|
208
|
+
password: this.config.adminPassword,
|
|
209
|
+
timezone: this.config.timezone
|
|
210
|
+
});
|
|
211
|
+
switch (outcome.action) {
|
|
212
|
+
case "created":
|
|
213
|
+
if (outcome.generated) {
|
|
214
|
+
this.log.warn(
|
|
215
|
+
`administrator "${outcome.login}" created with the start password "${outcome.password}" - change it at the first login`
|
|
216
|
+
);
|
|
217
|
+
} else {
|
|
218
|
+
this.log.info(
|
|
219
|
+
`administrator "${outcome.login}" created with the configured start password - it has to be changed at the first login`
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
return;
|
|
223
|
+
case "start_password_reset":
|
|
224
|
+
this.log.info(
|
|
225
|
+
`administrator "${outcome.login}" exists already and still has its start password - it was set from the instance settings now, it has to be changed at the first login`
|
|
226
|
+
);
|
|
227
|
+
return;
|
|
228
|
+
case "start_password_unchanged":
|
|
229
|
+
this.log.debug(`administrator "${outcome.login}" carries the configured start password already`);
|
|
230
|
+
return;
|
|
231
|
+
case "password_changed":
|
|
232
|
+
this.log.warn(
|
|
233
|
+
`the configured start password is not used: administrator "${outcome.login}" changed his password already (empty the field "Start password of the first administrator" to silence this)`
|
|
234
|
+
);
|
|
235
|
+
return;
|
|
236
|
+
case "existing":
|
|
237
|
+
this.log.debug(
|
|
238
|
+
`administrator "${outcome.login}" exists already - no start password is configured, so none is generated`
|
|
239
|
+
);
|
|
240
|
+
return;
|
|
241
|
+
default:
|
|
242
|
+
this.log.warn(`the administrator "${outcome.login}" could not be created: ${outcome.reason}`);
|
|
236
243
|
}
|
|
237
244
|
}
|
|
238
245
|
/**
|
package/build/main.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/main.ts"],
|
|
4
|
-
"sourcesContent": ["/*\n * ioBroker adapter \"time-tracker\" \u2013 time tracking\n * Scaffolded with @iobroker/create-adapter v3.1.5\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { hostname } from \"node:os\";\nimport * as utils from \"@iobroker/adapter-core\";\nimport { currentSchemaVersion, openAndMigrate, type Db } from \"./lib/db/database\";\nimport { seed } from \"./lib/db/seed\";\nimport type { HolidayCountry } from \"./lib/domain/holidays\";\nimport { createAbsencesRepository } from \"./lib/db/repositories/absences\";\nimport { createDayNotesRepository, type DayNotesRepository } from \"./lib/db/repositories/dayNotes\";\nimport { createEntriesRepository } from \"./lib/db/repositories/entries\";\nimport { createHolidaysRepository } from \"./lib/db/repositories/holidays\";\nimport { createPayoutsRepository } from \"./lib/db/repositories/payouts\";\nimport { createRulesRepository } from \"./lib/db/repositories/rules\";\nimport { createSettingsRepository } from \"./lib/db/repositories/settings\";\nimport { createUsersRepository, type UserRecord, type UsersRepository } from \"./lib/db/repositories/users\";\nimport {\n\tcreateAutomationsRepository,\n\ttype AutomationRuleRecord,\n\ttype AutomationsRepository,\n} from \"./lib/db/repositories/automations\";\nimport { createTerminalsRepository } from \"./lib/db/repositories/terminals\";\nimport { createRfidRepository } from \"./lib/db/repositories/rfid\";\nimport {\n\tcreateTriggersRepository,\n\ttype TriggerRuleRecord,\n\ttype TriggersRepository,\n} from \"./lib/db/repositories/triggers\";\nimport type { EntriesRepository } from \"./lib/db/repositories/entries\";\nimport type { AbsencesRepository } from \"./lib/db/repositories/absences\";\nimport type { SettingsRepository } from \"./lib/db/repositories/settings\";\nimport { createAggregationService, type AggregationService } from \"./lib/services/aggregation\";\nimport { createAuthService, hashPassword } from \"./lib/services/auth\";\nimport { SECRET_FILE_NAME, resolveSessionSecret } from \"./lib/services/sessionSecret\";\nimport { createClosingService, type ClosingService } from \"./lib/services/closing\";\nimport {\n\tapplyPendingRestore,\n\tcreateBackupService,\n\trecordRestore,\n\ttype AppliedRestore,\n\ttype BackupService,\n} from \"./lib/services/backup\";\nimport { createSyncService, type SyncService } from \"./lib/services/sync\";\nimport {\n\tabsenceEvents,\n\tabsenceFeed,\n\tAPI_PREFIX,\n\tcalendarDocument,\n\tcompanyFeedUrl,\n\tCOMPANY_CALENDAR_NAME,\n} from \"./lib/services/calendar\";\nimport { calendarFilePath, calendarMount, ensureCalendarFolder, writeCalendarFile } from \"./lib/adapter/calendarFile\";\nimport {\n\tCOMMAND_IDS,\n\tcreateCalendarStates,\n\tcreateCommandStates,\n\tcreateCompanyStates,\n\tcreateEventStates,\n\tcreateInfoStates,\n\tpublishAllUserStates,\n\tpublishCalendarSnapshot,\n\tpublishCompanySnapshot,\n\tpublishEventSnapshot,\n\treadCompanySnapshot,\n} from \"./lib/adapter/states\";\nimport { PRESENCE_SUFFIX, handlePresenceState, parsePresenceStateId, presenceEvent } from \"./lib/adapter/presence\";\nimport { handleCommand, punchEmployee, punchEvent, type CommandResult } from \"./lib/adapter/commands\";\nimport { evaluateTrigger, triggerText } from \"./lib/adapter/triggers\";\nimport { evaluateAutomation, isoWeekday, runPeriod, workBlock } from \"./lib/adapter/automation\";\nimport { handleMessage } from \"./lib/adapter/messages\";\nimport { createApi, MAX_BACKUP_UPLOAD_BYTES } from \"./lib/web/api\";\nimport type { ApiEvent, EventBus } from \"./lib/web/events\";\nimport { startWebServer, type WebServer } from \"./lib/web/server\";\nimport { localDateTime } from \"./lib/util/time\";\nimport { createStaticHandler } from \"./lib/web/static\";\n\nconst SUPPORTED_COUNTRIES: HolidayCountry[] = [\"CH\", \"DE\", \"AT\"];\n\n/** How often expired sessions are removed (minutes). */\nconst SESSION_PURGE_MINUTES = 30;\n\n/** How often the published figures are refreshed (minutes). */\nconst STATE_REFRESH_MINUTES = 5;\n\n/** How old the newest backup may be before the daily check writes a new one. */\nconst BACKUP_MAX_AGE_HOURS = 20;\n\n/** How often the automation rules are checked (minutes). */\nconst AUTOMATION_CHECK_MINUTES = 1;\n\n/** Name of the file that holds a generated badge (HMAC) secret next to the database. */\nconst TAG_SECRET_FILE_NAME = \"tag-secret\";\n\n/**\n * Events of the API that change the published figures.\n *\n * A punch or a correction arrives through the API of this very process, so nothing would republish the states\n * until the five minute timer fires; these events say exactly when data changed.\n */\nconst FIGURES_EVENT_TYPES = [\"punch\", \"terminal.punch\", \"rfid.scan\", \"entry.update\", \"entry.delete\", \"absence.change\"];\n\n/** Services created at startup. */\ninterface AdapterServices {\n\tusers: UsersRepository;\n\tentries: EntriesRepository;\n\tabsences: AbsencesRepository;\n\tdayNotes: DayNotesRepository;\n\tsettings: SettingsRepository;\n\taggregation: AggregationService;\n\tsync: SyncService;\n\tclosing: ClosingService;\n\tbackup: BackupService;\n\ttriggers: TriggersRepository;\n\tautomations: AutomationsRepository;\n}\n\nclass TimeTracker extends utils.Adapter {\n\tprivate db: Db | null = null;\n\tprivate webServer: WebServer | null = null;\n\tprivate services: AdapterServices | null = null;\n\t/** Bus of the API; `null` until the API is created */\n\tprivate events: EventBus | null = null;\n\t/** Last value seen per watched trigger state: only a change fires a rule */\n\tprivate readonly triggerValues = new Map<string, string>();\n\t/** Trigger states the adapter currently subscribes to */\n\tprivate readonly triggerStates = new Set<string>();\n\t/** Pending refresh of the figures after a change through the API */\n\tprivate stateRefreshTimer: ioBroker.Timeout | undefined = undefined;\n\n\t/** A refresh tick that is still running; the next tick is skipped instead of piling up on it. */\n\tprivate stateRefreshRunning = false;\n\n\t/** A backup run that is still running; the hourly check must not start a second one. */\n\tprivate backupRunning = false;\n\n\tpublic constructor(options: Partial<utils.AdapterOptions> = {}) {\n\t\tsuper({\n\t\t\t...options,\n\t\t\tname: \"time-tracker\",\n\t\t});\n\t\tthis.on(\"ready\", this.onReady.bind(this));\n\t\tthis.on(\"stateChange\", this.onStateChange.bind(this));\n\t\t// this.on(\"objectChange\", this.onObjectChange.bind(this));\n\t\t// scripts, Blockly and other adapters reach the instance through `sendTo` messages\n\t\tthis.on(\"message\", this.onMessage.bind(this));\n\t\tthis.on(\"unload\", this.onUnload.bind(this));\n\t}\n\n\t/** Database file: configured path or `<adapter instance data dir>/time-tracker.sqlite`. */\n\tprivate databaseFile(): string {\n\t\tconst configured = (this.config.dbPath ?? \"\").trim();\n\t\tif (configured) {\n\t\t\treturn path.resolve(configured);\n\t\t}\n\n\t\tconst dir = utils.getAbsoluteInstanceDataDir(this);\n\t\tfs.mkdirSync(dir, { recursive: true });\n\t\treturn path.join(dir, \"time-tracker.sqlite\");\n\t}\n\n\t/** Configured holiday country, falling back to Switzerland. */\n\tprivate holidayCountry(): HolidayCountry {\n\t\tconst configured = (this.config.holidayCountry ?? \"\").toUpperCase() as HolidayCountry;\n\t\treturn SUPPORTED_COUNTRIES.includes(configured) ? configured : \"CH\";\n\t}\n\n\t/**\n\t * Is called when databases are connected and adapter received configuration.\n\t */\n\tprivate async onReady(): Promise<void> {\n\t\ttry {\n\t\t\t// Reset the connection indicator during startup\n\t\t\tawait this.setState(\"info.connection\", false, true);\n\n\t\t\tconst file = this.databaseFile();\n\t\t\t// A restore that the administration queued is applied before the database is opened: the swap needs a\n\t\t\t// closed file, and that is the only moment the adapter can offer it (see the backup service).\n\t\t\tlet applied: AppliedRestore | null = null;\n\t\t\ttry {\n\t\t\t\tapplied = applyPendingRestore(file);\n\t\t\t\tif (applied) {\n\t\t\t\t\tthis.log.info(\n\t\t\t\t\t\t`restored the database from ${applied.restored.name}: ${applied.restored.users} users, ${applied.restored.entries} punches, previous file ${applied.previous ?? \"(none)\"}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\t// the queued files stay in place, so a working backup can be handed in afterwards\n\t\t\t\tthis.log.error(`the queued restore was refused: ${(error as Error).message}`);\n\t\t\t}\n\t\t\tthis.db = openAndMigrate(file, message => this.log.debug(message));\n\t\t\tif (applied) {\n\t\t\t\t// the audit entry can only be written once the restored database is open\n\t\t\t\trecordRestore(this.db, applied.restored, applied.actorId);\n\t\t\t}\n\n\t\t\tconst year = new Date().getUTCFullYear();\n\t\t\tconst country = this.holidayCountry();\n\t\t\tconst result = seed(this.db, { holidayCountry: country, holidayYears: [year, year + 1] });\n\t\t\tthis.log.info(\n\t\t\t\t`database ready at ${file} (${result.permissions} permissions, ${result.holidays} holidays added for ${country})`,\n\t\t\t);\n\n\t\t\t// Without an account nobody can log in, so the first administrator is created here (Phase 2 of the\n\t\t\t// specification: \"Standard-Admin, Passwort mit Pflichtwechsel\").\n\t\t\tthis.ensureAdministrator(this.db);\n\n\t\t\t// the settings of the admin UI win over the stored values (before the services are built)\n\t\t\tthis.applyConfiguration(createSettingsRepository(this.db));\n\n\t\t\t// NOTE: The implementation follows the internal specification (not part of this repository):\n\t\t\t// Phase 1\u20133 \u2013 storage, punch logic, target time, breaks, overtime, vacation\n\t\t\t// Phase 4 \u2013 REST API on an own port (router, sessions, RBAC)\n\t\t\t// Phase 5 \u2013 PWA delivered from www/\n\t\t\t// Phase 8 \u2013 publish aggregates as states and react to command.*\n\n\t\t\tawait this.startApi();\n\t\t\t// creates the command and info states, so the instance information can be published afterwards\n\t\t\tawait this.subscribeCommands();\n\t\t\t// watches the states the trigger rules name (fingerprint reader, button, door contact, \u2026)\n\t\t\tawait this.refreshTriggerSubscriptions();\n\t\t\t// the automation rules look at the clock, so they are checked once a minute\n\t\t\tthis.runAutomationRules();\n\t\t\tthis.setInterval(() => this.runAutomationRules(), AUTOMATION_CHECK_MINUTES * 60 * 1000);\n\t\t\tawait this.publishInstanceInfo();\n\t\t\t// Files below `files/` hang on a **mount point** \u2014 an object of type `meta`. Only there writes the file API\n\t\t\t// of the adapter; below the instance namespace itself the object database refuses with\n\t\t\t// `time-tracker.0 is not an object of type \"meta\"` (D11). Creating it here means the folder is in the file\n\t\t\t// manager from the first start on \u2014 before the first calendar was written and also on an instance that was\n\t\t\t// installed earlier.\n\t\t\ttry {\n\t\t\t\tawait ensureCalendarFolder(this, this.namespace);\n\t\t\t} catch (error) {\n\t\t\t\tthis.log.warn(\n\t\t\t\t\t`the mount point '${calendarMount(this.namespace)}' could not be prepared: ${(error as Error).message}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tawait this.refreshStates();\n\n\t\t\t// figures are refreshed regularly (the timer is cleared automatically on unload). A tick that finds the\n\t\t\t// previous run still busy is skipped: a slow run must not pile a second one on top of itself.\n\t\t\tthis.setInterval(\n\t\t\t\t() => {\n\t\t\t\t\tif (this.stateRefreshRunning) {\n\t\t\t\t\t\tthis.log.debug(`state refresh still running, skipping this ${STATE_REFRESH_MINUTES} min tick`);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tthis.stateRefreshRunning = true;\n\t\t\t\t\tvoid Promise.allSettled([this.publishInstanceInfo(), this.refreshStates()]).then(results => {\n\t\t\t\t\t\tthis.stateRefreshRunning = false;\n\t\t\t\t\t\tfor (const result of results) {\n\t\t\t\t\t\t\tif (result.status === \"rejected\") {\n\t\t\t\t\t\t\t\tthis.log.warn(`state refresh failed: ${(result.reason as Error).message}`);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t\tSTATE_REFRESH_MINUTES * 60 * 1000,\n\t\t\t);\n\n\t\t\t// one backup per day: the check runs every hour, so a missed run is caught up after a restart. A run that\n\t\t\t// is still going (a big database, a slow disk) blocks the next check instead of starting a second copy.\n\t\t\tawait this.runScheduledBackup();\n\t\t\tthis.setInterval(\n\t\t\t\t() => {\n\t\t\t\t\tif (this.backupRunning) {\n\t\t\t\t\t\tthis.log.debug(\"scheduled backup still running, skipping this hour\");\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tthis.backupRunning = true;\n\t\t\t\t\tvoid this.runScheduledBackup()\n\t\t\t\t\t\t.catch(error => this.log.warn(`scheduled backup failed: ${(error as Error).message}`))\n\t\t\t\t\t\t.finally(() => {\n\t\t\t\t\t\t\tthis.backupRunning = false;\n\t\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t\t60 * 60 * 1000,\n\t\t\t);\n\n\t\t\t// Service is ready\n\t\t\tawait this.setState(\"info.connection\", true, true);\n\t\t} catch (error) {\n\t\t\tconst message = (error as Error).message;\n\t\t\tthis.log.error(`Startup failed: ${message}`);\n\t\t\tawait this.setState(\"info.lastError\", message, true);\n\t\t}\n\t}\n\n\t/**\n\t * Creates the first administrator when the instance has none.\n\t *\n\t * The account is created with `must_change_pw = 1`, so the start password opens the door exactly once.\n\t * Login and password can be configured; without a configured password a random one is generated and written\n\t * to the log, because there is no other way to hand it to the operator.\n\t *\n\t * @param db - open database handle\n\t */\n\tprivate ensureAdministrator(db: Db): void {\n\t\tconst users = createUsersRepository(db);\n\t\tconst administrators = users.list().filter(user => users.roles(user.id).includes(\"admin\"));\n\t\tif (administrators.length > 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst login = (this.config.adminLogin ?? \"\").trim() || \"admin\";\n\t\tconst configured = (this.config.adminPassword ?? \"\").trim();\n\t\tconst password = configured || `Zf-${randomBytes(9).toString(\"base64url\")}-7`;\n\n\t\ttry {\n\t\t\tusers.create({\n\t\t\t\tlogin,\n\t\t\t\tdisplayName: login,\n\t\t\t\tpasswordHash: hashPassword(password),\n\t\t\t\tmustChangePw: true,\n\t\t\t\ttimezone: this.config.timezone || \"Europe/Berlin\",\n\t\t\t\troleKeys: [\"admin\"],\n\t\t\t\tnow: Math.floor(Date.now() / 1000),\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tthis.log.warn(`the administrator \"${login}\" could not be created: ${(error as Error).message}`);\n\t\t\treturn;\n\t\t}\n\n\t\tif (configured) {\n\t\t\tthis.log.info(\n\t\t\t\t`administrator \"${login}\" created with the configured start password - it has to be changed at the first login`,\n\t\t\t);\n\t\t} else {\n\t\t\tthis.log.warn(\n\t\t\t\t`administrator \"${login}\" created with the start password \"${password}\" - change it at the first login`,\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Copies the instance configuration into the stored settings and publishes the instance information.\n\t *\n\t * The ioBroker admin UI is where an operator configures the instance, so a configured value wins over what\n\t * is stored in the database; `PUT /api/settings` stays for the keys the admin UI does not offer. Empty\n\t * fields are ignored, so clearing a field never wipes a stored setting.\n\t *\n\t * @param settings - instance settings\n\t */\n\tprivate applyConfiguration(settings: SettingsRepository): void {\n\t\tconst text = (value: string | undefined): string | null => (value?.trim() ? value.trim() : null);\n\t\tconst number = (value: number | undefined): string | null =>\n\t\t\ttypeof value === \"number\" && Number.isFinite(value) ? String(value) : null;\n\t\tconst flag = (value: boolean | undefined): string | null => (value === undefined ? null : value ? \"1\" : \"0\");\n\n\t\tconst mapped: [string, string | null][] = [\n\t\t\t[\"holiday_country\", text(this.config.holidayCountry)?.toUpperCase() ?? null],\n\t\t\t[\"timezone\", text(this.config.timezone)],\n\t\t\t[\"default_language\", text(this.config.defaultLanguage)],\n\t\t\t[\"edit_window_days\", number(this.config.editWindowDays)],\n\t\t\t[\"quick_round_minutes\", number(this.config.quickRoundMinutes)],\n\t\t\t[\"session_ttl_minutes\", number(this.config.sessionTtlMinutes)],\n\t\t\t[\"backup_retention_days\", number(this.config.backupRetentionDays)],\n\t\t\t[\"absence_calc_until_today\", flag(this.config.absenceCalcUntilToday)],\n\t\t\t[\"absence_deduct_worktime\", flag(this.config.absenceDeductWorktime)],\n\t\t];\n\n\t\tconst now = Math.floor(Date.now() / 1000);\n\t\tfor (const [key, value] of mapped) {\n\t\t\tif (value !== null && settings.get(key) !== value) {\n\t\t\t\tsettings.set(key, value, null, now);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Publishes version, database size and schema version as states (specification 5.1).\n\t *\n\t * The objects are created by `createInfoStates`, so this runs after the command states were set up.\n\t */\n\tprivate async publishInstanceInfo(): Promise<void> {\n\t\tawait this.setState(\"info.version\", this.version ?? \"0.0.0\", true);\n\t\tconst file = this.db ? this.databaseFile() : \"\";\n\t\tawait this.setState(\"info.dbSizeBytes\", file && fs.existsSync(file) ? fs.statSync(file).size : 0, true);\n\t\tif (this.db) {\n\t\t\tawait this.setState(\"info.schemaVersion\", String(currentSchemaVersion(this.db)), true);\n\t\t}\n\t}\n\n\t/**\n\t * Secret used to sign CSRF tokens. A value from the instance settings wins; without one the adapter generates a\n\t * secret on the first start and stores it next to the database, so a restart keeps the CSRF tokens of clients\n\t * that are already open valid.\n\t */\n\tprivate sessionSecret(): string {\n\t\tconst resolved = resolveSessionSecret({\n\t\t\tconfigured: this.config.sessionSecret,\n\t\t\tfile: path.join(path.dirname(this.databaseFile()), SECRET_FILE_NAME),\n\t\t});\n\n\t\tif (resolved.source === \"configured\") {\n\t\t\tthis.log.debug(\"session secret taken from the instance settings\");\n\t\t} else if (resolved.source === \"stored\") {\n\t\t\tthis.log.debug(`session secret taken from ${resolved.file}`);\n\t\t} else if (resolved.file) {\n\t\t\tthis.log.info(\n\t\t\t\t`no session secret configured - generated one and stored it at ${resolved.file}, so sessions and CSRF tokens survive restarts`,\n\t\t\t);\n\t\t} else {\n\t\t\tthis.log.warn(\n\t\t\t\t`no session secret configured and it could not be stored (${resolved.error ?? \"unknown\"}) - a temporary one is used, so all sessions end with the next restart`,\n\t\t\t);\n\t\t}\n\n\t\treturn resolved.secret;\n\t}\n\n\t/**\n\t * Secret that signs the badge/NFC links.\n\t *\n\t * A value from the instance settings wins. Without one a secret is generated on the first start and stored next\n\t * to the database \u2014 otherwise a fresh installation could not create a badge at all and the administration would\n\t * only report \u201Cnot configured\u201D.\n\t */\n\tprivate tagSecret(): string {\n\t\tconst resolved = resolveSessionSecret({\n\t\t\tconfigured: this.config.hmacSecret,\n\t\t\tfile: path.join(path.dirname(this.databaseFile()), TAG_SECRET_FILE_NAME),\n\t\t});\n\n\t\tif (resolved.source === \"configured\") {\n\t\t\tthis.log.debug(\"badge link secret taken from the instance settings\");\n\t\t} else if (resolved.source === \"stored\") {\n\t\t\tthis.log.debug(`badge link secret taken from ${resolved.file}`);\n\t\t} else if (resolved.file) {\n\t\t\tthis.log.info(\n\t\t\t\t`no badge link secret configured - generated one and stored it at ${resolved.file}, so badges stay valid across restarts`,\n\t\t\t);\n\t\t} else {\n\t\t\tthis.log.warn(\n\t\t\t\t`no badge link secret configured and it could not be stored (${resolved.error ?? \"unknown\"}) - a temporary one is used, so badges stop working with the next restart`,\n\t\t\t);\n\t\t}\n\n\t\treturn resolved.secret;\n\t}\n\n\t/**\n\t * Builds the services and starts the HTTP server on the configured port.\n\t *\n\t * Binding can fail (port in use); that must never stop the adapter, so the failure is only logged.\n\t */\n\tprivate async startApi(): Promise<void> {\n\t\tconst db = this.db;\n\t\tif (!db) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst users = createUsersRepository(db);\n\t\tconst entries = createEntriesRepository(db);\n\t\tconst absences = createAbsencesRepository(db);\n\t\tconst dayNotes = createDayNotesRepository(db);\n\t\tconst holidays = createHolidaysRepository(db);\n\t\tconst rules = createRulesRepository(db);\n\t\tconst payouts = createPayoutsRepository(db);\n\t\tconst terminals = createTerminalsRepository(db);\n\t\tconst rfid = createRfidRepository(db);\n\t\tconst settings = createSettingsRepository(db);\n\t\tconst auth = createAuthService({\n\t\t\tdb,\n\t\t\tusers,\n\t\t\tsettings,\n\t\t\tsecret: this.sessionSecret(),\n\t\t\tdefaultTtlMinutes: this.config.sessionTtlMinutes || 720,\n\t\t});\n\t\tconst aggregation = createAggregationService({\n\t\t\tdb,\n\t\t\tusers,\n\t\t\tentries,\n\t\t\tabsences,\n\t\t\tholidays,\n\t\t\trules,\n\t\t\tsettings,\n\t\t});\n\t\tconst triggers = createTriggersRepository(db);\n\t\tconst automations = createAutomationsRepository(db);\n\t\tconst sync = createSyncService({ db, entries, users, aggregation });\n\t\tconst closing = createClosingService({ db, aggregation, payouts });\n\t\t// backups live next to the database file: `<data dir>/backups/time-tracker-<timestamp>.sqlite`\n\t\tconst backup = createBackupService({\n\t\t\tdb,\n\t\t\tdir: path.join(path.dirname(this.databaseFile()), \"backups\"),\n\t\t\tretentionDays: Math.max(0, Number(this.config.backupRetentionDays ?? 30) || 0),\n\t\t});\n\t\tconst api = createApi({\n\t\t\tdb,\n\t\t\tauth,\n\t\t\tusers,\n\t\t\tentries,\n\t\t\tabsences,\n\t\t\tdayNotes,\n\t\t\tholidays,\n\t\t\trules,\n\t\t\tpayouts,\n\t\t\tterminals,\n\t\t\trfid,\n\t\t\ttriggers,\n\t\t\tautomations,\n\t\t\taggregation,\n\t\t\tsync,\n\t\t\tsettings,\n\t\t\tbackup,\n\t\t\tkioskEnabled: this.config.kioskEnabled === true,\n\t\t\t// a reverse proxy in front is the normal case for HTTPS; without the switch the forwarded\n\t\t\t// headers are ignored, so a client cannot choose its own address or the HTTPS flag\n\t\t\ttrustProxy: this.config.trustProxy === true,\n\t\t\thmacSecret: this.tagSecret(),\n\t\t\tversion: this.version,\n\t\t\t// a saved trigger rule changes the states the adapter has to watch\n\t\t\tonTriggerRulesChanged: () => void this.refreshTriggerSubscriptions(),\n\t\t});\n\n\t\tthis.services = {\n\t\t\tusers,\n\t\t\tentries,\n\t\t\tabsences,\n\t\t\tdayNotes,\n\t\t\tsettings,\n\t\t\taggregation,\n\t\t\tsync,\n\t\t\tclosing,\n\t\t\tbackup,\n\t\t\ttriggers,\n\t\t\tautomations,\n\t\t};\n\t\tthis.events = api.events;\n\t\t// the newest event is mirrored into the state tree, so a notification only has to watch `events.*`;\n\t\t// a punch, a correction or an absence changes the figures, so they are republished right away\n\t\tapi.events.subscribe(event => {\n\t\t\tvoid this.publishEventState(event);\n\t\t\tif (FIGURES_EVENT_TYPES.includes(event.type)) {\n\t\t\t\tthis.scheduleStateRefresh();\n\t\t\t}\n\t\t});\n\t\tthis.log.debug(`API routes: ${api.routes().length}`);\n\n\t\t// the web interface is delivered from `www/` next to the compiled code (built by the PWA project);\n\t\t// without it the adapter still provides the API, e.g. for the terminal\n\t\tconst webDir = path.join(__dirname, \"..\", \"www\");\n\t\tconst staticFiles = fs.existsSync(path.join(webDir, \"index.html\"))\n\t\t\t? createStaticHandler({ root: webDir })\n\t\t\t: undefined;\n\t\tthis.log.info(\n\t\t\tstaticFiles\n\t\t\t\t? `web interface found at ${webDir}`\n\t\t\t\t: `no web interface at ${webDir} - only the API (and the terminal) is available`,\n\t\t);\n\n\t\ttry {\n\t\t\tthis.webServer = await startWebServer({\n\t\t\t\trouter: api.router,\n\t\t\t\tport: this.config.port || 8092,\n\t\t\t\tbind: this.config.bind || \"127.0.0.1\",\n\t\t\t\t// the same prefix the calendar link carries (`/api`) \u2014 the web app and its login live beside it\n\t\t\t\tapiPrefix: API_PREFIX,\n\t\t\t\tstaticFiles,\n\t\t\t\t// an uploaded backup is bigger than the default limit; the route carries the same bound\n\t\t\t\tmaxBodyBytes: MAX_BACKUP_UPLOAD_BYTES,\n\t\t\t\t// live events for the PWA and the terminal: `/api/stream?token=...`\n\t\t\t\tstream: {\n\t\t\t\t\tauth,\n\t\t\t\t\tevents: api.events,\n\t\t\t\t\tversion: this.version,\n\t\t\t\t\ttimers: {\n\t\t\t\t\t\t// the adapter's own timer functions: they are cleared with the adapter on unload\n\t\t\t\t\t\tsetInterval: (handler, milliseconds) => this.setInterval(handler, milliseconds),\n\t\t\t\t\t\tclearInterval: handle => this.clearInterval(handle as ioBroker.Interval),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tlog: {\n\t\t\t\t\tinfo: message => this.log.info(message),\n\t\t\t\t\twarn: message => this.log.warn(message),\n\t\t\t\t\terror: message => this.log.error(message),\n\t\t\t\t},\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tthis.log.error(\n\t\t\t\t`API could not be started on port ${this.config.port}: ${(error as Error).message} - the web interface and the terminal stay unavailable`,\n\t\t\t);\n\t\t\tthis.webServer = null;\n\t\t\treturn;\n\t\t}\n\n\t\t// expired sessions are removed regularly; the timer is cleared automatically on unload\n\t\tthis.setInterval(\n\t\t\t() => {\n\t\t\t\ttry {\n\t\t\t\t\tconst removed = auth.purge();\n\t\t\t\t\tif (removed > 0) {\n\t\t\t\t\t\tthis.log.debug(`removed ${removed} expired session(s)`);\n\t\t\t\t\t}\n\t\t\t\t} catch (error) {\n\t\t\t\t\tthis.log.warn(`session cleanup failed: ${(error as Error).message}`);\n\t\t\t\t}\n\t\t\t},\n\t\t\tSESSION_PURGE_MINUTES * 60 * 1000,\n\t\t);\n\t}\n\n\t/**\n\t * Creates the command states and subscribes to them.\n\t */\n\tprivate async subscribeCommands(): Promise<void> {\n\t\ttry {\n\t\t\tawait createCommandStates(this);\n\t\t\tawait createInfoStates(this);\n\t\t\tawait createCompanyStates(this);\n\t\t\tawait createEventStates(this);\n\t\t\tawait createCalendarStates(this);\n\t\t\tawait this.subscribeStatesAsync(\"commands.*\");\n\t\t\t// the presence switch of every employee: written by a script, a fingerprint reader or a dashboard\n\t\t\tawait this.subscribeStatesAsync(`users.*.${PRESENCE_SUFFIX}`);\n\t\t\tthis.log.debug(\"command states ready\");\n\t\t} catch (error) {\n\t\t\tthis.log.warn(`command states could not be created: ${(error as Error).message}`);\n\t\t}\n\t}\n\n\t/**\n\t * Writes a backup unless a recent one exists.\n\t *\n\t * The check runs hourly and once at start: a nightly backup therefore happens within an hour at the latest,\n\t * and an instance that was down for days does not write a burst of copies.\n\t */\n\tprivate async runScheduledBackup(): Promise<void> {\n\t\tconst backup = this.services?.backup;\n\t\tif (!backup) {\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tconst newest = backup.list()[0];\n\t\t\tconst ageHours = newest ? (Date.now() / 1000 - newest.createdAt) / 3600 : Number.POSITIVE_INFINITY;\n\t\t\tif (ageHours < BACKUP_MAX_AGE_HOURS) {\n\t\t\t\tthis.log.debug(`backup ${newest?.name ?? \"\"} is ${ageHours.toFixed(1)} h old, nothing to do`);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst created = backup.create({ actorId: null, reason: \"scheduled\" });\n\t\t\tthis.log.info(\n\t\t\t\t`backup ${created.backup.name} written (${created.backup.sizeBytes} bytes, ${created.backup.users} employees, ${created.backup.entries} punches), ${created.removed.length} old file(s) removed`,\n\t\t\t);\n\t\t\tawait this.announceBackup(created.backup.name, created.backup.createdAt);\n\t\t} catch (error) {\n\t\t\tthis.log.warn(`backup failed: ${(error as Error).message}`);\n\t\t}\n\t}\n\n\t/**\n\t * Publishes the instant of a written backup.\n\t *\n\t * @param name - file name of the backup\n\t * @param createdAt - instant the backup belongs to\n\t */\n\tprivate async announceBackup(name: string, createdAt: number): Promise<void> {\n\t\tawait this.setState(\"info.lastBackup\", createdAt, true);\n\t\tthis.events?.publish({\n\t\t\ttype: \"backup.create\",\n\t\t\tatUtc: createdAt,\n\t\t\tuserId: null,\n\t\t\tdata: { backup: name },\n\t\t});\n\t}\n\n\t/**\n\t * Applies a write on the presence state of an employee.\n\t *\n\t * `users.<id>.present` is the writable twin of `users.<id>.hasOpenEntry`: `true` opens an entry, `false` closes\n\t * it. Afterwards the figures of all employees are republished, so a dashboard sees the new state right away.\n\t *\n\t * @param id - state id without the instance prefix\n\t * @param value - value written by the user or a script\n\t */\n\tprivate async runPresence(id: string, value: ioBroker.StateValue): Promise<void> {\n\t\tconst services = this.services;\n\t\tif (!services) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tconst result = handlePresenceState(\n\t\t\t\t{\n\t\t\t\t\tentries: services.entries,\n\t\t\t\t\tusers: services.users,\n\t\t\t\t\taggregation: services.aggregation,\n\t\t\t\t},\n\t\t\t\tid,\n\t\t\t\tvalue,\n\t\t\t);\n\t\t\tif (result.ok) {\n\t\t\t\tthis.log.info(`presence ${id}: ${result.message}`);\n\t\t\t\t// A punch from the presence state is published like every other one: the subscription in `onReady`\n\t\t\t\t// mirrors it into `events.*` and tells the connected web clients.\n\t\t\t\tconst event = presenceEvent(result, Math.floor(Date.now() / 1000));\n\t\t\t\tif (event) {\n\t\t\t\t\tthis.events?.publish(event);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// a value the state does not understand, an unknown employee: worth a warning, not an error\n\t\t\t\tthis.log.warn(`presence ${id}: ${result.message}`);\n\t\t\t}\n\t\t\tawait this.refreshStates();\n\t\t} catch (error) {\n\t\t\tthis.log.error(`Error in runPresence for ${id}: ${(error as Error).message}`);\n\t\t}\n\t}\n\n\t/**\n\t * Publishes the figures of all employees.\n\t */\n\tprivate async refreshStates(): Promise<void> {\n\t\tconst services = this.services;\n\t\tif (!services) {\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tconst snapshots = await publishAllUserStates({\n\t\t\t\tport: this,\n\t\t\t\taggregation: services.aggregation,\n\t\t\t\tusers: services.users,\n\t\t\t\tsync: services.sync,\n\t\t\t});\n\t\t\tthis.log.debug(`published ${snapshots.length} employee state(s)`);\n\t\t\t// the company figures are derived from the same snapshots, so they never disagree\n\t\t\tawait publishCompanySnapshot(this, readCompanySnapshot(snapshots));\n\t\t\t// the target employee of the punch commands is mirrored into the state the user writes (acknowledged, so\n\t\t\t// the adapter ignores its own write \u2014 see `onStateChange`). `0` means \"the only employee\".\n\t\t\tawait this.setState(COMMAND_IDS.punchUserId, services.settings.getNumber(\"command_punch_user_id\", 0), true);\n\t\t} catch (error) {\n\t\t\tthis.log.warn(`states could not be published: ${(error as Error).message}`);\n\t\t}\n\n\t\t// The calendar is a file and not a state, so its failure is reported as what it is. Until 0.7.11 it ran inside\n\t\t// the block above: a write error of the file API then appeared as \u201Estates could not be published\" and hid the\n\t\t// real cause (the log of the acceptance test showed exactly that line \u2014 see D11).\n\t\ttry {\n\t\t\tawait this.publishCalendar();\n\t\t} catch (error) {\n\t\t\tthis.log.warn(`the calendar could not be written: ${(error as Error).message}`);\n\t\t}\n\t}\n\n\t/**\n\t * Writes the calendar of the instance and publishes what ioBroker needs.\n\t *\n\t * The `.ics` file is what the `ical` adapter reads as a **local source** \u2014 no URL, no token, no network \u2014 and what\n\t * a script can hand to another consumer. `calendar.absences` carries the same days as JSON for scripts and\n\t * dashboards. The subscription link is published only once the administration created it\n\t * (`commands.rotateCalendarToken`), because that link opens the absences of **all** employees to whoever holds it.\n\t */\n\tprivate async publishCalendar(): Promise<void> {\n\t\tconst services = this.services;\n\t\tif (!services) {\n\t\t\treturn;\n\t\t}\n\n\t\t// the same window the feed route uses: a year back and to the end of next year\n\t\tconst stamp = Math.floor(Date.now() / 1000);\n\t\tconst year = new Date(stamp * 1000).getUTCFullYear();\n\t\tconst from = `${year - 1}-01-01`;\n\t\tconst to = `${year + 1}-12-31`;\n\n\t\t// deactivated accounts stay out (`list()` returns the active ones)\n\t\tconst employees = new Map(\n\t\t\tservices.users\n\t\t\t\t.list()\n\t\t\t\t.map(user => [user.id, { id: user.id, displayName: user.displayName, login: user.login }]),\n\t\t);\n\t\tconst list = services.absences.allInRange(from, to);\n\n\t\t// The file lives below `files/<namespace>/<mount point>/` and is written through the file API of the adapter:\n\t\t// only that way is it a folder the file manager shows and a `web` instance hands out (D11) \u2014 a file written\n\t\t// with `fs` stays a private file. The absolute path is what `calendar.feedFile` carries for the `ical` adapter.\n\t\tconst file = calendarFilePath(utils.getAbsoluteDefaultDataDir(), this.namespace);\n\t\tconst document = calendarDocument(COMPANY_CALENDAR_NAME, absenceEvents(list, employees, true), stamp);\n\t\tawait writeCalendarFile(this, this.namespace, document);\n\n\t\tconst token = services.settings.get(\"calendar_token\");\n\t\tawait publishCalendarSnapshot(this, {\n\t\t\t// the host part is the name of this machine: an address that is reachable from the LAN and simple enough\n\t\t\t// to correct by hand (a reverse proxy or another address is a matter of the copy in the calendar app)\n\t\t\tfeedUrl: token ? companyFeedUrl(hostname(), this.config.port || 8092, token) : \"\",\n\t\t\tfeedFile: file,\n\t\t\tupdatedAt: stamp,\n\t\t\tabsences: JSON.stringify(absenceFeed(list, employees)),\n\t\t});\n\t}\n\n\t/**\n\t * Runs a command state and reports the outcome.\n\t *\n\t * @param id - state id without the instance prefix\n\t * @param value - value written by the user or a script\n\t */\n\tprivate async runCommand(id: string, value: ioBroker.StateValue): Promise<void> {\n\t\tconst services = this.services;\n\t\tconst db = this.db;\n\t\tif (!services || !db) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tconst result = handleCommand(\n\t\t\t\t{\n\t\t\t\t\tdb,\n\t\t\t\t\tentries: services.entries,\n\t\t\t\t\tusers: services.users,\n\t\t\t\t\tsettings: services.settings,\n\t\t\t\t\taggregation: services.aggregation,\n\t\t\t\t\tclosing: services.closing,\n\t\t\t\t\tbackup: services.backup,\n\t\t\t\t},\n\t\t\t\tid,\n\t\t\t\tvalue,\n\t\t\t);\n\t\t\tthis.log.info(`command ${id}: ${result.message}`);\n\t\t\t// a punch written by a command state lands in `events.*` and in the web app like every other one\n\t\t\tthis.publishPunch(punchEvent(result, `commands.${id}`, Math.floor(Date.now() / 1000)));\n\t\t\t// a closing is triggered from a state, not from a request: tell the connected clients about it\n\t\t\t// (only the period is published, never the figures of the employee it belongs to)\n\t\t\tif (id === COMMAND_IDS.closeMonth) {\n\t\t\t\tthis.events?.publish({\n\t\t\t\t\ttype: \"month.close\",\n\t\t\t\t\tatUtc: Math.floor(Date.now() / 1000),\n\t\t\t\t\tuserId: null,\n\t\t\t\t\tdata: { period: String(value ?? \"\") },\n\t\t\t\t});\n\t\t\t}\n\t\t\t// a manual backup reports its file the same way the scheduled one does\n\t\t\tif (id === COMMAND_IDS.backup && result.ok) {\n\t\t\t\tconst newest = services.backup.list()[0];\n\t\t\t\tif (newest) {\n\t\t\t\t\tawait this.announceBackup(newest.name, newest.createdAt);\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthis.log.warn(`command ${id} failed: ${(error as Error).message}`);\n\t\t}\n\n\t\t// buttons are stateless: always release them again\n\t\tif (\n\t\t\tid === COMMAND_IDS.punch ||\n\t\t\tid === COMMAND_IDS.quickPunch ||\n\t\t\tid === COMMAND_IDS.backup ||\n\t\t\tid === COMMAND_IDS.rotateCalendarToken\n\t\t) {\n\t\t\tawait this.setState(id, false, true);\n\t\t}\n\t\tawait this.refreshStates();\n\t}\n\n\t/**\n\t * Watches exactly the states the active trigger rules name.\n\t *\n\t * Runs at startup and whenever the administration saves the rules, so an added, changed or removed rule takes\n\t * effect without a restart. A state that no rule names any more is dropped from the watch list.\n\t */\n\tprivate async refreshTriggerSubscriptions(): Promise<void> {\n\t\tconst services = this.services;\n\t\tif (!services) {\n\t\t\treturn;\n\t\t}\n\n\t\tlet rules: TriggerRuleRecord[] = [];\n\t\ttry {\n\t\t\trules = services.triggers.list();\n\t\t} catch (error) {\n\t\t\tthis.log.warn(`trigger rules could not be read: ${(error as Error).message}`);\n\t\t\treturn;\n\t\t}\n\n\t\tconst wanted = new Set(rules.map(rule => rule.sourceState));\n\t\tfor (const id of [...this.triggerStates]) {\n\t\t\tif (wanted.has(id)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tawait this.unsubscribeForeignStatesAsync(id);\n\t\t\t} catch (error) {\n\t\t\t\tthis.log.debug(`trigger state ${id} could not be released: ${(error as Error).message}`);\n\t\t\t}\n\t\t\tthis.triggerStates.delete(id);\n\t\t\tthis.triggerValues.delete(id);\n\t\t}\n\n\t\tfor (const id of wanted) {\n\t\t\tif (this.triggerStates.has(id)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tawait this.subscribeForeignStatesAsync(id);\n\t\t\t\tthis.triggerStates.add(id);\n\t\t\t} catch (error) {\n\t\t\t\tthis.log.warn(`trigger state ${id} could not be watched: ${(error as Error).message}`);\n\t\t\t}\n\t\t}\n\n\t\tthis.log.debug(`watching ${this.triggerStates.size} trigger state(s) for ${rules.length} active rule(s)`);\n\t}\n\n\t/**\n\t * Applies a write on a state that trigger rules watch.\n\t *\n\t * Such a state belongs to another adapter, so its `ack` says nothing about whether the value was set by a\n\t * script or by the device. What counts is that the value **changed**: a reader that repeats itself, a\n\t * dashboard that refreshes or a script that writes the same value twice does not punch twice. Together with\n\t * the cooldown of a rule even a rapidly blinking state stays harmless.\n\t *\n\t * @param id - full state id\n\t * @param state - state object of the write\n\t * @returns true when the state belongs to a rule and was handled here\n\t */\n\tprivate handleTriggerState(id: string, state: ioBroker.State): boolean {\n\t\tconst services = this.services;\n\t\tif (!services) {\n\t\t\treturn false;\n\t\t}\n\n\t\tconst previous = this.triggerValues.get(id) ?? null;\n\t\tthis.triggerValues.set(id, triggerText(state.val));\n\n\t\tconst rules = services.triggers.list().filter(rule => rule.sourceState === id);\n\t\tif (rules.length === 0) {\n\t\t\treturn false;\n\t\t}\n\n\t\tconst now = Math.floor(Date.now() / 1000);\n\t\tfor (const rule of rules) {\n\t\t\tconst decision = evaluateTrigger(rule, { value: state.val, previous, now }, services.users);\n\t\t\tif (!decision.fire || decision.userId === null) {\n\t\t\t\tthis.log.debug(`trigger ${rule.id} (${rule.sourceState}) did not fire: ${decision.reason}`);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tthis.runTrigger(rule, decision.userId);\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * Runs the action of a rule that fired.\n\t *\n\t * The punch goes through the same path as the command state and a presence change through the same handler as\n\t * the `users.<id>.present` state, so direction, rounding, notes and the recalculation behave identically.\n\t *\n\t * @param rule - rule that fired\n\t * @param userId - employee the action applies to\n\t */\n\tprivate runTrigger(rule: TriggerRuleRecord, userId: number): void {\n\t\tvoid (async (): Promise<void> => {\n\t\t\tconst services = this.services;\n\t\t\tif (!services) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst result =\n\t\t\t\t\trule.action === \"present\" || rule.action === \"absent\"\n\t\t\t\t\t\t? handlePresenceState(\n\t\t\t\t\t\t\t\t{ entries: services.entries, users: services.users, aggregation: services.aggregation },\n\t\t\t\t\t\t\t\t`users.${userId}.${PRESENCE_SUFFIX}`,\n\t\t\t\t\t\t\t\trule.action === \"present\",\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t: punchEmployee(\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tentries: services.entries,\n\t\t\t\t\t\t\t\t\tusers: services.users,\n\t\t\t\t\t\t\t\t\tsettings: services.settings,\n\t\t\t\t\t\t\t\t\taggregation: services.aggregation,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{ userId, quick: rule.action === \"quickPunch\", note: `trigger.${rule.id}` },\n\t\t\t\t\t\t\t);\n\t\t\t\tthis.log.info(`trigger ${rule.id} (${rule.sourceState}) \u2192 ${result.message}`);\n\t\t\t\t// a punch written by a rule lands in `events.*` too; a presence write that changed nothing has no\n\t\t\t\t// event (the builder answers `null` then)\n\t\t\t\tconst atUtc = Math.floor(Date.now() / 1000);\n\t\t\t\tif (\"changed\" in result) {\n\t\t\t\t\tthis.publishPunch(presenceEvent(result, atUtc));\n\t\t\t\t} else {\n\t\t\t\t\tthis.publishPunch(punchEvent(result, `trigger.${rule.id}`, atUtc));\n\t\t\t\t}\n\t\t\t\tservices.triggers.markFired({ id: rule.id });\n\t\t\t\tawait this.refreshStates();\n\t\t\t} catch (error) {\n\t\t\t\t// a deleted employee, a deactivated one, a value that cannot be used: never take the adapter down\n\t\t\t\tthis.log.warn(`trigger ${rule.id} failed: ${(error as Error).message}`);\n\t\t\t}\n\t\t})();\n\t}\n\n\t/**\n\t * Stops the HTTP server.\n\t */\n\tprivate async stopApi(): Promise<void> {\n\t\tconst server = this.webServer;\n\t\tthis.webServer = null;\n\t\tif (server) {\n\t\t\tawait server.close();\n\t\t\tthis.log.debug(\"API stopped\");\n\t\t}\n\t}\n\n\t/**\n\t * Is called when adapter shuts down - callback has to be called under any circumstances!\n\t *\n\t * @param callback - Callback function\n\t */\n\tprivate onUnload(callback: () => void): void {\n\t\ttry {\n\t\t\t// Timers created with this.setTimeout/this.setInterval are cleared automatically on unload.\n\t\t\tvoid this.stopApi()\n\t\t\t\t.catch(error => this.log.warn(`stopping the API failed: ${(error as Error).message}`))\n\t\t\t\t.finally(() => {\n\t\t\t\t\tthis.db?.close();\n\t\t\t\t\tthis.db = null;\n\t\t\t\t\tcallback();\n\t\t\t\t});\n\t\t} catch (error) {\n\t\t\tthis.log.error(`Error during unloading: ${(error as Error).message}`);\n\t\t\tcallback();\n\t\t}\n\t}\n\n\t/**\n\t * Runs the automation rules that are due.\n\t *\n\t * A rule acts at most once per employee and period \u2014 the local date, or the ISO week for a weekly rule;\n\t * `automation_runs` holds that decision, so a restart\n\t * cannot punch twice. The run is noted **before** the action (a double punch would be worse than a missed\n\t * reminder) and taken back when the action fails, so the next minute retries it.\n\t */\n\tprivate runAutomationRules(): void {\n\t\tconst services = this.services;\n\t\tif (!services) {\n\t\t\treturn;\n\t\t}\n\n\t\tlet rules: AutomationRuleRecord[] = [];\n\t\ttry {\n\t\t\trules = services.automations.list();\n\t\t} catch (error) {\n\t\t\tthis.log.warn(`automation rules could not be read: ${(error as Error).message}`);\n\t\t\treturn;\n\t\t}\n\t\tif (rules.length === 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst nowUtc = Math.floor(Date.now() / 1000);\n\t\tlet acted = false;\n\n\t\tfor (const rule of rules) {\n\t\t\tconst candidates =\n\t\t\t\trule.userId === null\n\t\t\t\t\t? services.users.list()\n\t\t\t\t\t: [services.users.findById(rule.userId)].filter((user): user is UserRecord => user !== null);\n\n\t\t\tfor (const user of candidates) {\n\t\t\t\tif (!user.isActive) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst local = localDateTime(nowUtc, user.timezone);\n\t\t\t\t// a weekly rule is guarded per ISO week, a daily rule per local date\n\t\t\t\tconst period = runPeriod(rule.repeat, local.date);\n\t\t\t\tif (services.automations.hasRun({ ruleId: rule.id, userId: user.id, period })) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst punches = services.entries\n\t\t\t\t\t.listByDate(user.id, local.date)\n\t\t\t\t\t.map(entry => ({ tsUtc: entry.tsUtc, direction: entry.direction }));\n\t\t\t\tconst block = workBlock(punches, nowUtc);\n\t\t\t\tconst decision = evaluateAutomation(rule, {\n\t\t\t\t\tlocalDate: local.date,\n\t\t\t\t\tweekday: isoWeekday(local.date),\n\t\t\t\t\tminuteOfDay: local.minutes,\n\t\t\t\t\thasOpenEntry: block.hasOpenEntry,\n\t\t\t\t\tblockMinutes: block.blockMinutes,\n\t\t\t\t\talreadyRan: false,\n\t\t\t\t});\n\t\t\t\tif (!decision.fire) {\n\t\t\t\t\tthis.log.debug(`automation ${rule.id} (${user.displayName}): ${decision.reason}`);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst action = this.automationAction(rule, block.blockMinutes);\n\t\t\t\tif (\n\t\t\t\t\t!services.automations.recordRun({\n\t\t\t\t\t\truleId: rule.id,\n\t\t\t\t\t\tuserId: user.id,\n\t\t\t\t\t\tperiod,\n\t\t\t\t\t\taction,\n\t\t\t\t\t\tnow: nowUtc,\n\t\t\t\t\t})\n\t\t\t\t) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\ttry {\n\t\t\t\t\tlet punched: CommandResult | null = null;\n\t\t\t\t\tif (rule.kind === \"clockOut\" || rule.kind === \"clockIn\") {\n\t\t\t\t\t\tpunched = punchEmployee(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tentries: services.entries,\n\t\t\t\t\t\t\t\tusers: services.users,\n\t\t\t\t\t\t\t\tsettings: services.settings,\n\t\t\t\t\t\t\t\taggregation: services.aggregation,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{ userId: user.id, note: `auto.${rule.kind}`, now: nowUtc },\n\t\t\t\t\t\t);\n\t\t\t\t\t\tthis.log.info(`automation ${rule.id} for ${user.displayName}: ${punched.message}`);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.log.info(`automation ${rule.id} for ${user.displayName}: ${action}`);\n\t\t\t\t\t}\n\t\t\t\t\tacted = true;\n\t\t\t\t\tthis.events?.publish({\n\t\t\t\t\t\ttype: `automation.${rule.kind}`,\n\t\t\t\t\t\tatUtc: nowUtc,\n\t\t\t\t\t\tuserId: user.id,\n\t\t\t\t\t\t// the direction belongs into the event when the rule wrote a punch (checker: `events.lastDirection`)\n\t\t\t\t\t\tdata: { ruleId: rule.id, action, reason: decision.reason, direction: punched?.direction },\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// give the next check a chance instead of losing the period\n\t\t\t\t\tservices.automations.forgetRun({ ruleId: rule.id, userId: user.id, period });\n\t\t\t\t\tthis.log.warn(`automation ${rule.id} for ${user.displayName} failed: ${(error as Error).message}`);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (acted) {\n\t\t\t// a punch written by a rule changes the published figures\n\t\t\tthis.scheduleStateRefresh();\n\t\t}\n\t}\n\n\t/**\n\t * Short description of what a rule did (the log entry of `automation_runs`).\n\t *\n\t * @param rule - rule that fired\n\t * @param blockMinutes - length of the running work block, `null` when the employee is not clocked in\n\t * @returns text for the log\n\t */\n\tprivate automationAction(rule: AutomationRuleRecord, blockMinutes: number | null): string {\n\t\tif (rule.kind === \"clockOut\") {\n\t\t\treturn \"clocked out\";\n\t\t}\n\t\tif (rule.kind === \"clockIn\") {\n\t\t\treturn \"clocked in\";\n\t\t}\n\t\tif (rule.kind === \"breakReminder\") {\n\t\t\treturn `reminded after ${blockMinutes ?? 0} min without a break`;\n\t\t}\n\t\treturn \"reported a missing punch\";\n\t}\n\n\t/**\n\t * Republishes the figures shortly after data changed.\n\t *\n\t * A punch from the web app, the terminal or a badge arrives through the API of this very process, so without\n\t * this the states would only change with the five minute timer. One action often produces several events, so\n\t * the refresh is collected for a moment instead of running once per event.\n\t */\n\tprivate scheduleStateRefresh(): void {\n\t\tif (this.stateRefreshTimer) {\n\t\t\treturn;\n\t\t}\n\t\tthis.stateRefreshTimer = this.setTimeout(() => {\n\t\t\tthis.stateRefreshTimer = undefined;\n\t\t\tvoid this.refreshStates();\n\t\t}, 1000);\n\t}\n\n\t/**\n\t * Publishes the newest event of the instance into the state tree.\n\t *\n\t * A notification adapter, a dashboard or a Blockly script then only has to watch `events.*` instead of\n\t * following the live stream of the web app.\n\t *\n\t * @param event - event published by the API\n\t */\n\tprivate async publishEventState(event: ApiEvent): Promise<void> {\n\t\tconst services = this.services;\n\t\tif (!services) {\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tconst data = event.data ?? {};\n\t\t\tconst user =\n\t\t\t\tevent.userId === null || event.userId === undefined ? null : services.users.findById(event.userId);\n\t\t\tawait publishEventSnapshot(this, {\n\t\t\t\ttype: event.type,\n\t\t\t\tatUtc: event.atUtc,\n\t\t\t\tuserName: user?.displayName ?? \"\",\n\t\t\t\tdirection: typeof data.direction === \"string\" ? data.direction : \"\",\n\t\t\t\tsource: typeof data.source === \"string\" ? data.source : \"\",\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tthis.log.debug(`event state could not be published: ${(error as Error).message}`);\n\t\t}\n\t}\n\n\t/**\n\t * Publishes a punch on the event bus, so it reaches `events.*` and the connected web clients.\n\t *\n\t * A punch can be written from several places \u2014 the command states, a `sendTo` message, a trigger rule, an\n\t * automation rule and the presence state. Every one of them passes its result through here; a path that forgets\n\t * it leaves the event states (`lastType`, `lastUser`, \u2026) on the punch before.\n\t *\n\t * @param event - event built from the punch, `null` when nothing was written\n\t */\n\tprivate publishPunch(event: ApiEvent | null): void {\n\t\tif (event) {\n\t\t\tthis.events?.publish(event);\n\t\t}\n\t}\n\n\t/**\n\t * Is called when another adapter sends a message (`sendTo`).\n\t *\n\t * @param obj - message object of the adapter framework\n\t */\n\tprivate onMessage(obj: ioBroker.Message): void {\n\t\tvoid (async (): Promise<void> => {\n\t\t\tconst services = this.services;\n\t\t\tif (!services) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst answer = await handleMessage(\n\t\t\t\t\t{\n\t\t\t\t\t\tusers: services.users,\n\t\t\t\t\t\tentries: services.entries,\n\t\t\t\t\t\tabsences: services.absences,\n\t\t\t\t\t\tsettings: services.settings,\n\t\t\t\t\t\taggregation: services.aggregation,\n\t\t\t\t\t\tsync: services.sync,\n\t\t\t\t\t\tbackup: services.backup,\n\t\t\t\t\t\tversion: this.version,\n\t\t\t\t\t},\n\t\t\t\t\tobj.command,\n\t\t\t\t\tobj.message,\n\t\t\t\t);\n\t\t\t\tthis.log.info(`message ${obj.command}: ${answer.message}`);\n\t\t\t\t// a punch answered by a message reaches `events.*` and the web app like every other one\n\t\t\t\tconst direction = answer.data?.direction;\n\t\t\t\tconst userId = answer.data?.userId;\n\t\t\t\tif (typeof direction === \"string\" && typeof userId === \"number\") {\n\t\t\t\t\tthis.publishPunch({\n\t\t\t\t\t\ttype: \"punch\",\n\t\t\t\t\t\tatUtc: Math.floor(Date.now() / 1000),\n\t\t\t\t\t\tuserId,\n\t\t\t\t\t\tdata: { direction, source: `sendTo.${obj.command}` },\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tif (obj.callback) {\n\t\t\t\t\tthis.sendTo(obj.from, obj.command, answer, obj.callback);\n\t\t\t\t}\n\t\t\t\tawait this.refreshStates();\n\t\t\t} catch (error) {\n\t\t\t\tconst message = (error as Error).message;\n\t\t\t\tthis.log.warn(`message ${obj.command} failed: ${message}`);\n\t\t\t\tif (obj.callback) {\n\t\t\t\t\tthis.sendTo(obj.from, obj.command, { ok: false, message }, obj.callback);\n\t\t\t\t}\n\t\t\t}\n\t\t})();\n\t}\n\n\t/**\n\t * Is called if a subscribed state changes\n\t *\n\t * @param id - State ID\n\t * @param state - State object\n\t */\n\tprivate onStateChange(id: string, state: ioBroker.State | null | undefined): void {\n\t\ttry {\n\t\t\tif (!state) {\n\t\t\t\t// deletions carry no value\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// a watched state of another adapter is handled first: such a state is acknowledged by its own\n\t\t\t// adapter, and what counts there is the change of the value\n\t\t\tif (this.handleTriggerState(id, state)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (state.ack) {\n\t\t\t\t// Ignore acknowledged (status) states of this instance\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst prefix = `${this.namespace}.`;\n\t\t\tconst localId = id.startsWith(prefix) ? id.slice(prefix.length) : id;\n\t\t\tif (localId.startsWith(\"commands.\")) {\n\t\t\t\tvoid this.runCommand(localId, state.val);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (parsePresenceStateId(localId) !== null) {\n\t\t\t\tvoid this.runPresence(localId, state.val);\n\t\t\t\treturn;\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthis.log.error(`Error in onStateChange for ${id}: ${(error as Error).message}`);\n\t\t}\n\t}\n}\n\nif (require.main !== module) {\n\t// Export the constructor in compact mode\n\tmodule.exports = (options: Partial<utils.AdapterOptions> | undefined) => new TimeTracker(options);\n} else {\n\t// otherwise start the instance directly\n\t(() => new TimeTracker())();\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["/*\n * ioBroker adapter \"time-tracker\" \u2013 time tracking\n * Scaffolded with @iobroker/create-adapter v3.1.5\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { hostname } from \"node:os\";\nimport * as utils from \"@iobroker/adapter-core\";\nimport { currentSchemaVersion, openAndMigrate, type Db } from \"./lib/db/database\";\nimport { seed } from \"./lib/db/seed\";\nimport type { HolidayCountry } from \"./lib/domain/holidays\";\nimport { createAbsencesRepository } from \"./lib/db/repositories/absences\";\nimport { createDayNotesRepository, type DayNotesRepository } from \"./lib/db/repositories/dayNotes\";\nimport { createEntriesRepository } from \"./lib/db/repositories/entries\";\nimport { createHolidaysRepository } from \"./lib/db/repositories/holidays\";\nimport { createPayoutsRepository } from \"./lib/db/repositories/payouts\";\nimport { createRulesRepository } from \"./lib/db/repositories/rules\";\nimport { createSettingsRepository } from \"./lib/db/repositories/settings\";\nimport { createUsersRepository, type UserRecord, type UsersRepository } from \"./lib/db/repositories/users\";\nimport {\n\tcreateAutomationsRepository,\n\ttype AutomationRuleRecord,\n\ttype AutomationsRepository,\n} from \"./lib/db/repositories/automations\";\nimport { createTerminalsRepository } from \"./lib/db/repositories/terminals\";\nimport { createRfidRepository } from \"./lib/db/repositories/rfid\";\nimport {\n\tcreateTriggersRepository,\n\ttype TriggerRuleRecord,\n\ttype TriggersRepository,\n} from \"./lib/db/repositories/triggers\";\nimport type { EntriesRepository } from \"./lib/db/repositories/entries\";\nimport type { AbsencesRepository } from \"./lib/db/repositories/absences\";\nimport type { SettingsRepository } from \"./lib/db/repositories/settings\";\nimport { createAggregationService, type AggregationService } from \"./lib/services/aggregation\";\nimport { createAuthService } from \"./lib/services/auth\";\nimport { ensureFirstAdministrator } from \"./lib/services/firstAdministrator\";\nimport { SECRET_FILE_NAME, resolveSessionSecret } from \"./lib/services/sessionSecret\";\nimport { createClosingService, type ClosingService } from \"./lib/services/closing\";\nimport {\n\tapplyPendingRestore,\n\tcreateBackupService,\n\trecordRestore,\n\ttype AppliedRestore,\n\ttype BackupService,\n} from \"./lib/services/backup\";\nimport { createSyncService, type SyncService } from \"./lib/services/sync\";\nimport {\n\tabsenceEvents,\n\tabsenceFeed,\n\tAPI_PREFIX,\n\tcalendarDocument,\n\tcompanyFeedUrl,\n\tCOMPANY_CALENDAR_NAME,\n} from \"./lib/services/calendar\";\nimport { calendarFilePath, calendarMount, ensureCalendarFolder, writeCalendarFile } from \"./lib/adapter/calendarFile\";\nimport {\n\tCOMMAND_IDS,\n\tcreateCalendarStates,\n\tcreateCommandStates,\n\tcreateCompanyStates,\n\tcreateEventStates,\n\tcreateInfoStates,\n\tpublishAllUserStates,\n\tpublishCalendarSnapshot,\n\tpublishCompanySnapshot,\n\tpublishEventSnapshot,\n\treadCompanySnapshot,\n} from \"./lib/adapter/states\";\nimport { PRESENCE_SUFFIX, handlePresenceState, parsePresenceStateId, presenceEvent } from \"./lib/adapter/presence\";\nimport { handleCommand, punchEmployee, punchEvent, type CommandResult } from \"./lib/adapter/commands\";\nimport { evaluateTrigger, triggerText } from \"./lib/adapter/triggers\";\nimport { evaluateAutomation, isoWeekday, runPeriod, workBlock } from \"./lib/adapter/automation\";\nimport { handleMessage } from \"./lib/adapter/messages\";\nimport { createApi, MAX_BACKUP_UPLOAD_BYTES } from \"./lib/web/api\";\nimport type { ApiEvent, EventBus } from \"./lib/web/events\";\nimport { startWebServer, type WebServer } from \"./lib/web/server\";\nimport { localDateTime } from \"./lib/util/time\";\nimport { createStaticHandler } from \"./lib/web/static\";\n\nconst SUPPORTED_COUNTRIES: HolidayCountry[] = [\"CH\", \"DE\", \"AT\"];\n\n/** How often expired sessions are removed (minutes). */\nconst SESSION_PURGE_MINUTES = 30;\n\n/** How often the published figures are refreshed (minutes). */\nconst STATE_REFRESH_MINUTES = 5;\n\n/** How old the newest backup may be before the daily check writes a new one. */\nconst BACKUP_MAX_AGE_HOURS = 20;\n\n/** How often the automation rules are checked (minutes). */\nconst AUTOMATION_CHECK_MINUTES = 1;\n\n/** Name of the file that holds a generated badge (HMAC) secret next to the database. */\nconst TAG_SECRET_FILE_NAME = \"tag-secret\";\n\n/**\n * Events of the API that change the published figures.\n *\n * A punch or a correction arrives through the API of this very process, so nothing would republish the states\n * until the five minute timer fires; these events say exactly when data changed.\n */\nconst FIGURES_EVENT_TYPES = [\"punch\", \"terminal.punch\", \"rfid.scan\", \"entry.update\", \"entry.delete\", \"absence.change\"];\n\n/** Services created at startup. */\ninterface AdapterServices {\n\tusers: UsersRepository;\n\tentries: EntriesRepository;\n\tabsences: AbsencesRepository;\n\tdayNotes: DayNotesRepository;\n\tsettings: SettingsRepository;\n\taggregation: AggregationService;\n\tsync: SyncService;\n\tclosing: ClosingService;\n\tbackup: BackupService;\n\ttriggers: TriggersRepository;\n\tautomations: AutomationsRepository;\n}\n\nclass TimeTracker extends utils.Adapter {\n\tprivate db: Db | null = null;\n\tprivate webServer: WebServer | null = null;\n\tprivate services: AdapterServices | null = null;\n\t/** Bus of the API; `null` until the API is created */\n\tprivate events: EventBus | null = null;\n\t/** Last value seen per watched trigger state: only a change fires a rule */\n\tprivate readonly triggerValues = new Map<string, string>();\n\t/** Trigger states the adapter currently subscribes to */\n\tprivate readonly triggerStates = new Set<string>();\n\t/** Pending refresh of the figures after a change through the API */\n\tprivate stateRefreshTimer: ioBroker.Timeout | undefined = undefined;\n\n\t/** A refresh tick that is still running; the next tick is skipped instead of piling up on it. */\n\tprivate stateRefreshRunning = false;\n\n\t/** A backup run that is still running; the hourly check must not start a second one. */\n\tprivate backupRunning = false;\n\n\tpublic constructor(options: Partial<utils.AdapterOptions> = {}) {\n\t\tsuper({\n\t\t\t...options,\n\t\t\tname: \"time-tracker\",\n\t\t});\n\t\tthis.on(\"ready\", this.onReady.bind(this));\n\t\tthis.on(\"stateChange\", this.onStateChange.bind(this));\n\t\t// this.on(\"objectChange\", this.onObjectChange.bind(this));\n\t\t// scripts, Blockly and other adapters reach the instance through `sendTo` messages\n\t\tthis.on(\"message\", this.onMessage.bind(this));\n\t\tthis.on(\"unload\", this.onUnload.bind(this));\n\t}\n\n\t/** Database file: configured path or `<adapter instance data dir>/time-tracker.sqlite`. */\n\tprivate databaseFile(): string {\n\t\tconst configured = (this.config.dbPath ?? \"\").trim();\n\t\tif (configured) {\n\t\t\treturn path.resolve(configured);\n\t\t}\n\n\t\tconst dir = utils.getAbsoluteInstanceDataDir(this);\n\t\tfs.mkdirSync(dir, { recursive: true });\n\t\treturn path.join(dir, \"time-tracker.sqlite\");\n\t}\n\n\t/** Configured holiday country, falling back to Switzerland. */\n\tprivate holidayCountry(): HolidayCountry {\n\t\tconst configured = (this.config.holidayCountry ?? \"\").toUpperCase() as HolidayCountry;\n\t\treturn SUPPORTED_COUNTRIES.includes(configured) ? configured : \"CH\";\n\t}\n\n\t/**\n\t * Is called when databases are connected and adapter received configuration.\n\t */\n\tprivate async onReady(): Promise<void> {\n\t\ttry {\n\t\t\t// Reset the connection indicator during startup\n\t\t\tawait this.setState(\"info.connection\", false, true);\n\n\t\t\tconst file = this.databaseFile();\n\t\t\t// A restore that the administration queued is applied before the database is opened: the swap needs a\n\t\t\t// closed file, and that is the only moment the adapter can offer it (see the backup service).\n\t\t\tlet applied: AppliedRestore | null = null;\n\t\t\ttry {\n\t\t\t\tapplied = applyPendingRestore(file);\n\t\t\t\tif (applied) {\n\t\t\t\t\tthis.log.info(\n\t\t\t\t\t\t`restored the database from ${applied.restored.name}: ${applied.restored.users} users, ${applied.restored.entries} punches, previous file ${applied.previous ?? \"(none)\"}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\t// the queued files stay in place, so a working backup can be handed in afterwards\n\t\t\t\tthis.log.error(`the queued restore was refused: ${(error as Error).message}`);\n\t\t\t}\n\t\t\tthis.db = openAndMigrate(file, message => this.log.debug(message));\n\t\t\tif (applied) {\n\t\t\t\t// the audit entry can only be written once the restored database is open\n\t\t\t\trecordRestore(this.db, applied.restored, applied.actorId);\n\t\t\t}\n\n\t\t\tconst year = new Date().getUTCFullYear();\n\t\t\tconst country = this.holidayCountry();\n\t\t\tconst result = seed(this.db, { holidayCountry: country, holidayYears: [year, year + 1] });\n\t\t\tthis.log.info(\n\t\t\t\t`database ready at ${file} (${result.permissions} permissions, ${result.holidays} holidays added for ${country})`,\n\t\t\t);\n\n\t\t\t// Without an account nobody can log in, so the first administrator is created here (Phase 2 of the\n\t\t\t// specification: \"Standard-Admin, Passwort mit Pflichtwechsel\").\n\t\t\tthis.ensureAdministrator(this.db);\n\n\t\t\t// the settings of the admin UI win over the stored values (before the services are built)\n\t\t\tthis.applyConfiguration(createSettingsRepository(this.db));\n\n\t\t\t// NOTE: The implementation follows the internal specification (not part of this repository):\n\t\t\t// Phase 1\u20133 \u2013 storage, punch logic, target time, breaks, overtime, vacation\n\t\t\t// Phase 4 \u2013 REST API on an own port (router, sessions, RBAC)\n\t\t\t// Phase 5 \u2013 PWA delivered from www/\n\t\t\t// Phase 8 \u2013 publish aggregates as states and react to command.*\n\n\t\t\tawait this.startApi();\n\t\t\t// creates the command and info states, so the instance information can be published afterwards\n\t\t\tawait this.subscribeCommands();\n\t\t\t// watches the states the trigger rules name (fingerprint reader, button, door contact, \u2026)\n\t\t\tawait this.refreshTriggerSubscriptions();\n\t\t\t// the automation rules look at the clock, so they are checked once a minute\n\t\t\tthis.runAutomationRules();\n\t\t\tthis.setInterval(() => this.runAutomationRules(), AUTOMATION_CHECK_MINUTES * 60 * 1000);\n\t\t\tawait this.publishInstanceInfo();\n\t\t\t// Files below `files/` hang on a **mount point** \u2014 an object of type `meta`. Only there writes the file API\n\t\t\t// of the adapter; below the instance namespace itself the object database refuses with\n\t\t\t// `time-tracker.0 is not an object of type \"meta\"` (D11). Creating it here means the folder is in the file\n\t\t\t// manager from the first start on \u2014 before the first calendar was written and also on an instance that was\n\t\t\t// installed earlier.\n\t\t\ttry {\n\t\t\t\tawait ensureCalendarFolder(this, this.namespace);\n\t\t\t} catch (error) {\n\t\t\t\tthis.log.warn(\n\t\t\t\t\t`the mount point '${calendarMount(this.namespace)}' could not be prepared: ${(error as Error).message}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tawait this.refreshStates();\n\n\t\t\t// figures are refreshed regularly (the timer is cleared automatically on unload). A tick that finds the\n\t\t\t// previous run still busy is skipped: a slow run must not pile a second one on top of itself.\n\t\t\tthis.setInterval(\n\t\t\t\t() => {\n\t\t\t\t\tif (this.stateRefreshRunning) {\n\t\t\t\t\t\tthis.log.debug(`state refresh still running, skipping this ${STATE_REFRESH_MINUTES} min tick`);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tthis.stateRefreshRunning = true;\n\t\t\t\t\tvoid Promise.allSettled([this.publishInstanceInfo(), this.refreshStates()]).then(results => {\n\t\t\t\t\t\tthis.stateRefreshRunning = false;\n\t\t\t\t\t\tfor (const result of results) {\n\t\t\t\t\t\t\tif (result.status === \"rejected\") {\n\t\t\t\t\t\t\t\tthis.log.warn(`state refresh failed: ${(result.reason as Error).message}`);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t\tSTATE_REFRESH_MINUTES * 60 * 1000,\n\t\t\t);\n\n\t\t\t// one backup per day: the check runs every hour, so a missed run is caught up after a restart. A run that\n\t\t\t// is still going (a big database, a slow disk) blocks the next check instead of starting a second copy.\n\t\t\tawait this.runScheduledBackup();\n\t\t\tthis.setInterval(\n\t\t\t\t() => {\n\t\t\t\t\tif (this.backupRunning) {\n\t\t\t\t\t\tthis.log.debug(\"scheduled backup still running, skipping this hour\");\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tthis.backupRunning = true;\n\t\t\t\t\tvoid this.runScheduledBackup()\n\t\t\t\t\t\t.catch(error => this.log.warn(`scheduled backup failed: ${(error as Error).message}`))\n\t\t\t\t\t\t.finally(() => {\n\t\t\t\t\t\t\tthis.backupRunning = false;\n\t\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t\t60 * 60 * 1000,\n\t\t\t);\n\n\t\t\t// Service is ready\n\t\t\tawait this.setState(\"info.connection\", true, true);\n\t\t} catch (error) {\n\t\t\tconst message = (error as Error).message;\n\t\t\tthis.log.error(`Startup failed: ${message}`);\n\t\t\tawait this.setState(\"info.lastError\", message, true);\n\t\t}\n\t}\n\n\t/**\n\t * Creates the first administrator when the instance has none.\n\t *\n\t * The decision itself sits in `services/firstAdministrator.ts` (it is unit tested there); this method only\n\t * reports the result. Every start says what happened, so \u201CI set a start password and nothing appears in the\n\t * log\u201D cannot happen again \u2014 see `docs/entscheidungen.md`, D15.\n\t *\n\t * @param db - open database handle\n\t */\n\tprivate ensureAdministrator(db: Db): void {\n\t\tconst outcome = ensureFirstAdministrator({\n\t\t\tusers: createUsersRepository(db),\n\t\t\tlogin: this.config.adminLogin,\n\t\t\tpassword: this.config.adminPassword,\n\t\t\ttimezone: this.config.timezone,\n\t\t});\n\n\t\tswitch (outcome.action) {\n\t\t\tcase \"created\":\n\t\t\t\tif (outcome.generated) {\n\t\t\t\t\tthis.log.warn(\n\t\t\t\t\t\t`administrator \"${outcome.login}\" created with the start password \"${outcome.password}\" - change it at the first login`,\n\t\t\t\t\t);\n\t\t\t\t} else {\n\t\t\t\t\tthis.log.info(\n\t\t\t\t\t\t`administrator \"${outcome.login}\" created with the configured start password - it has to be changed at the first login`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\tcase \"start_password_reset\":\n\t\t\t\tthis.log.info(\n\t\t\t\t\t`administrator \"${outcome.login}\" exists already and still has its start password - it was set from the instance settings now, it has to be changed at the first login`,\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\tcase \"start_password_unchanged\":\n\t\t\t\tthis.log.debug(`administrator \"${outcome.login}\" carries the configured start password already`);\n\t\t\t\treturn;\n\t\t\tcase \"password_changed\":\n\t\t\t\tthis.log.warn(\n\t\t\t\t\t`the configured start password is not used: administrator \"${outcome.login}\" changed his password already (empty the field \"Start password of the first administrator\" to silence this)`,\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\tcase \"existing\":\n\t\t\t\tthis.log.debug(\n\t\t\t\t\t`administrator \"${outcome.login}\" exists already - no start password is configured, so none is generated`,\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\tdefault:\n\t\t\t\tthis.log.warn(`the administrator \"${outcome.login}\" could not be created: ${outcome.reason}`);\n\t\t}\n\t}\n\n\t/**\n\t * Copies the instance configuration into the stored settings and publishes the instance information.\n\t *\n\t * The ioBroker admin UI is where an operator configures the instance, so a configured value wins over what\n\t * is stored in the database; `PUT /api/settings` stays for the keys the admin UI does not offer. Empty\n\t * fields are ignored, so clearing a field never wipes a stored setting.\n\t *\n\t * @param settings - instance settings\n\t */\n\tprivate applyConfiguration(settings: SettingsRepository): void {\n\t\tconst text = (value: string | undefined): string | null => (value?.trim() ? value.trim() : null);\n\t\tconst number = (value: number | undefined): string | null =>\n\t\t\ttypeof value === \"number\" && Number.isFinite(value) ? String(value) : null;\n\t\tconst flag = (value: boolean | undefined): string | null => (value === undefined ? null : value ? \"1\" : \"0\");\n\n\t\tconst mapped: [string, string | null][] = [\n\t\t\t[\"holiday_country\", text(this.config.holidayCountry)?.toUpperCase() ?? null],\n\t\t\t[\"timezone\", text(this.config.timezone)],\n\t\t\t[\"default_language\", text(this.config.defaultLanguage)],\n\t\t\t[\"edit_window_days\", number(this.config.editWindowDays)],\n\t\t\t[\"quick_round_minutes\", number(this.config.quickRoundMinutes)],\n\t\t\t[\"session_ttl_minutes\", number(this.config.sessionTtlMinutes)],\n\t\t\t[\"backup_retention_days\", number(this.config.backupRetentionDays)],\n\t\t\t[\"absence_calc_until_today\", flag(this.config.absenceCalcUntilToday)],\n\t\t\t[\"absence_deduct_worktime\", flag(this.config.absenceDeductWorktime)],\n\t\t];\n\n\t\tconst now = Math.floor(Date.now() / 1000);\n\t\tfor (const [key, value] of mapped) {\n\t\t\tif (value !== null && settings.get(key) !== value) {\n\t\t\t\tsettings.set(key, value, null, now);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Publishes version, database size and schema version as states (specification 5.1).\n\t *\n\t * The objects are created by `createInfoStates`, so this runs after the command states were set up.\n\t */\n\tprivate async publishInstanceInfo(): Promise<void> {\n\t\tawait this.setState(\"info.version\", this.version ?? \"0.0.0\", true);\n\t\tconst file = this.db ? this.databaseFile() : \"\";\n\t\tawait this.setState(\"info.dbSizeBytes\", file && fs.existsSync(file) ? fs.statSync(file).size : 0, true);\n\t\tif (this.db) {\n\t\t\tawait this.setState(\"info.schemaVersion\", String(currentSchemaVersion(this.db)), true);\n\t\t}\n\t}\n\n\t/**\n\t * Secret used to sign CSRF tokens. A value from the instance settings wins; without one the adapter generates a\n\t * secret on the first start and stores it next to the database, so a restart keeps the CSRF tokens of clients\n\t * that are already open valid.\n\t */\n\tprivate sessionSecret(): string {\n\t\tconst resolved = resolveSessionSecret({\n\t\t\tconfigured: this.config.sessionSecret,\n\t\t\tfile: path.join(path.dirname(this.databaseFile()), SECRET_FILE_NAME),\n\t\t});\n\n\t\tif (resolved.source === \"configured\") {\n\t\t\tthis.log.debug(\"session secret taken from the instance settings\");\n\t\t} else if (resolved.source === \"stored\") {\n\t\t\tthis.log.debug(`session secret taken from ${resolved.file}`);\n\t\t} else if (resolved.file) {\n\t\t\tthis.log.info(\n\t\t\t\t`no session secret configured - generated one and stored it at ${resolved.file}, so sessions and CSRF tokens survive restarts`,\n\t\t\t);\n\t\t} else {\n\t\t\tthis.log.warn(\n\t\t\t\t`no session secret configured and it could not be stored (${resolved.error ?? \"unknown\"}) - a temporary one is used, so all sessions end with the next restart`,\n\t\t\t);\n\t\t}\n\n\t\treturn resolved.secret;\n\t}\n\n\t/**\n\t * Secret that signs the badge/NFC links.\n\t *\n\t * A value from the instance settings wins. Without one a secret is generated on the first start and stored next\n\t * to the database \u2014 otherwise a fresh installation could not create a badge at all and the administration would\n\t * only report \u201Cnot configured\u201D.\n\t */\n\tprivate tagSecret(): string {\n\t\tconst resolved = resolveSessionSecret({\n\t\t\tconfigured: this.config.hmacSecret,\n\t\t\tfile: path.join(path.dirname(this.databaseFile()), TAG_SECRET_FILE_NAME),\n\t\t});\n\n\t\tif (resolved.source === \"configured\") {\n\t\t\tthis.log.debug(\"badge link secret taken from the instance settings\");\n\t\t} else if (resolved.source === \"stored\") {\n\t\t\tthis.log.debug(`badge link secret taken from ${resolved.file}`);\n\t\t} else if (resolved.file) {\n\t\t\tthis.log.info(\n\t\t\t\t`no badge link secret configured - generated one and stored it at ${resolved.file}, so badges stay valid across restarts`,\n\t\t\t);\n\t\t} else {\n\t\t\tthis.log.warn(\n\t\t\t\t`no badge link secret configured and it could not be stored (${resolved.error ?? \"unknown\"}) - a temporary one is used, so badges stop working with the next restart`,\n\t\t\t);\n\t\t}\n\n\t\treturn resolved.secret;\n\t}\n\n\t/**\n\t * Builds the services and starts the HTTP server on the configured port.\n\t *\n\t * Binding can fail (port in use); that must never stop the adapter, so the failure is only logged.\n\t */\n\tprivate async startApi(): Promise<void> {\n\t\tconst db = this.db;\n\t\tif (!db) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst users = createUsersRepository(db);\n\t\tconst entries = createEntriesRepository(db);\n\t\tconst absences = createAbsencesRepository(db);\n\t\tconst dayNotes = createDayNotesRepository(db);\n\t\tconst holidays = createHolidaysRepository(db);\n\t\tconst rules = createRulesRepository(db);\n\t\tconst payouts = createPayoutsRepository(db);\n\t\tconst terminals = createTerminalsRepository(db);\n\t\tconst rfid = createRfidRepository(db);\n\t\tconst settings = createSettingsRepository(db);\n\t\tconst auth = createAuthService({\n\t\t\tdb,\n\t\t\tusers,\n\t\t\tsettings,\n\t\t\tsecret: this.sessionSecret(),\n\t\t\tdefaultTtlMinutes: this.config.sessionTtlMinutes || 720,\n\t\t});\n\t\tconst aggregation = createAggregationService({\n\t\t\tdb,\n\t\t\tusers,\n\t\t\tentries,\n\t\t\tabsences,\n\t\t\tholidays,\n\t\t\trules,\n\t\t\tsettings,\n\t\t});\n\t\tconst triggers = createTriggersRepository(db);\n\t\tconst automations = createAutomationsRepository(db);\n\t\tconst sync = createSyncService({ db, entries, users, aggregation });\n\t\tconst closing = createClosingService({ db, aggregation, payouts });\n\t\t// backups live next to the database file: `<data dir>/backups/time-tracker-<timestamp>.sqlite`\n\t\tconst backup = createBackupService({\n\t\t\tdb,\n\t\t\tdir: path.join(path.dirname(this.databaseFile()), \"backups\"),\n\t\t\tretentionDays: Math.max(0, Number(this.config.backupRetentionDays ?? 30) || 0),\n\t\t});\n\t\tconst api = createApi({\n\t\t\tdb,\n\t\t\tauth,\n\t\t\tusers,\n\t\t\tentries,\n\t\t\tabsences,\n\t\t\tdayNotes,\n\t\t\tholidays,\n\t\t\trules,\n\t\t\tpayouts,\n\t\t\tterminals,\n\t\t\trfid,\n\t\t\ttriggers,\n\t\t\tautomations,\n\t\t\taggregation,\n\t\t\tsync,\n\t\t\tsettings,\n\t\t\tbackup,\n\t\t\tkioskEnabled: this.config.kioskEnabled === true,\n\t\t\t// a reverse proxy in front is the normal case for HTTPS; without the switch the forwarded\n\t\t\t// headers are ignored, so a client cannot choose its own address or the HTTPS flag\n\t\t\ttrustProxy: this.config.trustProxy === true,\n\t\t\thmacSecret: this.tagSecret(),\n\t\t\tversion: this.version,\n\t\t\t// a saved trigger rule changes the states the adapter has to watch\n\t\t\tonTriggerRulesChanged: () => void this.refreshTriggerSubscriptions(),\n\t\t});\n\n\t\tthis.services = {\n\t\t\tusers,\n\t\t\tentries,\n\t\t\tabsences,\n\t\t\tdayNotes,\n\t\t\tsettings,\n\t\t\taggregation,\n\t\t\tsync,\n\t\t\tclosing,\n\t\t\tbackup,\n\t\t\ttriggers,\n\t\t\tautomations,\n\t\t};\n\t\tthis.events = api.events;\n\t\t// the newest event is mirrored into the state tree, so a notification only has to watch `events.*`;\n\t\t// a punch, a correction or an absence changes the figures, so they are republished right away\n\t\tapi.events.subscribe(event => {\n\t\t\tvoid this.publishEventState(event);\n\t\t\tif (FIGURES_EVENT_TYPES.includes(event.type)) {\n\t\t\t\tthis.scheduleStateRefresh();\n\t\t\t}\n\t\t});\n\t\tthis.log.debug(`API routes: ${api.routes().length}`);\n\n\t\t// the web interface is delivered from `www/` next to the compiled code (built by the PWA project);\n\t\t// without it the adapter still provides the API, e.g. for the terminal\n\t\tconst webDir = path.join(__dirname, \"..\", \"www\");\n\t\tconst staticFiles = fs.existsSync(path.join(webDir, \"index.html\"))\n\t\t\t? createStaticHandler({ root: webDir })\n\t\t\t: undefined;\n\t\tthis.log.info(\n\t\t\tstaticFiles\n\t\t\t\t? `web interface found at ${webDir}`\n\t\t\t\t: `no web interface at ${webDir} - only the API (and the terminal) is available`,\n\t\t);\n\n\t\ttry {\n\t\t\tthis.webServer = await startWebServer({\n\t\t\t\trouter: api.router,\n\t\t\t\tport: this.config.port || 8092,\n\t\t\t\tbind: this.config.bind || \"127.0.0.1\",\n\t\t\t\t// the same prefix the calendar link carries (`/api`) \u2014 the web app and its login live beside it\n\t\t\t\tapiPrefix: API_PREFIX,\n\t\t\t\tstaticFiles,\n\t\t\t\t// an uploaded backup is bigger than the default limit; the route carries the same bound\n\t\t\t\tmaxBodyBytes: MAX_BACKUP_UPLOAD_BYTES,\n\t\t\t\t// live events for the PWA and the terminal: `/api/stream?token=...`\n\t\t\t\tstream: {\n\t\t\t\t\tauth,\n\t\t\t\t\tevents: api.events,\n\t\t\t\t\tversion: this.version,\n\t\t\t\t\ttimers: {\n\t\t\t\t\t\t// the adapter's own timer functions: they are cleared with the adapter on unload\n\t\t\t\t\t\tsetInterval: (handler, milliseconds) => this.setInterval(handler, milliseconds),\n\t\t\t\t\t\tclearInterval: handle => this.clearInterval(handle as ioBroker.Interval),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tlog: {\n\t\t\t\t\tinfo: message => this.log.info(message),\n\t\t\t\t\twarn: message => this.log.warn(message),\n\t\t\t\t\terror: message => this.log.error(message),\n\t\t\t\t},\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tthis.log.error(\n\t\t\t\t`API could not be started on port ${this.config.port}: ${(error as Error).message} - the web interface and the terminal stay unavailable`,\n\t\t\t);\n\t\t\tthis.webServer = null;\n\t\t\treturn;\n\t\t}\n\n\t\t// expired sessions are removed regularly; the timer is cleared automatically on unload\n\t\tthis.setInterval(\n\t\t\t() => {\n\t\t\t\ttry {\n\t\t\t\t\tconst removed = auth.purge();\n\t\t\t\t\tif (removed > 0) {\n\t\t\t\t\t\tthis.log.debug(`removed ${removed} expired session(s)`);\n\t\t\t\t\t}\n\t\t\t\t} catch (error) {\n\t\t\t\t\tthis.log.warn(`session cleanup failed: ${(error as Error).message}`);\n\t\t\t\t}\n\t\t\t},\n\t\t\tSESSION_PURGE_MINUTES * 60 * 1000,\n\t\t);\n\t}\n\n\t/**\n\t * Creates the command states and subscribes to them.\n\t */\n\tprivate async subscribeCommands(): Promise<void> {\n\t\ttry {\n\t\t\tawait createCommandStates(this);\n\t\t\tawait createInfoStates(this);\n\t\t\tawait createCompanyStates(this);\n\t\t\tawait createEventStates(this);\n\t\t\tawait createCalendarStates(this);\n\t\t\tawait this.subscribeStatesAsync(\"commands.*\");\n\t\t\t// the presence switch of every employee: written by a script, a fingerprint reader or a dashboard\n\t\t\tawait this.subscribeStatesAsync(`users.*.${PRESENCE_SUFFIX}`);\n\t\t\tthis.log.debug(\"command states ready\");\n\t\t} catch (error) {\n\t\t\tthis.log.warn(`command states could not be created: ${(error as Error).message}`);\n\t\t}\n\t}\n\n\t/**\n\t * Writes a backup unless a recent one exists.\n\t *\n\t * The check runs hourly and once at start: a nightly backup therefore happens within an hour at the latest,\n\t * and an instance that was down for days does not write a burst of copies.\n\t */\n\tprivate async runScheduledBackup(): Promise<void> {\n\t\tconst backup = this.services?.backup;\n\t\tif (!backup) {\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tconst newest = backup.list()[0];\n\t\t\tconst ageHours = newest ? (Date.now() / 1000 - newest.createdAt) / 3600 : Number.POSITIVE_INFINITY;\n\t\t\tif (ageHours < BACKUP_MAX_AGE_HOURS) {\n\t\t\t\tthis.log.debug(`backup ${newest?.name ?? \"\"} is ${ageHours.toFixed(1)} h old, nothing to do`);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst created = backup.create({ actorId: null, reason: \"scheduled\" });\n\t\t\tthis.log.info(\n\t\t\t\t`backup ${created.backup.name} written (${created.backup.sizeBytes} bytes, ${created.backup.users} employees, ${created.backup.entries} punches), ${created.removed.length} old file(s) removed`,\n\t\t\t);\n\t\t\tawait this.announceBackup(created.backup.name, created.backup.createdAt);\n\t\t} catch (error) {\n\t\t\tthis.log.warn(`backup failed: ${(error as Error).message}`);\n\t\t}\n\t}\n\n\t/**\n\t * Publishes the instant of a written backup.\n\t *\n\t * @param name - file name of the backup\n\t * @param createdAt - instant the backup belongs to\n\t */\n\tprivate async announceBackup(name: string, createdAt: number): Promise<void> {\n\t\tawait this.setState(\"info.lastBackup\", createdAt, true);\n\t\tthis.events?.publish({\n\t\t\ttype: \"backup.create\",\n\t\t\tatUtc: createdAt,\n\t\t\tuserId: null,\n\t\t\tdata: { backup: name },\n\t\t});\n\t}\n\n\t/**\n\t * Applies a write on the presence state of an employee.\n\t *\n\t * `users.<id>.present` is the writable twin of `users.<id>.hasOpenEntry`: `true` opens an entry, `false` closes\n\t * it. Afterwards the figures of all employees are republished, so a dashboard sees the new state right away.\n\t *\n\t * @param id - state id without the instance prefix\n\t * @param value - value written by the user or a script\n\t */\n\tprivate async runPresence(id: string, value: ioBroker.StateValue): Promise<void> {\n\t\tconst services = this.services;\n\t\tif (!services) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tconst result = handlePresenceState(\n\t\t\t\t{\n\t\t\t\t\tentries: services.entries,\n\t\t\t\t\tusers: services.users,\n\t\t\t\t\taggregation: services.aggregation,\n\t\t\t\t},\n\t\t\t\tid,\n\t\t\t\tvalue,\n\t\t\t);\n\t\t\tif (result.ok) {\n\t\t\t\tthis.log.info(`presence ${id}: ${result.message}`);\n\t\t\t\t// A punch from the presence state is published like every other one: the subscription in `onReady`\n\t\t\t\t// mirrors it into `events.*` and tells the connected web clients.\n\t\t\t\tconst event = presenceEvent(result, Math.floor(Date.now() / 1000));\n\t\t\t\tif (event) {\n\t\t\t\t\tthis.events?.publish(event);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// a value the state does not understand, an unknown employee: worth a warning, not an error\n\t\t\t\tthis.log.warn(`presence ${id}: ${result.message}`);\n\t\t\t}\n\t\t\tawait this.refreshStates();\n\t\t} catch (error) {\n\t\t\tthis.log.error(`Error in runPresence for ${id}: ${(error as Error).message}`);\n\t\t}\n\t}\n\n\t/**\n\t * Publishes the figures of all employees.\n\t */\n\tprivate async refreshStates(): Promise<void> {\n\t\tconst services = this.services;\n\t\tif (!services) {\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tconst snapshots = await publishAllUserStates({\n\t\t\t\tport: this,\n\t\t\t\taggregation: services.aggregation,\n\t\t\t\tusers: services.users,\n\t\t\t\tsync: services.sync,\n\t\t\t});\n\t\t\tthis.log.debug(`published ${snapshots.length} employee state(s)`);\n\t\t\t// the company figures are derived from the same snapshots, so they never disagree\n\t\t\tawait publishCompanySnapshot(this, readCompanySnapshot(snapshots));\n\t\t\t// the target employee of the punch commands is mirrored into the state the user writes (acknowledged, so\n\t\t\t// the adapter ignores its own write \u2014 see `onStateChange`). `0` means \"the only employee\".\n\t\t\tawait this.setState(COMMAND_IDS.punchUserId, services.settings.getNumber(\"command_punch_user_id\", 0), true);\n\t\t} catch (error) {\n\t\t\tthis.log.warn(`states could not be published: ${(error as Error).message}`);\n\t\t}\n\n\t\t// The calendar is a file and not a state, so its failure is reported as what it is. Until 0.7.11 it ran inside\n\t\t// the block above: a write error of the file API then appeared as \u201Estates could not be published\" and hid the\n\t\t// real cause (the log of the acceptance test showed exactly that line \u2014 see D11).\n\t\ttry {\n\t\t\tawait this.publishCalendar();\n\t\t} catch (error) {\n\t\t\tthis.log.warn(`the calendar could not be written: ${(error as Error).message}`);\n\t\t}\n\t}\n\n\t/**\n\t * Writes the calendar of the instance and publishes what ioBroker needs.\n\t *\n\t * The `.ics` file is what the `ical` adapter reads as a **local source** \u2014 no URL, no token, no network \u2014 and what\n\t * a script can hand to another consumer. `calendar.absences` carries the same days as JSON for scripts and\n\t * dashboards. The subscription link is published only once the administration created it\n\t * (`commands.rotateCalendarToken`), because that link opens the absences of **all** employees to whoever holds it.\n\t */\n\tprivate async publishCalendar(): Promise<void> {\n\t\tconst services = this.services;\n\t\tif (!services) {\n\t\t\treturn;\n\t\t}\n\n\t\t// the same window the feed route uses: a year back and to the end of next year\n\t\tconst stamp = Math.floor(Date.now() / 1000);\n\t\tconst year = new Date(stamp * 1000).getUTCFullYear();\n\t\tconst from = `${year - 1}-01-01`;\n\t\tconst to = `${year + 1}-12-31`;\n\n\t\t// deactivated accounts stay out (`list()` returns the active ones)\n\t\tconst employees = new Map(\n\t\t\tservices.users\n\t\t\t\t.list()\n\t\t\t\t.map(user => [user.id, { id: user.id, displayName: user.displayName, login: user.login }]),\n\t\t);\n\t\tconst list = services.absences.allInRange(from, to);\n\n\t\t// The file lives below `files/<namespace>/<mount point>/` and is written through the file API of the adapter:\n\t\t// only that way is it a folder the file manager shows and a `web` instance hands out (D11) \u2014 a file written\n\t\t// with `fs` stays a private file. The absolute path is what `calendar.feedFile` carries for the `ical` adapter.\n\t\tconst file = calendarFilePath(utils.getAbsoluteDefaultDataDir(), this.namespace);\n\t\tconst document = calendarDocument(COMPANY_CALENDAR_NAME, absenceEvents(list, employees, true), stamp);\n\t\tawait writeCalendarFile(this, this.namespace, document);\n\n\t\tconst token = services.settings.get(\"calendar_token\");\n\t\tawait publishCalendarSnapshot(this, {\n\t\t\t// the host part is the name of this machine: an address that is reachable from the LAN and simple enough\n\t\t\t// to correct by hand (a reverse proxy or another address is a matter of the copy in the calendar app)\n\t\t\tfeedUrl: token ? companyFeedUrl(hostname(), this.config.port || 8092, token) : \"\",\n\t\t\tfeedFile: file,\n\t\t\tupdatedAt: stamp,\n\t\t\tabsences: JSON.stringify(absenceFeed(list, employees)),\n\t\t});\n\t}\n\n\t/**\n\t * Runs a command state and reports the outcome.\n\t *\n\t * @param id - state id without the instance prefix\n\t * @param value - value written by the user or a script\n\t */\n\tprivate async runCommand(id: string, value: ioBroker.StateValue): Promise<void> {\n\t\tconst services = this.services;\n\t\tconst db = this.db;\n\t\tif (!services || !db) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tconst result = handleCommand(\n\t\t\t\t{\n\t\t\t\t\tdb,\n\t\t\t\t\tentries: services.entries,\n\t\t\t\t\tusers: services.users,\n\t\t\t\t\tsettings: services.settings,\n\t\t\t\t\taggregation: services.aggregation,\n\t\t\t\t\tclosing: services.closing,\n\t\t\t\t\tbackup: services.backup,\n\t\t\t\t},\n\t\t\t\tid,\n\t\t\t\tvalue,\n\t\t\t);\n\t\t\tthis.log.info(`command ${id}: ${result.message}`);\n\t\t\t// a punch written by a command state lands in `events.*` and in the web app like every other one\n\t\t\tthis.publishPunch(punchEvent(result, `commands.${id}`, Math.floor(Date.now() / 1000)));\n\t\t\t// a closing is triggered from a state, not from a request: tell the connected clients about it\n\t\t\t// (only the period is published, never the figures of the employee it belongs to)\n\t\t\tif (id === COMMAND_IDS.closeMonth) {\n\t\t\t\tthis.events?.publish({\n\t\t\t\t\ttype: \"month.close\",\n\t\t\t\t\tatUtc: Math.floor(Date.now() / 1000),\n\t\t\t\t\tuserId: null,\n\t\t\t\t\tdata: { period: String(value ?? \"\") },\n\t\t\t\t});\n\t\t\t}\n\t\t\t// a manual backup reports its file the same way the scheduled one does\n\t\t\tif (id === COMMAND_IDS.backup && result.ok) {\n\t\t\t\tconst newest = services.backup.list()[0];\n\t\t\t\tif (newest) {\n\t\t\t\t\tawait this.announceBackup(newest.name, newest.createdAt);\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthis.log.warn(`command ${id} failed: ${(error as Error).message}`);\n\t\t}\n\n\t\t// buttons are stateless: always release them again\n\t\tif (\n\t\t\tid === COMMAND_IDS.punch ||\n\t\t\tid === COMMAND_IDS.quickPunch ||\n\t\t\tid === COMMAND_IDS.backup ||\n\t\t\tid === COMMAND_IDS.rotateCalendarToken\n\t\t) {\n\t\t\tawait this.setState(id, false, true);\n\t\t}\n\t\tawait this.refreshStates();\n\t}\n\n\t/**\n\t * Watches exactly the states the active trigger rules name.\n\t *\n\t * Runs at startup and whenever the administration saves the rules, so an added, changed or removed rule takes\n\t * effect without a restart. A state that no rule names any more is dropped from the watch list.\n\t */\n\tprivate async refreshTriggerSubscriptions(): Promise<void> {\n\t\tconst services = this.services;\n\t\tif (!services) {\n\t\t\treturn;\n\t\t}\n\n\t\tlet rules: TriggerRuleRecord[] = [];\n\t\ttry {\n\t\t\trules = services.triggers.list();\n\t\t} catch (error) {\n\t\t\tthis.log.warn(`trigger rules could not be read: ${(error as Error).message}`);\n\t\t\treturn;\n\t\t}\n\n\t\tconst wanted = new Set(rules.map(rule => rule.sourceState));\n\t\tfor (const id of [...this.triggerStates]) {\n\t\t\tif (wanted.has(id)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tawait this.unsubscribeForeignStatesAsync(id);\n\t\t\t} catch (error) {\n\t\t\t\tthis.log.debug(`trigger state ${id} could not be released: ${(error as Error).message}`);\n\t\t\t}\n\t\t\tthis.triggerStates.delete(id);\n\t\t\tthis.triggerValues.delete(id);\n\t\t}\n\n\t\tfor (const id of wanted) {\n\t\t\tif (this.triggerStates.has(id)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tawait this.subscribeForeignStatesAsync(id);\n\t\t\t\tthis.triggerStates.add(id);\n\t\t\t} catch (error) {\n\t\t\t\tthis.log.warn(`trigger state ${id} could not be watched: ${(error as Error).message}`);\n\t\t\t}\n\t\t}\n\n\t\tthis.log.debug(`watching ${this.triggerStates.size} trigger state(s) for ${rules.length} active rule(s)`);\n\t}\n\n\t/**\n\t * Applies a write on a state that trigger rules watch.\n\t *\n\t * Such a state belongs to another adapter, so its `ack` says nothing about whether the value was set by a\n\t * script or by the device. What counts is that the value **changed**: a reader that repeats itself, a\n\t * dashboard that refreshes or a script that writes the same value twice does not punch twice. Together with\n\t * the cooldown of a rule even a rapidly blinking state stays harmless.\n\t *\n\t * @param id - full state id\n\t * @param state - state object of the write\n\t * @returns true when the state belongs to a rule and was handled here\n\t */\n\tprivate handleTriggerState(id: string, state: ioBroker.State): boolean {\n\t\tconst services = this.services;\n\t\tif (!services) {\n\t\t\treturn false;\n\t\t}\n\n\t\tconst previous = this.triggerValues.get(id) ?? null;\n\t\tthis.triggerValues.set(id, triggerText(state.val));\n\n\t\tconst rules = services.triggers.list().filter(rule => rule.sourceState === id);\n\t\tif (rules.length === 0) {\n\t\t\treturn false;\n\t\t}\n\n\t\tconst now = Math.floor(Date.now() / 1000);\n\t\tfor (const rule of rules) {\n\t\t\tconst decision = evaluateTrigger(rule, { value: state.val, previous, now }, services.users);\n\t\t\tif (!decision.fire || decision.userId === null) {\n\t\t\t\tthis.log.debug(`trigger ${rule.id} (${rule.sourceState}) did not fire: ${decision.reason}`);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tthis.runTrigger(rule, decision.userId);\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * Runs the action of a rule that fired.\n\t *\n\t * The punch goes through the same path as the command state and a presence change through the same handler as\n\t * the `users.<id>.present` state, so direction, rounding, notes and the recalculation behave identically.\n\t *\n\t * @param rule - rule that fired\n\t * @param userId - employee the action applies to\n\t */\n\tprivate runTrigger(rule: TriggerRuleRecord, userId: number): void {\n\t\tvoid (async (): Promise<void> => {\n\t\t\tconst services = this.services;\n\t\t\tif (!services) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst result =\n\t\t\t\t\trule.action === \"present\" || rule.action === \"absent\"\n\t\t\t\t\t\t? handlePresenceState(\n\t\t\t\t\t\t\t\t{ entries: services.entries, users: services.users, aggregation: services.aggregation },\n\t\t\t\t\t\t\t\t`users.${userId}.${PRESENCE_SUFFIX}`,\n\t\t\t\t\t\t\t\trule.action === \"present\",\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t: punchEmployee(\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tentries: services.entries,\n\t\t\t\t\t\t\t\t\tusers: services.users,\n\t\t\t\t\t\t\t\t\tsettings: services.settings,\n\t\t\t\t\t\t\t\t\taggregation: services.aggregation,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{ userId, quick: rule.action === \"quickPunch\", note: `trigger.${rule.id}` },\n\t\t\t\t\t\t\t);\n\t\t\t\tthis.log.info(`trigger ${rule.id} (${rule.sourceState}) \u2192 ${result.message}`);\n\t\t\t\t// a punch written by a rule lands in `events.*` too; a presence write that changed nothing has no\n\t\t\t\t// event (the builder answers `null` then)\n\t\t\t\tconst atUtc = Math.floor(Date.now() / 1000);\n\t\t\t\tif (\"changed\" in result) {\n\t\t\t\t\tthis.publishPunch(presenceEvent(result, atUtc));\n\t\t\t\t} else {\n\t\t\t\t\tthis.publishPunch(punchEvent(result, `trigger.${rule.id}`, atUtc));\n\t\t\t\t}\n\t\t\t\tservices.triggers.markFired({ id: rule.id });\n\t\t\t\tawait this.refreshStates();\n\t\t\t} catch (error) {\n\t\t\t\t// a deleted employee, a deactivated one, a value that cannot be used: never take the adapter down\n\t\t\t\tthis.log.warn(`trigger ${rule.id} failed: ${(error as Error).message}`);\n\t\t\t}\n\t\t})();\n\t}\n\n\t/**\n\t * Stops the HTTP server.\n\t */\n\tprivate async stopApi(): Promise<void> {\n\t\tconst server = this.webServer;\n\t\tthis.webServer = null;\n\t\tif (server) {\n\t\t\tawait server.close();\n\t\t\tthis.log.debug(\"API stopped\");\n\t\t}\n\t}\n\n\t/**\n\t * Is called when adapter shuts down - callback has to be called under any circumstances!\n\t *\n\t * @param callback - Callback function\n\t */\n\tprivate onUnload(callback: () => void): void {\n\t\ttry {\n\t\t\t// Timers created with this.setTimeout/this.setInterval are cleared automatically on unload.\n\t\t\tvoid this.stopApi()\n\t\t\t\t.catch(error => this.log.warn(`stopping the API failed: ${(error as Error).message}`))\n\t\t\t\t.finally(() => {\n\t\t\t\t\tthis.db?.close();\n\t\t\t\t\tthis.db = null;\n\t\t\t\t\tcallback();\n\t\t\t\t});\n\t\t} catch (error) {\n\t\t\tthis.log.error(`Error during unloading: ${(error as Error).message}`);\n\t\t\tcallback();\n\t\t}\n\t}\n\n\t/**\n\t * Runs the automation rules that are due.\n\t *\n\t * A rule acts at most once per employee and period \u2014 the local date, or the ISO week for a weekly rule;\n\t * `automation_runs` holds that decision, so a restart\n\t * cannot punch twice. The run is noted **before** the action (a double punch would be worse than a missed\n\t * reminder) and taken back when the action fails, so the next minute retries it.\n\t */\n\tprivate runAutomationRules(): void {\n\t\tconst services = this.services;\n\t\tif (!services) {\n\t\t\treturn;\n\t\t}\n\n\t\tlet rules: AutomationRuleRecord[] = [];\n\t\ttry {\n\t\t\trules = services.automations.list();\n\t\t} catch (error) {\n\t\t\tthis.log.warn(`automation rules could not be read: ${(error as Error).message}`);\n\t\t\treturn;\n\t\t}\n\t\tif (rules.length === 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst nowUtc = Math.floor(Date.now() / 1000);\n\t\tlet acted = false;\n\n\t\tfor (const rule of rules) {\n\t\t\tconst candidates =\n\t\t\t\trule.userId === null\n\t\t\t\t\t? services.users.list()\n\t\t\t\t\t: [services.users.findById(rule.userId)].filter((user): user is UserRecord => user !== null);\n\n\t\t\tfor (const user of candidates) {\n\t\t\t\tif (!user.isActive) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst local = localDateTime(nowUtc, user.timezone);\n\t\t\t\t// a weekly rule is guarded per ISO week, a daily rule per local date\n\t\t\t\tconst period = runPeriod(rule.repeat, local.date);\n\t\t\t\tif (services.automations.hasRun({ ruleId: rule.id, userId: user.id, period })) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst punches = services.entries\n\t\t\t\t\t.listByDate(user.id, local.date)\n\t\t\t\t\t.map(entry => ({ tsUtc: entry.tsUtc, direction: entry.direction }));\n\t\t\t\tconst block = workBlock(punches, nowUtc);\n\t\t\t\tconst decision = evaluateAutomation(rule, {\n\t\t\t\t\tlocalDate: local.date,\n\t\t\t\t\tweekday: isoWeekday(local.date),\n\t\t\t\t\tminuteOfDay: local.minutes,\n\t\t\t\t\thasOpenEntry: block.hasOpenEntry,\n\t\t\t\t\tblockMinutes: block.blockMinutes,\n\t\t\t\t\talreadyRan: false,\n\t\t\t\t});\n\t\t\t\tif (!decision.fire) {\n\t\t\t\t\tthis.log.debug(`automation ${rule.id} (${user.displayName}): ${decision.reason}`);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst action = this.automationAction(rule, block.blockMinutes);\n\t\t\t\tif (\n\t\t\t\t\t!services.automations.recordRun({\n\t\t\t\t\t\truleId: rule.id,\n\t\t\t\t\t\tuserId: user.id,\n\t\t\t\t\t\tperiod,\n\t\t\t\t\t\taction,\n\t\t\t\t\t\tnow: nowUtc,\n\t\t\t\t\t})\n\t\t\t\t) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\ttry {\n\t\t\t\t\tlet punched: CommandResult | null = null;\n\t\t\t\t\tif (rule.kind === \"clockOut\" || rule.kind === \"clockIn\") {\n\t\t\t\t\t\tpunched = punchEmployee(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tentries: services.entries,\n\t\t\t\t\t\t\t\tusers: services.users,\n\t\t\t\t\t\t\t\tsettings: services.settings,\n\t\t\t\t\t\t\t\taggregation: services.aggregation,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{ userId: user.id, note: `auto.${rule.kind}`, now: nowUtc },\n\t\t\t\t\t\t);\n\t\t\t\t\t\tthis.log.info(`automation ${rule.id} for ${user.displayName}: ${punched.message}`);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.log.info(`automation ${rule.id} for ${user.displayName}: ${action}`);\n\t\t\t\t\t}\n\t\t\t\t\tacted = true;\n\t\t\t\t\tthis.events?.publish({\n\t\t\t\t\t\ttype: `automation.${rule.kind}`,\n\t\t\t\t\t\tatUtc: nowUtc,\n\t\t\t\t\t\tuserId: user.id,\n\t\t\t\t\t\t// the direction belongs into the event when the rule wrote a punch (checker: `events.lastDirection`)\n\t\t\t\t\t\tdata: { ruleId: rule.id, action, reason: decision.reason, direction: punched?.direction },\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// give the next check a chance instead of losing the period\n\t\t\t\t\tservices.automations.forgetRun({ ruleId: rule.id, userId: user.id, period });\n\t\t\t\t\tthis.log.warn(`automation ${rule.id} for ${user.displayName} failed: ${(error as Error).message}`);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (acted) {\n\t\t\t// a punch written by a rule changes the published figures\n\t\t\tthis.scheduleStateRefresh();\n\t\t}\n\t}\n\n\t/**\n\t * Short description of what a rule did (the log entry of `automation_runs`).\n\t *\n\t * @param rule - rule that fired\n\t * @param blockMinutes - length of the running work block, `null` when the employee is not clocked in\n\t * @returns text for the log\n\t */\n\tprivate automationAction(rule: AutomationRuleRecord, blockMinutes: number | null): string {\n\t\tif (rule.kind === \"clockOut\") {\n\t\t\treturn \"clocked out\";\n\t\t}\n\t\tif (rule.kind === \"clockIn\") {\n\t\t\treturn \"clocked in\";\n\t\t}\n\t\tif (rule.kind === \"breakReminder\") {\n\t\t\treturn `reminded after ${blockMinutes ?? 0} min without a break`;\n\t\t}\n\t\treturn \"reported a missing punch\";\n\t}\n\n\t/**\n\t * Republishes the figures shortly after data changed.\n\t *\n\t * A punch from the web app, the terminal or a badge arrives through the API of this very process, so without\n\t * this the states would only change with the five minute timer. One action often produces several events, so\n\t * the refresh is collected for a moment instead of running once per event.\n\t */\n\tprivate scheduleStateRefresh(): void {\n\t\tif (this.stateRefreshTimer) {\n\t\t\treturn;\n\t\t}\n\t\tthis.stateRefreshTimer = this.setTimeout(() => {\n\t\t\tthis.stateRefreshTimer = undefined;\n\t\t\tvoid this.refreshStates();\n\t\t}, 1000);\n\t}\n\n\t/**\n\t * Publishes the newest event of the instance into the state tree.\n\t *\n\t * A notification adapter, a dashboard or a Blockly script then only has to watch `events.*` instead of\n\t * following the live stream of the web app.\n\t *\n\t * @param event - event published by the API\n\t */\n\tprivate async publishEventState(event: ApiEvent): Promise<void> {\n\t\tconst services = this.services;\n\t\tif (!services) {\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tconst data = event.data ?? {};\n\t\t\tconst user =\n\t\t\t\tevent.userId === null || event.userId === undefined ? null : services.users.findById(event.userId);\n\t\t\tawait publishEventSnapshot(this, {\n\t\t\t\ttype: event.type,\n\t\t\t\tatUtc: event.atUtc,\n\t\t\t\tuserName: user?.displayName ?? \"\",\n\t\t\t\tdirection: typeof data.direction === \"string\" ? data.direction : \"\",\n\t\t\t\tsource: typeof data.source === \"string\" ? data.source : \"\",\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tthis.log.debug(`event state could not be published: ${(error as Error).message}`);\n\t\t}\n\t}\n\n\t/**\n\t * Publishes a punch on the event bus, so it reaches `events.*` and the connected web clients.\n\t *\n\t * A punch can be written from several places \u2014 the command states, a `sendTo` message, a trigger rule, an\n\t * automation rule and the presence state. Every one of them passes its result through here; a path that forgets\n\t * it leaves the event states (`lastType`, `lastUser`, \u2026) on the punch before.\n\t *\n\t * @param event - event built from the punch, `null` when nothing was written\n\t */\n\tprivate publishPunch(event: ApiEvent | null): void {\n\t\tif (event) {\n\t\t\tthis.events?.publish(event);\n\t\t}\n\t}\n\n\t/**\n\t * Is called when another adapter sends a message (`sendTo`).\n\t *\n\t * @param obj - message object of the adapter framework\n\t */\n\tprivate onMessage(obj: ioBroker.Message): void {\n\t\tvoid (async (): Promise<void> => {\n\t\t\tconst services = this.services;\n\t\t\tif (!services) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst answer = await handleMessage(\n\t\t\t\t\t{\n\t\t\t\t\t\tusers: services.users,\n\t\t\t\t\t\tentries: services.entries,\n\t\t\t\t\t\tabsences: services.absences,\n\t\t\t\t\t\tsettings: services.settings,\n\t\t\t\t\t\taggregation: services.aggregation,\n\t\t\t\t\t\tsync: services.sync,\n\t\t\t\t\t\tbackup: services.backup,\n\t\t\t\t\t\tversion: this.version,\n\t\t\t\t\t},\n\t\t\t\t\tobj.command,\n\t\t\t\t\tobj.message,\n\t\t\t\t);\n\t\t\t\tthis.log.info(`message ${obj.command}: ${answer.message}`);\n\t\t\t\t// a punch answered by a message reaches `events.*` and the web app like every other one\n\t\t\t\tconst direction = answer.data?.direction;\n\t\t\t\tconst userId = answer.data?.userId;\n\t\t\t\tif (typeof direction === \"string\" && typeof userId === \"number\") {\n\t\t\t\t\tthis.publishPunch({\n\t\t\t\t\t\ttype: \"punch\",\n\t\t\t\t\t\tatUtc: Math.floor(Date.now() / 1000),\n\t\t\t\t\t\tuserId,\n\t\t\t\t\t\tdata: { direction, source: `sendTo.${obj.command}` },\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tif (obj.callback) {\n\t\t\t\t\tthis.sendTo(obj.from, obj.command, answer, obj.callback);\n\t\t\t\t}\n\t\t\t\tawait this.refreshStates();\n\t\t\t} catch (error) {\n\t\t\t\tconst message = (error as Error).message;\n\t\t\t\tthis.log.warn(`message ${obj.command} failed: ${message}`);\n\t\t\t\tif (obj.callback) {\n\t\t\t\t\tthis.sendTo(obj.from, obj.command, { ok: false, message }, obj.callback);\n\t\t\t\t}\n\t\t\t}\n\t\t})();\n\t}\n\n\t/**\n\t * Is called if a subscribed state changes\n\t *\n\t * @param id - State ID\n\t * @param state - State object\n\t */\n\tprivate onStateChange(id: string, state: ioBroker.State | null | undefined): void {\n\t\ttry {\n\t\t\tif (!state) {\n\t\t\t\t// deletions carry no value\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// a watched state of another adapter is handled first: such a state is acknowledged by its own\n\t\t\t// adapter, and what counts there is the change of the value\n\t\t\tif (this.handleTriggerState(id, state)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (state.ack) {\n\t\t\t\t// Ignore acknowledged (status) states of this instance\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst prefix = `${this.namespace}.`;\n\t\t\tconst localId = id.startsWith(prefix) ? id.slice(prefix.length) : id;\n\t\t\tif (localId.startsWith(\"commands.\")) {\n\t\t\t\tvoid this.runCommand(localId, state.val);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (parsePresenceStateId(localId) !== null) {\n\t\t\t\tvoid this.runPresence(localId, state.val);\n\t\t\t\treturn;\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthis.log.error(`Error in onStateChange for ${id}: ${(error as Error).message}`);\n\t\t}\n\t}\n}\n\nif (require.main !== module) {\n\t// Export the constructor in compact mode\n\tmodule.exports = (options: Partial<utils.AdapterOptions> | undefined) => new TimeTracker(options);\n} else {\n\t// otherwise start the instance directly\n\t(() => new TimeTracker())();\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["import_triggers", "result"]
|
|
7
7
|
}
|
package/io-package.json
CHANGED
|
@@ -1,9 +1,54 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "time-tracker",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.15",
|
|
5
5
|
"nogit": true,
|
|
6
|
+
"localLinks": {
|
|
7
|
+
"_default": {
|
|
8
|
+
"link": "%protocol%://%ip%:%port%/",
|
|
9
|
+
"name": {
|
|
10
|
+
"en": "Open the time tracking app",
|
|
11
|
+
"de": "Zeiterfassungs-App öffnen",
|
|
12
|
+
"ru": "Открыть приложение учёта времени",
|
|
13
|
+
"pt": "Abrir a aplicação de controlo de tempo",
|
|
14
|
+
"nl": "Tijdregistratie-app openen",
|
|
15
|
+
"fr": "Ouvrir l'application de suivi du temps",
|
|
16
|
+
"it": "Apri l'app di monitoraggio del tempo",
|
|
17
|
+
"es": "Abrir la aplicación de seguimiento del tiempo",
|
|
18
|
+
"pl": "Otwórz aplikację śledzenia czasu",
|
|
19
|
+
"uk": "Відкрити застосунок обліку часу",
|
|
20
|
+
"zh-cn": "打开时间追踪应用"
|
|
21
|
+
},
|
|
22
|
+
"order": 5
|
|
23
|
+
}
|
|
24
|
+
},
|
|
6
25
|
"news": {
|
|
26
|
+
"0.7.15": {
|
|
27
|
+
"en": "new: the instance tile in the admin opens the time tracking app now - the adapter announces its web interface as common.localLinks, so the admin shows a symbol next to the instance and on the overview page. The address comes from the bind address and the port of the instance settings, nothing is hardcoded.",
|
|
28
|
+
"de": "new: Die Instanzkachel im Admin öffnet jetzt die Zeiterfassungs-App — der Adapter meldet seine Oberfläche als `common.localLinks` an, deshalb zeigt der Admin ein Symbol neben der Instanz und auf der Übersichtsseite. Die Adresse setzt sich aus der Bind-Adresse und dem Port der Instanzeinstellungen zusammen, fest verdrahtet ist nichts.",
|
|
29
|
+
"ru": "новое: плитка экземпляра в admin теперь открывает приложение учёта времени — адаптер объявляет свой веб-интерфейс как `common.localLinks`, поэтому admin показывает символ рядом с экземпляром и на странице обзора. Адрес складывается из адреса привязки и порта из настроек экземпляра, жёстко ничего не задано.",
|
|
30
|
+
"pt": "novidade: o mosaico da instância no admin abre agora a aplicação de controlo de tempo — o adaptador anuncia a sua interface web como `common.localLinks`, por isso o admin mostra um símbolo ao lado da instância e na página de resumo. O endereço resulta do endereço de escuta e da porta das definições da instância; nada está fixo no código.",
|
|
31
|
+
"nl": "nieuw: de tegel van de instantie in de admin opent nu de tijdregistratie-app — de adapter meldt zijn webinterface aan als `common.localLinks`, daarom toont de admin een symbool naast de instantie en op de overzichtspagina. Het adres komt uit het bindadres en de poort uit de instellingen van de instantie, niets is hard gecodeerd.",
|
|
32
|
+
"fr": "nouveauté : la tuile de l'instance dans l'admin ouvre désormais l'application de suivi du temps — l'adaptateur annonce son interface web sous `common.localLinks`, c'est pourquoi l'admin affiche un symbole à côté de l'instance et sur la page de vue d'ensemble. L'adresse provient de l'adresse d'écoute et du port des réglages de l'instance, rien n'est codé en dur.",
|
|
33
|
+
"it": "novità: il riquadro dell'istanza nell'admin apre ora l'app di monitoraggio del tempo — l'adattatore annuncia la propria interfaccia web come `common.localLinks`, perciò l'admin mostra un simbolo accanto all'istanza e nella pagina di panoramica. L'indirizzo deriva dall'indirizzo di bind e dalla porta delle impostazioni dell'istanza, nulla è codificato a mano.",
|
|
34
|
+
"es": "novedad: el mosaico de la instancia en el admin abre ahora la aplicación de seguimiento del tiempo — el adaptador anuncia su interfaz web como `common.localLinks`, por eso el admin muestra un símbolo junto a la instancia y en la página de resumen. La dirección resulta de la dirección de escucha y del puerto de los ajustes de la instancia, nada está fijado en el código.",
|
|
35
|
+
"pl": "nowość: kafelek instancji w adminie otwiera teraz aplikację śledzenia czasu — adapter zgłasza swój interfejs web jako `common.localLinks`, dlatego admin pokazuje symbol obok instancji i na stronie przeglądu. Adres składa się z adresu bindowania i portu z ustawień instancji, nic nie jest zapisane na stałe.",
|
|
36
|
+
"uk": "нове: плитка екземпляра в admin тепер відкриває застосунок обліку часу — адаптер оголошує свій веб-інтерфейс як `common.localLinks`, тому admin показує символ поруч із екземпляром і на сторінці огляду. Адреса складається з адреси прив'язки та порту з налаштувань екземпляра, жорстко нічого не прописано.",
|
|
37
|
+
"zh-cn": "新增:管理界面中的实例卡片现在可以打开时间追踪应用——适配器将其 Web 界面声明为 `common.localLinks`,因此管理界面会在实例旁和概览页显示一个图标。地址由绑定地址和实例设置中的端口组成,没有任何内容被写死。"
|
|
38
|
+
},
|
|
39
|
+
"0.7.14": {
|
|
40
|
+
"en": "fix: a start password that is entered after the first start opens the installation now — as long as the first administrator still carries his unchanged start password, the configured value is applied on every start and the log names what happened; once the password was changed in the app the setting stays out of play and the log says so.",
|
|
41
|
+
"de": "fix: Ein Startpasswort, das erst nach dem ersten Start eingetragen wird, öffnet die Installation jetzt — solange der Erst-Administrator sein Startpasswort noch nicht gewechselt hat, setzt der Adapter den konfigurierten Wert bei jedem Start und nennt im Log, was passiert ist; nach einem Passwortwechsel in der App bleibt die Einstellung wirkungslos und das Log sagt es.",
|
|
42
|
+
"ru": "исправление: стартовый пароль, введённый после первого запуска, теперь открывает установку — пока первый администратор ещё не сменил свой стартовый пароль, адаптер применяет заданное значение при каждом запуске и сообщает в логе, что произошло; после смены пароля в приложении настройка не действует, и лог говорит об этом.",
|
|
43
|
+
"pt": "correção: uma palavra-passe inicial introduzida depois do primeiro arranque abre agora a instalação — enquanto o primeiro administrador ainda não alterou a sua palavra-passe inicial, o adaptador aplica o valor configurado em cada arranque e indica no log o que aconteceu; depois de a palavra-passe ser alterada na aplicação, a definição fica sem efeito e o log di-lo.",
|
|
44
|
+
"nl": "fix: een startwachtwoord dat pas na de eerste start wordt ingevuld, opent de installatie nu — zolang de eerste beheerder zijn startwachtwoord nog niet heeft gewijzigd, past de adapter de ingestelde waarde bij elke start toe en zegt het logboek wat er is gebeurd; na een wachtwoordwijziging in de app blijft de instelling buiten werking en zegt het logboek dat.",
|
|
45
|
+
"fr": "correctif : un mot de passe de démarrage saisi après le premier démarrage ouvre désormais l'installation — tant que le premier administrateur n'a pas encore changé son mot de passe de démarrage, l'adaptateur applique la valeur configurée à chaque démarrage et indique dans le journal ce qui s'est passé ; après un changement de mot de passe dans l'application, le réglage reste sans effet et le journal le dit.",
|
|
46
|
+
"it": "correzione: una password iniziale inserita dopo il primo avvio ora apre l'installazione — finché il primo amministratore non ha ancora cambiato la password iniziale, l'adattatore applica il valore configurato a ogni avvio e indica nel log che cosa è successo; dopo il cambio della password nell'app l'impostazione resta inefficace e il log lo dice.",
|
|
47
|
+
"es": "corrección: una contraseña inicial introducida después del primer inicio abre ahora la instalación — mientras el primer administrador aún no haya cambiado su contraseña inicial, el adaptador aplica el valor configurado en cada inicio e indica en el registro qué ha ocurrido; una vez cambiada la contraseña en la aplicación, el ajuste queda sin efecto y el registro lo dice.",
|
|
48
|
+
"pl": "poprawka: hasło startowe wpisane po pierwszym uruchomieniu otwiera teraz instalację — dopóki pierwszy administrator nie zmienił jeszcze swojego hasła startowego, adapter stosuje skonfigurowaną wartość przy każdym uruchomieniu i podaje w logu, co się stało; po zmianie hasła w aplikacji ustawienie pozostaje bez znaczenia, a log o tym mówi.",
|
|
49
|
+
"uk": "виправлення: стартовий пароль, введений після першого запуску, тепер відкриває інсталяцію — поки перший адміністратор ще не змінив свій стартовий пароль, адаптер застосовує налаштоване значення під час кожного запуску і повідомляє в журналі, що сталося; після зміни пароля в застосунку налаштування не діє, і журнал про це говорить.",
|
|
50
|
+
"zh-cn": "修复:在首次启动之后才填入的启动密码现在可以打开安装——只要第一位管理员尚未更改其启动密码,适配器每次启动都会应用所配置的值并在日志中说明发生了什么;在应用中更改密码后,该设置不再生效,日志也会说明这一点。"
|
|
51
|
+
},
|
|
7
52
|
"0.7.13": {
|
|
8
53
|
"en": "docs: the calendar file is reachable through the file server of a `web` instance again — `http://<host>:8081/files/time-tracker.0/storage/calendar.ics` (checked on a real installation, the `storage` folder belongs to the path); the acceptance protocol `docs/testplan.md` is complete.",
|
|
9
54
|
"de": "docs: Die Kalenderdatei ist über den Dateiserver einer `web`-Instanz wieder erreichbar — `http://<host>:8081/files/time-tracker.0/storage/calendar.ics` (in einer echten Installation geprüft, der Ordner `storage` gehört zum Pfad). Das Abnahmeprotokoll `docs/testplan.md` ist vollständig.",
|
|
@@ -68,32 +113,6 @@
|
|
|
68
113
|
"pl": "poprawka: „Feiertag hinzufügen” był wyszarzony, dopóki dzień trzeba było wpisywać jako tekst — teraz pochodzi z pola daty przeglądarki, a etykieta nie podaje już formatu. Święto dodane bez regionu należy do kraju instancji, a nie zawsze do „DE”, lista pokazuje dni wszystkich regionów roku, a dzień następnego roku pociąga za sobą wyświetlany rok. „Abbrechen” zamyka też okno zmiany.",
|
|
69
114
|
"uk": "виправлення: «Feiertag hinzufügen» залишалася сірою, доки день потрібно було вводити текстом — тепер він надходить із поля дати браузера, і підпис більше не називає формат. Свято, додане без регіону, належить країні екземпляра, а не завжди «DE», список показує дні всіх регіонів року, а день наступного року тягне за собою показаний рік. «Abbrechen» закриває й вікно зміни.",
|
|
70
115
|
"zh-cn": "修复:“Feiertag hinzufügen”在必须以文本输入日期时一直是灰色——现在日期来自浏览器的日期字段,标签也不再说明格式。未填写地区的节日属于实例所在国家,而不再固定为“DE”,列表会显示该年度所有地区的日期,下一年度的日期会一并切换显示的年份。“Abbrechen”也会关闭修改窗口。"
|
|
71
|
-
},
|
|
72
|
-
"0.7.8": {
|
|
73
|
-
"en": "new: an absence can be taken back — an employee withdraws an open request, and for an approved absence he asks for the cancellation (with a reason); the days keep counting until the administration accepts or declines it. The administration changes and deletes absences in the app, the rules name their newest run in their own row, and a hint beside “Save rules” says that the table is saved as a whole.",
|
|
74
|
-
"de": "neu: Eine Abwesenheit lässt sich zurücknehmen – ein offener Antrag wird zurückgezogen, und für eine genehmigte Abwesenheit wird das Storno beantragt (mit Grund); die Tage zählen weiter, bis die Verwaltung annimmt oder ablehnt. Die Verwaltung ändert und löscht Abwesenheiten in der App, die Regeln nennen ihre neueste Ausführung in einer eigenen Zeile, und ein Hinweis neben „Regeln speichern“ sagt, dass die Tabelle als Ganzes gespeichert wird.",
|
|
75
|
-
"ru": "new: отсутствие можно отозвать — открытая заявка отзывается, а для одобренного отсутствия запрашивается отмена (с причиной); дни продолжают учитываться, пока администрация не примет или не отклонит запрос. Администрация изменяет и удаляет отсутствия в приложении, правила показывают последний запуск в отдельной строке, а подсказка рядом с «Сохранить правила» напоминает, что таблица сохраняется целиком.",
|
|
76
|
-
"pt": "new: uma ausência pode ser retirada — um pedido aberto é retirado e, para uma ausência aprovada, pede-se o cancelamento (com motivo); os dias continuam a contar até a administração aceitar ou rejeitar. A administração altera e elimina ausências na aplicação, as regras mostram a sua última execução numa linha própria e uma indicação junto a «Guardar regras» diz que a tabela é guardada como um todo.",
|
|
77
|
-
"nl": "new: een afwezigheid kan worden ingetrokken — een openstaande aanvraag wordt ingetrokken en voor een goedgekeurde afwezigheid wordt de annulering aangevraagd (met reden); de dagen blijven tellen tot de administratie accepteert of afwijst. De administratie bewerkt en verwijdert afwezigheden in de app, de regels noemen hun laatste uitvoering in een eigen regel en een hint bij „Regels opslaan” zegt dat de tabel als geheel wordt opgeslagen.",
|
|
78
|
-
"fr": "new: une absence peut être retirée — une demande ouverte est retirée et, pour une absence approuvée, l'annulation est demandée (avec un motif) ; les jours continuent de compter jusqu'à ce que l'administration accepte ou refuse. L'administration modifie et supprime les absences dans l'application, les règles indiquent leur dernière exécution sur une ligne propre et une indication près de « Enregistrer les règles » rappelle que le tableau est enregistré en entier.",
|
|
79
|
-
"it": "new: un'assenza può essere ritirata — una richiesta aperta viene ritirata e per un'assenza approvata si chiede la cancellazione (con motivo); i giorni continuano a contare finché l'amministrazione accetta o rifiuta. L'amministrazione modifica ed elimina le assenze nell'app, le regole indicano la loro ultima esecuzione in una riga propria e un suggerimento accanto a «Salva regole» ricorda che la tabella viene salvata nel suo insieme.",
|
|
80
|
-
"es": "new: una ausencia se puede retirar — una solicitud abierta se retira y, para una ausencia aprobada, se pide la cancelación (con motivo); los días siguen contando hasta que la administración acepte o rechace. La administración edita y elimina ausencias en la aplicación, las reglas indican su última ejecución en una línea propia y un aviso junto a «Guardar reglas» recuerda que la tabla se guarda entera.",
|
|
81
|
-
"pl": "new: nieobecność można wycofać — otwarty wniosek jest wycofywany, a dla zatwierdzonej nieobecności zgłaszane jest anulowanie (z powodem); dni liczą się dalej, dopóki administracja nie przyjmie lub nie odrzuci. Administracja edytuje i usuwa nieobecności w aplikacji, reguły podają swoją ostatnią realizację w osobnym wierszu, a podpowiedź obok „Zapisz reguły” mówi, że tabela zapisuje się w całości.",
|
|
82
|
-
"uk": "new: відсутність можна відкликати — відкриту заявку відкликають, а для схваленої відсутності запитують скасування (із причиною); дні рахуються далі, доки адміністрація не прийме або не відхилить. Адміністрація змінює та видаляє відсутності в застосунку, правила показують останній запуск в окремому рядку, а підказка біля «Зберегти правила» нагадує, що таблиця зберігається повністю.",
|
|
83
|
-
"zh-cn": "new: 缺勤可以被撤回——开放的申请会被撤回,已批准的缺勤可以申请撤销(可填写原因);在管理部门接受或拒绝之前,这些天仍然计算。管理部门可以在应用中编辑和删除缺勤,规则会在单独一行显示最近一次执行,而“保存规则”旁的提示说明整张表会一起保存。"
|
|
84
|
-
},
|
|
85
|
-
"0.7.7": {
|
|
86
|
-
"en": "change: the rule dialog keeps “Aktiv” beside “Wiederholung”, and the validity dates (“Gültig ab” and “Gültig bis”) stand in a row of their own below it — the two date fields no longer wrap around the button.",
|
|
87
|
-
"de": "change: Im Regel-Dialog bleibt „Aktiv“ neben „Wiederholung“, und die Gültigkeitsfelder „Gültig ab“ und „Gültig bis“ stehen in einer eigenen Zeile darunter – die beiden Datumsfelder umfließen den Knopf nicht mehr.",
|
|
88
|
-
"ru": "изменение: в диалоге правила «Aktiv» остаётся рядом с «Wiederholung», а поля срока действия «Gültig ab» и «Gültig bis» стоят в отдельной строке под ним — два поля даты больше не обтекают кнопку.",
|
|
89
|
-
"pt": "alteração: no diálogo da regra «Aktiv» fica ao lado de «Wiederholung», e os campos de validade «Gültig ab» e «Gültig bis» ficam numa linha própria abaixo dele — as duas datas deixam de contornar o botão.",
|
|
90
|
-
"nl": "wijziging: in het regeldialoog blijft ‘Aktiv’ naast ‘Wiederholung’ staan, en de geldigheidsvelden ‘Gültig ab’ en ‘Gültig bis’ staan in een eigen regel daaronder — de twee datumvelden lopen niet meer om de knop heen.",
|
|
91
|
-
"fr": "changement : dans la boîte de dialogue, « Aktiv » reste à côté de « Wiederholung » et les champs de validité « Gültig ab » et « Gültig bis » occupent une ligne à part en dessous — les deux dates ne s'enroulent plus autour du bouton.",
|
|
92
|
-
"it": "modifica: nella finestra della regola «Aktiv» resta accanto a «Wiederholung», mentre i campi di validità «Gültig ab» e «Gültig bis» stanno su una riga propria sotto di esso — le due date non si avvolgono più attorno al pulsante.",
|
|
93
|
-
"es": "cambio: en el diálogo de la regla «Aktiv» se queda junto a «Wiederholung», y los campos de validez «Gültig ab» y «Gültig bis» ocupan una fila propia debajo — las dos fechas ya no rodean el botón.",
|
|
94
|
-
"pl": "zmiana: w oknie reguły „Aktiv” pozostaje obok „Wiederholung”, a pola ważności „Gültig ab” i „Gültig bis” stoją w osobnym wierszu pod nim — dwa pola daty nie opływają już przycisku.",
|
|
95
|
-
"uk": "зміна: у вікні правила «Aktiv» залишається поруч із «Wiederholung», а поля терміну дії «Gültig ab» і «Gültig bis» стоять в окремому рядку під ним — два поля дати більше не обтікають кнопку.",
|
|
96
|
-
"zh-cn": "变更:规则对话框中“Aktiv”仍与“Wiederholung”同一行,而有效期字段“Gültig ab”和“Gültig bis”单独位于下一行——两个日期字段不再围绕按钮换行。"
|
|
97
116
|
}
|
|
98
117
|
},
|
|
99
118
|
"titleLang": {
|
package/package.json
CHANGED
package/www/sw.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sw.js","sources":["../../../../../../tmp/
|
|
1
|
+
{"version":3,"file":"sw.js","sources":["../../../../../../tmp/64e37e67a2233e637456ec225b71d20c/sw.js"],"sourcesContent":["import {clientsClaim as workbox_core_clientsClaim} from '/home/runner/work/ioBroker.time-tracker/ioBroker.time-tracker/src-www/node_modules/workbox-core/clientsClaim.mjs';\nimport {precacheAndRoute as workbox_precaching_precacheAndRoute} from '/home/runner/work/ioBroker.time-tracker/ioBroker.time-tracker/src-www/node_modules/workbox-precaching/precacheAndRoute.mjs';\nimport {cleanupOutdatedCaches as workbox_precaching_cleanupOutdatedCaches} from '/home/runner/work/ioBroker.time-tracker/ioBroker.time-tracker/src-www/node_modules/workbox-precaching/cleanupOutdatedCaches.mjs';\nimport {registerRoute as workbox_routing_registerRoute} from '/home/runner/work/ioBroker.time-tracker/ioBroker.time-tracker/src-www/node_modules/workbox-routing/registerRoute.mjs';\nimport {NavigationRoute as workbox_routing_NavigationRoute} from '/home/runner/work/ioBroker.time-tracker/ioBroker.time-tracker/src-www/node_modules/workbox-routing/NavigationRoute.mjs';\nimport {createHandlerBoundToURL as workbox_precaching_createHandlerBoundToURL} from '/home/runner/work/ioBroker.time-tracker/ioBroker.time-tracker/src-www/node_modules/workbox-precaching/createHandlerBoundToURL.mjs';/**\n * Welcome to your Workbox-powered service worker!\n *\n * You'll need to register this file in your web app.\n * See https://goo.gl/nhQhGp\n *\n * The rest of the code is auto-generated. Please don't update this file\n * directly; instead, make changes to your Workbox build configuration\n * and re-run your build process.\n * See https://goo.gl/2aRDsh\n */\n\n\n\n\nself.skipWaiting();\nworkbox_core_clientsClaim();\n/**\n * The precacheAndRoute() method efficiently caches and responds to\n * requests for URLs in the manifest.\n * See https://goo.gl/S9QRab\n */\nworkbox_precaching_precacheAndRoute([\n {\n \"url\": \"person.png\",\n \"revision\": \"771d3283ece4739e203e02aac535855e\"\n },\n {\n \"url\": \"index.html\",\n \"revision\": \"6d2083467c5cde28f5f7b04204705994\"\n },\n {\n \"url\": \"icon-512.png\",\n \"revision\": \"d8e3715779f4cd7e67e1f82f215a2f22\"\n },\n {\n \"url\": \"icon-192.png\",\n \"revision\": \"940ef37caef09c52272ddab844b9ea9b\"\n },\n {\n \"url\": \"favicon.svg\",\n \"revision\": \"309e9efece69d60920618512ab9d57bf\"\n },\n {\n \"url\": \"assets/workbox-window.prod.es5-Bd17z0YL.js\",\n \"revision\": null\n },\n {\n \"url\": \"assets/index-D5c1Xz-S.js\",\n \"revision\": null\n },\n {\n \"url\": \"icon-192.png\",\n \"revision\": \"940ef37caef09c52272ddab844b9ea9b\"\n },\n {\n \"url\": \"icon-512.png\",\n \"revision\": \"d8e3715779f4cd7e67e1f82f215a2f22\"\n },\n {\n \"url\": \"manifest.webmanifest\",\n \"revision\": \"6f40f1ce0ce138d5af307fb0dbb1e6f9\"\n }\n], {});\nworkbox_precaching_cleanupOutdatedCaches();workbox_routing_registerRoute(new workbox_routing_NavigationRoute(workbox_precaching_createHandlerBoundToURL(\"index.html\"), {\n allowlist: [/^\\/$/], denylist: [/^\\/api\\//],}));\n\n\n"],"names":["self","skipWaiting","workbox_core_clientsClaim","workbox_precaching_precacheAndRoute","url","revision","workbox_precaching_cleanupOutdatedCaches","workbox_routing_registerRoute","workbox_routing_NavigationRoute","workbox_precaching_createHandlerBoundToURL","allowlist","denylist"],"mappings":"inBAoBAA,KAAKC,cACLC,EAAAA,eAMAC,EAAAA,iBAAoC,CAClC,CACEC,IAAO,aACPC,SAAY,oCAEd,CACED,IAAO,aACPC,SAAY,oCAEd,CACED,IAAO,eACPC,SAAY,oCAEd,CACED,IAAO,eACPC,SAAY,oCAEd,CACED,IAAO,cACPC,SAAY,oCAEd,CACED,IAAO,6CACPC,SAAY,MAEd,CACED,IAAO,2BACPC,SAAY,MAEd,CACED,IAAO,eACPC,SAAY,oCAEd,CACED,IAAO,eACPC,SAAY,oCAEd,CACED,IAAO,uBACPC,SAAY,qCAEb,CAAA,GACHC,EAAAA,wBAA2CC,EAAAA,cAA8B,IAAIC,EAAAA,gBAAgCC,0BAA2C,cAAe,CACrKC,UAAW,CAAC,QAAUC,SAAU,CAAC"}
|