iobroker.time-tracker 0.2.3 → 0.2.4
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 +50 -8
- package/build/lib/adapter/commands.js +20 -0
- package/build/lib/adapter/commands.js.map +2 -2
- package/build/main.js +1 -0
- package/build/main.js.map +2 -2
- package/io-package.json +14 -1
- package/package.json +1 -1
- package/www/sw.js.map +1 -1
package/README.md
CHANGED
|
@@ -160,7 +160,7 @@ Punches stay in the database; the adapter publishes aggregates and controls:
|
|
|
160
160
|
| `time-tracker.0.users.<id>.todayWorkedMinutes` | number | minutes worked today |
|
|
161
161
|
| `time-tracker.0.users.<id>.todayBalanceMinutes` | number | balance of today in minutes |
|
|
162
162
|
| `time-tracker.0.users.<id>.openConflicts` | number | punches waiting for a decision |
|
|
163
|
-
| `time-tracker.0.commands.punchUserId` | number | employee the punch commands apply to
|
|
163
|
+
| `time-tracker.0.commands.punchUserId` | number | employee the punch commands apply to (`0` = the only one) |
|
|
164
164
|
| `time-tracker.0.commands.punch` | boolean | punch in or out (button) |
|
|
165
165
|
| `time-tracker.0.commands.quickPunch` | boolean | punch with the configured quick rounding |
|
|
166
166
|
| `time-tracker.0.commands.closeMonth` | string | close a month, value `YYYY-MM` |
|
|
@@ -172,6 +172,47 @@ Punches stay in the database; the adapter publishes aggregates and controls:
|
|
|
172
172
|
| `time-tracker.0.company.openConflicts` / `company.lastPunch` | number | punches waiting for a decision, last punch |
|
|
173
173
|
| `time-tracker.0.events.lastAt` / `lastType` / `lastUser` / `lastDirection` / `lastSource` | — | newest event of the instance |
|
|
174
174
|
|
|
175
|
+
### Commands (states)
|
|
176
|
+
|
|
177
|
+
A script, a Blockly block, a dashboard or another adapter drives the instance through states — no HTTP and no login
|
|
178
|
+
involved. The adapter writes such actions into the audit log as *system* (`actorId: 0`) and the log line says what
|
|
179
|
+
happened.
|
|
180
|
+
|
|
181
|
+
| State | Value | Effect |
|
|
182
|
+
| --- | --- | --- |
|
|
183
|
+
| `commands.punchUserId` | employee id, `0` = automatic | the employee the two punch buttons apply to |
|
|
184
|
+
| `commands.punch` | `true` | punches in or out — the direction comes from the punches of the day |
|
|
185
|
+
| `commands.quickPunch` | `true` | same, but the instant is rounded with *Round quick punches to minutes* |
|
|
186
|
+
| `commands.closeMonth` | `YYYY-MM` | closes the month (the log line reports balance and overtime) |
|
|
187
|
+
| `commands.recalc` | `YYYY-MM` or `YYYY` | recalculates the aggregates of that period |
|
|
188
|
+
| `commands.backup` | `true` | writes a database backup |
|
|
189
|
+
|
|
190
|
+
```js
|
|
191
|
+
setState("time-tracker.0.commands.punchUserId", 3); // target employee (0 = the only one)
|
|
192
|
+
setState("time-tracker.0.commands.punch", true); // punch in or out
|
|
193
|
+
setState("time-tracker.0.commands.quickPunch", true); // punch with quick rounding
|
|
194
|
+
setState("time-tracker.0.commands.recalc", "2026-08"); // a month, or "2026" for a year
|
|
195
|
+
setState("time-tracker.0.commands.closeMonth", "2026-08"); // closing needs YYYY-MM
|
|
196
|
+
setState("time-tracker.0.commands.backup", true); // write a backup now
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Two things to know:
|
|
200
|
+
|
|
201
|
+
- **The buttons are stateless:** only `true` triggers them (every other value is ignored), and the adapter sets
|
|
202
|
+
them back to `false` immediately — so never write `false` to “clock out”, use `punch` and let the adapter decide
|
|
203
|
+
the direction.
|
|
204
|
+
- **Which employee?** The punch commands use the employee written into `commands.punchUserId`; the adapter mirrors
|
|
205
|
+
the current choice back into that state on every refresh, and `0` means “the only employee”. With several
|
|
206
|
+
employees and no choice the command refuses instead of guessing — the log then says
|
|
207
|
+
`several employees exist - set command_punch_user_id or write users.<id> commands`.
|
|
208
|
+
|
|
209
|
+
Per employee, without any target: write `users.<id>.present` (`true` = clock in, `false` = clock out, idempotent —
|
|
210
|
+
see *First start*) or send a message (see *Messages (`sendTo`)* below).
|
|
211
|
+
|
|
212
|
+
Every command ends with a refreshed state tree (`users.*`, `company.*`, `events.*`), so a dashboard follows along.
|
|
213
|
+
A wrong period, an unknown or deactivated employee is answered with a warning in the adapter log — never with a
|
|
214
|
+
broken instance.
|
|
215
|
+
|
|
175
216
|
### Actions (trigger rules)
|
|
176
217
|
|
|
177
218
|
The ioBroker way of connecting hardware is a state: a fingerprint reader, a button, a door contact or a dashboard
|
|
@@ -305,6 +346,14 @@ local SQLite file, access is role-based, and every correction is written to an a
|
|
|
305
346
|
|
|
306
347
|
### **WORK IN PROGRESS**
|
|
307
348
|
|
|
349
|
+
### 0.2.4 (2026-09-21)
|
|
350
|
+
|
|
351
|
+
- (Alex) `commands.punchUserId` does what it promises now: writing an employee id selects the employee the punch
|
|
352
|
+
buttons apply to (`0` = the only employee again), and the adapter mirrors the current choice back into the state.
|
|
353
|
+
For the user the README has a new *Commands (states)* section and `docs/erste-schritte.md` explains all six command
|
|
354
|
+
states — both with copy-ready examples and the two traps (buttons act on `true` only, a wrong period answers in the
|
|
355
|
+
log)
|
|
356
|
+
|
|
308
357
|
### 0.2.3 (2026-09-20)
|
|
309
358
|
|
|
310
359
|
- (Alex) fix: a corrected object definition really reaches existing installations now — `ensureObject` merges the
|
|
@@ -334,13 +383,6 @@ local SQLite file, access is role-based, and every correction is written to an a
|
|
|
334
383
|
“Database file” at the old file or by renaming it (before 0.2.0 it was `zeiterfassung.sqlite`). German UI texts
|
|
335
384
|
stay German; `common.titleLang` keeps all 11 languages
|
|
336
385
|
|
|
337
|
-
### 0.1.9 (2026-09-19)
|
|
338
|
-
|
|
339
|
-
- (Alex) ioBroker repository: two findings of the repository checker are fixed — `common.title` is removed (it is
|
|
340
|
-
deprecated, `common.titleLang` replaced it, E1084) and the `common.news` entry of 0.1.7 is gone, because that version
|
|
341
|
-
never reached npm (E2004, its pipeline was red). `npm run version:check` watches both rules from now on: it refuses
|
|
342
|
-
`common.title` and every version in the news list has to exist on npm
|
|
343
|
-
|
|
344
386
|
Older entries are kept in [`CHANGELOG_OLD.md`](CHANGELOG_OLD.md).
|
|
345
387
|
|
|
346
388
|
## Provenance
|
|
@@ -93,6 +93,26 @@ function handleCommand(deps, id, value) {
|
|
|
93
93
|
var _a;
|
|
94
94
|
const now = (_a = deps.now) != null ? _a : (() => Math.floor(Date.now() / 1e3));
|
|
95
95
|
const timestamp = now();
|
|
96
|
+
if (id === import_states.COMMAND_IDS.punchUserId) {
|
|
97
|
+
const userId = Number(value != null ? value : Number.NaN);
|
|
98
|
+
if (!Number.isInteger(userId) || userId < 0) {
|
|
99
|
+
throw new import_errors.ValidationError(`expected the id of an employee or 0 (got "${String(value != null ? value : "")}")`);
|
|
100
|
+
}
|
|
101
|
+
if (userId === 0) {
|
|
102
|
+
deps.settings.set("command_punch_user_id", 0, 0, timestamp);
|
|
103
|
+
return { ok: true, message: "punch commands use the only employee again", recalculated: [] };
|
|
104
|
+
}
|
|
105
|
+
const chosen = deps.users.findById(userId);
|
|
106
|
+
if (!chosen) {
|
|
107
|
+
throw new import_errors.ValidationError(`employee ${userId} does not exist`);
|
|
108
|
+
}
|
|
109
|
+
deps.settings.set("command_punch_user_id", userId, 0, timestamp);
|
|
110
|
+
return {
|
|
111
|
+
ok: true,
|
|
112
|
+
message: `punch commands now apply to ${chosen.displayName}`,
|
|
113
|
+
recalculated: []
|
|
114
|
+
};
|
|
115
|
+
}
|
|
96
116
|
if (id === import_states.COMMAND_IDS.closeMonth || id === import_states.COMMAND_IDS.recalc) {
|
|
97
117
|
const period = parsePeriod(String(value != null ? value : ""));
|
|
98
118
|
const target = targetUser(deps);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/adapter/commands.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Command states of the adapter.\n *\n * The adapter reacts to the states below, so a script, a dashboard or another adapter can punch, close a month\n * or trigger a recalculation without the REST API. Commands are triggered from inside ioBroker, not from a\n * browser, so no session is involved; the audit trail records them with `actorId: 0` (system).\n */\n\nimport type { Db } from \"../db/database\";\nimport type { EntriesRepository } from \"../db/repositories/entries\";\nimport type { SettingsRepository } from \"../db/repositories/settings\";\nimport type { UsersRepository } from \"../db/repositories/users\";\nimport type { AggregationService } from \"../services/aggregation\";\nimport type { ClosingService } from \"../services/closing\";\nimport type { BackupService } from \"../services/backup\";\nimport { ValidationError } from \"../errors\";\nimport { buildDayPunches, nextDirection, roundToStep, type PunchEntry } from \"../domain/punch\";\nimport { localDate as resolveLocalDate } from \"../util/time\";\nimport { COMMAND_IDS } from \"./states\";\n\n/** Data sources of the command handler. */\nexport interface CommandDeps {\n\t/** Open database handle */\n\tdb: Db;\n\t/** Punch storage */\n\tentries: EntriesRepository;\n\t/** User storage */\n\tusers: UsersRepository;\n\t/** Instance settings (quick rounding, target employee) */\n\tsettings: SettingsRepository;\n\t/** Aggregation service */\n\taggregation: Pick<AggregationService, \"recalculateDay\" | \"recalculateMonth\" | \"recalculateYear\">;\n\t/** Closing service (month close) */\n\tclosing: Pick<ClosingService, \"closeMonth\">;\n\t/** Backup service (button `commands.backup`), optional */\n\tbackup?: Pick<BackupService, \"create\">;\n\t/** Instant source, defaults to the system clock */\n\tnow?: () => number;\n}\n\n/** Result of a command. */\nexport interface CommandResult {\n\t/** True when the command was executed */\n\tok: boolean;\n\t/** Human readable summary for the log */\n\tmessage: string;\n\t/** Local dates that were recalculated */\n\trecalculated: string[];\n}\n\n/** A period taken from a command value. */\ninterface Period {\n\t/** Four digit year */\n\tyear: number;\n\t/** Month (1 to 12) or `null` for a whole year */\n\tmonth: number | null;\n}\n\n/**\n * Parses a period string.\n *\n * @param value - `YYYY-MM` or `YYYY`\n * @returns the parsed period\n */\nfunction parsePeriod(value: string): Period {\n\tconst monthMatch = /^(\\d{4})-(\\d{1,2})$/.exec(value.trim());\n\tif (monthMatch) {\n\t\tconst month = Number(monthMatch[2]);\n\t\tif (month < 1 || month > 12) {\n\t\t\tthrow new ValidationError(`month must be between 1 and 12 (got ${value})`);\n\t\t}\n\t\treturn { year: Number(monthMatch[1]), month };\n\t}\n\n\tconst yearMatch = /^(\\d{4})$/.exec(value.trim());\n\tif (yearMatch) {\n\t\treturn { year: Number(yearMatch[1]), month: null };\n\t}\n\tthrow new ValidationError(`expected YYYY-MM or YYYY (got \"${value}\")`);\n}\n\n/**\n * Resolves the employee a command applies to: the only one, or the configured one.\n *\n * @param deps - data sources\n * @returns the employee\n */\nfunction targetUser(deps: CommandDeps): { id: number; displayName: string; timezone: string } {\n\tconst configured = Number(deps.settings.get(\"command_punch_user_id\") ?? Number.NaN);\n\tconst explicit = Number.isInteger(configured) ? deps.users.findById(configured) : null;\n\tif (explicit) {\n\t\treturn explicit;\n\t}\n\n\tconst candidates = deps.users.list();\n\tif (candidates.length === 1) {\n\t\treturn candidates[0];\n\t}\n\tif (candidates.length === 0) {\n\t\tthrow new ValidationError(\"no employee exists yet\");\n\t}\n\tthrow new ValidationError(\"several employees exist - set command_punch_user_id or write users.<id> commands\");\n}\n\n/** Data a punch needs: where to write, how to round and how to recalculate. */\nexport type PunchDeps = Pick<CommandDeps, \"entries\" | \"users\" | \"settings\" | \"aggregation\" | \"now\">;\n\n/**\n * Punches in or out for one employee.\n *\n * This is the single punch path of the adapter: the `commands.punch` button, a trigger rule and a `sendTo`\n * message all end up here, so direction, rounding, note and recalculation behave the same everywhere.\n *\n * @param deps - data sources\n * @param input - employee, rounding and note\n * @param input.userId - employee the punch belongs to\n * @param input.quick - true to round the instant with the configured quick rounding\n * @param input.note - note stored with the punch\n * @param input.now - instant of the punch, defaults to the system clock\n * @returns result of the punch\n */\nexport function punchEmployee(\n\tdeps: PunchDeps,\n\tinput: { userId: number; quick?: boolean; note?: string; now?: number },\n): CommandResult {\n\tconst target = deps.users.findById(input.userId);\n\tif (!target) {\n\t\tthrow new ValidationError(`employee ${input.userId} does not exist`);\n\t}\n\tif (!target.isActive) {\n\t\tthrow new ValidationError(`${target.displayName} is deactivated, no punch was written`);\n\t}\n\n\tconst now = deps.now ?? (() => Math.floor(Date.now() / 1000));\n\tconst timestamp = input.now ?? now();\n\tconst quickRound = input.quick === true ? deps.settings.getNumber(\"quick_round_minutes\", 0) : 0;\n\tconst tsUtc = quickRound > 0 ? roundToStep(timestamp, quickRound, target.timezone) : timestamp;\n\tconst today = resolveLocalDate(tsUtc, target.timezone);\n\n\tconst existing: PunchEntry[] = deps.entries\n\t\t.listByDate(target.id, today)\n\t\t.map(entry => ({ id: entry.id, tsUtc: entry.tsUtc, syncState: entry.syncState }));\n\tconst direction = nextDirection(buildDayPunches(existing));\n\n\tconst stored = deps.entries.insert({\n\t\tuserId: target.id,\n\t\ttsUtc,\n\t\ttimeZone: target.timezone,\n\t\tsource: \"api\",\n\t\tdirection,\n\t\tnote: input.note ?? null,\n\t\tnow: timestamp,\n\t});\n\tconst day = deps.aggregation.recalculateDay(target.id, stored.entry.localDate, { now: timestamp });\n\n\treturn {\n\t\tok: true,\n\t\tmessage: `${target.displayName} punched ${direction} (${day.workedMin} min today, open: ${day.hasOpenEntry})`,\n\t\trecalculated: [stored.entry.localDate],\n\t};\n}\n\n/**\n * Handles a command state.\n *\n * @param deps - data sources\n * @param id - state id that was written (without the instance prefix)\n * @param value - value that was written\n * @returns result of the command\n */\nexport function handleCommand(deps: CommandDeps, id: string, value: ioBroker.StateValue): CommandResult {\n\tconst now = deps.now ?? (() => Math.floor(Date.now() / 1000));\n\tconst timestamp = now();\n\n\tif (id === COMMAND_IDS.closeMonth || id === COMMAND_IDS.recalc) {\n\t\tconst period = parsePeriod(String(value ?? \"\"));\n\t\tconst target = targetUser(deps);\n\n\t\tif (id === COMMAND_IDS.recalc) {\n\t\t\tif (period.month === null) {\n\t\t\t\tdeps.aggregation.recalculateYear(target.id, period.year, { now: timestamp });\n\t\t\t\treturn { ok: true, message: `year ${period.year} recalculated`, recalculated: [] };\n\t\t\t}\n\t\t\tdeps.aggregation.recalculateMonth(target.id, period.year, period.month, { now: timestamp });\n\t\t\treturn {\n\t\t\t\tok: true,\n\t\t\t\tmessage: `month ${period.year}-${String(period.month).padStart(2, \"0\")} recalculated`,\n\t\t\t\trecalculated: [],\n\t\t\t};\n\t\t}\n\n\t\tif (period.month === null) {\n\t\t\tthrow new ValidationError(\"closing a month needs YYYY-MM\");\n\t\t}\n\t\tconst closed = deps.closing.closeMonth({\n\t\t\tuserId: target.id,\n\t\t\tyear: period.year,\n\t\t\tmonth: period.month,\n\t\t\tactorId: 0,\n\t\t\tnow: timestamp,\n\t\t});\n\t\treturn {\n\t\t\tok: true,\n\t\t\tmessage: `month ${period.year}-${String(period.month).padStart(2, \"0\")} closed for ${target.displayName} (balance ${closed.month.balanceMin} min, overtime ${closed.month.overtimeMin} min)`,\n\t\t\trecalculated: [],\n\t\t};\n\t}\n\n\tif (id === COMMAND_IDS.punch || id === COMMAND_IDS.quickPunch) {\n\t\tif (value !== true) {\n\t\t\t// buttons only act on `true`\n\t\t\treturn { ok: false, message: \"ignored (buttons act on true)\", recalculated: [] };\n\t\t}\n\n\t\tconst target = targetUser(deps);\n\t\treturn punchEmployee(deps, {\n\t\t\tuserId: target.id,\n\t\t\tquick: id === COMMAND_IDS.quickPunch,\n\t\t\tnote: \"command.punch\",\n\t\t\tnow: timestamp,\n\t\t});\n\t}\n\n\tif (id === COMMAND_IDS.backup) {\n\t\tif (value !== true) {\n\t\t\t// buttons only act on `true`\n\t\t\treturn { ok: false, message: \"ignored (buttons act on true)\", recalculated: [] };\n\t\t}\n\t\tif (!deps.backup) {\n\t\t\tthrow new ValidationError(\"this instance has no backup service\");\n\t\t}\n\n\t\tconst created = deps.backup.create({ actorId: 0, reason: \"command state\" });\n\t\treturn {\n\t\t\tok: true,\n\t\t\tmessage: `backup ${created.backup.name} written (${created.backup.sizeBytes} bytes, ${created.backup.entries} punches), ${created.removed.length} old file(s) removed`,\n\t\t\trecalculated: [],\n\t\t};\n\t}\n\n\tthrow new ValidationError(`unknown command state ${id}`);\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["/**\n * Command states of the adapter.\n *\n * The adapter reacts to the states below, so a script, a dashboard or another adapter can punch, close a month\n * or trigger a recalculation without the REST API. Commands are triggered from inside ioBroker, not from a\n * browser, so no session is involved; the audit trail records them with `actorId: 0` (system).\n */\n\nimport type { Db } from \"../db/database\";\nimport type { EntriesRepository } from \"../db/repositories/entries\";\nimport type { SettingsRepository } from \"../db/repositories/settings\";\nimport type { UsersRepository } from \"../db/repositories/users\";\nimport type { AggregationService } from \"../services/aggregation\";\nimport type { ClosingService } from \"../services/closing\";\nimport type { BackupService } from \"../services/backup\";\nimport { ValidationError } from \"../errors\";\nimport { buildDayPunches, nextDirection, roundToStep, type PunchEntry } from \"../domain/punch\";\nimport { localDate as resolveLocalDate } from \"../util/time\";\nimport { COMMAND_IDS } from \"./states\";\n\n/** Data sources of the command handler. */\nexport interface CommandDeps {\n\t/** Open database handle */\n\tdb: Db;\n\t/** Punch storage */\n\tentries: EntriesRepository;\n\t/** User storage */\n\tusers: UsersRepository;\n\t/** Instance settings (quick rounding, target employee) */\n\tsettings: SettingsRepository;\n\t/** Aggregation service */\n\taggregation: Pick<AggregationService, \"recalculateDay\" | \"recalculateMonth\" | \"recalculateYear\">;\n\t/** Closing service (month close) */\n\tclosing: Pick<ClosingService, \"closeMonth\">;\n\t/** Backup service (button `commands.backup`), optional */\n\tbackup?: Pick<BackupService, \"create\">;\n\t/** Instant source, defaults to the system clock */\n\tnow?: () => number;\n}\n\n/** Result of a command. */\nexport interface CommandResult {\n\t/** True when the command was executed */\n\tok: boolean;\n\t/** Human readable summary for the log */\n\tmessage: string;\n\t/** Local dates that were recalculated */\n\trecalculated: string[];\n}\n\n/** A period taken from a command value. */\ninterface Period {\n\t/** Four digit year */\n\tyear: number;\n\t/** Month (1 to 12) or `null` for a whole year */\n\tmonth: number | null;\n}\n\n/**\n * Parses a period string.\n *\n * @param value - `YYYY-MM` or `YYYY`\n * @returns the parsed period\n */\nfunction parsePeriod(value: string): Period {\n\tconst monthMatch = /^(\\d{4})-(\\d{1,2})$/.exec(value.trim());\n\tif (monthMatch) {\n\t\tconst month = Number(monthMatch[2]);\n\t\tif (month < 1 || month > 12) {\n\t\t\tthrow new ValidationError(`month must be between 1 and 12 (got ${value})`);\n\t\t}\n\t\treturn { year: Number(monthMatch[1]), month };\n\t}\n\n\tconst yearMatch = /^(\\d{4})$/.exec(value.trim());\n\tif (yearMatch) {\n\t\treturn { year: Number(yearMatch[1]), month: null };\n\t}\n\tthrow new ValidationError(`expected YYYY-MM or YYYY (got \"${value}\")`);\n}\n\n/**\n * Resolves the employee a command applies to: the only one, or the configured one.\n *\n * @param deps - data sources\n * @returns the employee\n */\nfunction targetUser(deps: CommandDeps): { id: number; displayName: string; timezone: string } {\n\tconst configured = Number(deps.settings.get(\"command_punch_user_id\") ?? Number.NaN);\n\tconst explicit = Number.isInteger(configured) ? deps.users.findById(configured) : null;\n\tif (explicit) {\n\t\treturn explicit;\n\t}\n\n\tconst candidates = deps.users.list();\n\tif (candidates.length === 1) {\n\t\treturn candidates[0];\n\t}\n\tif (candidates.length === 0) {\n\t\tthrow new ValidationError(\"no employee exists yet\");\n\t}\n\tthrow new ValidationError(\"several employees exist - set command_punch_user_id or write users.<id> commands\");\n}\n\n/** Data a punch needs: where to write, how to round and how to recalculate. */\nexport type PunchDeps = Pick<CommandDeps, \"entries\" | \"users\" | \"settings\" | \"aggregation\" | \"now\">;\n\n/**\n * Punches in or out for one employee.\n *\n * This is the single punch path of the adapter: the `commands.punch` button, a trigger rule and a `sendTo`\n * message all end up here, so direction, rounding, note and recalculation behave the same everywhere.\n *\n * @param deps - data sources\n * @param input - employee, rounding and note\n * @param input.userId - employee the punch belongs to\n * @param input.quick - true to round the instant with the configured quick rounding\n * @param input.note - note stored with the punch\n * @param input.now - instant of the punch, defaults to the system clock\n * @returns result of the punch\n */\nexport function punchEmployee(\n\tdeps: PunchDeps,\n\tinput: { userId: number; quick?: boolean; note?: string; now?: number },\n): CommandResult {\n\tconst target = deps.users.findById(input.userId);\n\tif (!target) {\n\t\tthrow new ValidationError(`employee ${input.userId} does not exist`);\n\t}\n\tif (!target.isActive) {\n\t\tthrow new ValidationError(`${target.displayName} is deactivated, no punch was written`);\n\t}\n\n\tconst now = deps.now ?? (() => Math.floor(Date.now() / 1000));\n\tconst timestamp = input.now ?? now();\n\tconst quickRound = input.quick === true ? deps.settings.getNumber(\"quick_round_minutes\", 0) : 0;\n\tconst tsUtc = quickRound > 0 ? roundToStep(timestamp, quickRound, target.timezone) : timestamp;\n\tconst today = resolveLocalDate(tsUtc, target.timezone);\n\n\tconst existing: PunchEntry[] = deps.entries\n\t\t.listByDate(target.id, today)\n\t\t.map(entry => ({ id: entry.id, tsUtc: entry.tsUtc, syncState: entry.syncState }));\n\tconst direction = nextDirection(buildDayPunches(existing));\n\n\tconst stored = deps.entries.insert({\n\t\tuserId: target.id,\n\t\ttsUtc,\n\t\ttimeZone: target.timezone,\n\t\tsource: \"api\",\n\t\tdirection,\n\t\tnote: input.note ?? null,\n\t\tnow: timestamp,\n\t});\n\tconst day = deps.aggregation.recalculateDay(target.id, stored.entry.localDate, { now: timestamp });\n\n\treturn {\n\t\tok: true,\n\t\tmessage: `${target.displayName} punched ${direction} (${day.workedMin} min today, open: ${day.hasOpenEntry})`,\n\t\trecalculated: [stored.entry.localDate],\n\t};\n}\n\n/**\n * Handles a command state.\n *\n * @param deps - data sources\n * @param id - state id that was written (without the instance prefix)\n * @param value - value that was written\n * @returns result of the command\n */\nexport function handleCommand(deps: CommandDeps, id: string, value: ioBroker.StateValue): CommandResult {\n\tconst now = deps.now ?? (() => Math.floor(Date.now() / 1000));\n\tconst timestamp = now();\n\n\tif (id === COMMAND_IDS.punchUserId) {\n\t\t// The state chooses the employee the punch commands apply to (`0` = take the only employee again).\n\t\tconst userId = Number(value ?? Number.NaN);\n\t\tif (!Number.isInteger(userId) || userId < 0) {\n\t\t\tthrow new ValidationError(`expected the id of an employee or 0 (got \"${String(value ?? \"\")}\")`);\n\t\t}\n\t\tif (userId === 0) {\n\t\t\tdeps.settings.set(\"command_punch_user_id\", 0, 0, timestamp);\n\t\t\treturn { ok: true, message: \"punch commands use the only employee again\", recalculated: [] };\n\t\t}\n\t\tconst chosen = deps.users.findById(userId);\n\t\tif (!chosen) {\n\t\t\tthrow new ValidationError(`employee ${userId} does not exist`);\n\t\t}\n\t\tdeps.settings.set(\"command_punch_user_id\", userId, 0, timestamp);\n\t\treturn {\n\t\t\tok: true,\n\t\t\tmessage: `punch commands now apply to ${chosen.displayName}`,\n\t\t\trecalculated: [],\n\t\t};\n\t}\n\n\tif (id === COMMAND_IDS.closeMonth || id === COMMAND_IDS.recalc) {\n\t\tconst period = parsePeriod(String(value ?? \"\"));\n\t\tconst target = targetUser(deps);\n\n\t\tif (id === COMMAND_IDS.recalc) {\n\t\t\tif (period.month === null) {\n\t\t\t\tdeps.aggregation.recalculateYear(target.id, period.year, { now: timestamp });\n\t\t\t\treturn { ok: true, message: `year ${period.year} recalculated`, recalculated: [] };\n\t\t\t}\n\t\t\tdeps.aggregation.recalculateMonth(target.id, period.year, period.month, { now: timestamp });\n\t\t\treturn {\n\t\t\t\tok: true,\n\t\t\t\tmessage: `month ${period.year}-${String(period.month).padStart(2, \"0\")} recalculated`,\n\t\t\t\trecalculated: [],\n\t\t\t};\n\t\t}\n\n\t\tif (period.month === null) {\n\t\t\tthrow new ValidationError(\"closing a month needs YYYY-MM\");\n\t\t}\n\t\tconst closed = deps.closing.closeMonth({\n\t\t\tuserId: target.id,\n\t\t\tyear: period.year,\n\t\t\tmonth: period.month,\n\t\t\tactorId: 0,\n\t\t\tnow: timestamp,\n\t\t});\n\t\treturn {\n\t\t\tok: true,\n\t\t\tmessage: `month ${period.year}-${String(period.month).padStart(2, \"0\")} closed for ${target.displayName} (balance ${closed.month.balanceMin} min, overtime ${closed.month.overtimeMin} min)`,\n\t\t\trecalculated: [],\n\t\t};\n\t}\n\n\tif (id === COMMAND_IDS.punch || id === COMMAND_IDS.quickPunch) {\n\t\tif (value !== true) {\n\t\t\t// buttons only act on `true`\n\t\t\treturn { ok: false, message: \"ignored (buttons act on true)\", recalculated: [] };\n\t\t}\n\n\t\tconst target = targetUser(deps);\n\t\treturn punchEmployee(deps, {\n\t\t\tuserId: target.id,\n\t\t\tquick: id === COMMAND_IDS.quickPunch,\n\t\t\tnote: \"command.punch\",\n\t\t\tnow: timestamp,\n\t\t});\n\t}\n\n\tif (id === COMMAND_IDS.backup) {\n\t\tif (value !== true) {\n\t\t\t// buttons only act on `true`\n\t\t\treturn { ok: false, message: \"ignored (buttons act on true)\", recalculated: [] };\n\t\t}\n\t\tif (!deps.backup) {\n\t\t\tthrow new ValidationError(\"this instance has no backup service\");\n\t\t}\n\n\t\tconst created = deps.backup.create({ actorId: 0, reason: \"command state\" });\n\t\treturn {\n\t\t\tok: true,\n\t\t\tmessage: `backup ${created.backup.name} written (${created.backup.sizeBytes} bytes, ${created.backup.entries} punches), ${created.removed.length} old file(s) removed`,\n\t\t\trecalculated: [],\n\t\t};\n\t}\n\n\tthrow new ValidationError(`unknown command state ${id}`);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["resolveLocalDate"]
|
|
7
7
|
}
|
package/build/main.js
CHANGED
|
@@ -550,6 +550,7 @@ class TimeTracker extends utils.Adapter {
|
|
|
550
550
|
});
|
|
551
551
|
this.log.debug(`published ${snapshots.length} employee state(s)`);
|
|
552
552
|
await (0, import_states.publishCompanySnapshot)(this, (0, import_states.readCompanySnapshot)(snapshots));
|
|
553
|
+
await this.setState(import_states.COMMAND_IDS.punchUserId, services.settings.getNumber("command_punch_user_id", 0), true);
|
|
553
554
|
} catch (error) {
|
|
554
555
|
this.log.warn(`states could not be published: ${error.message}`);
|
|
555
556
|
}
|
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 * 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 { 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\tCOMMAND_IDS,\n\tcreateCommandStates,\n\tcreateCompanyStates,\n\tcreateEventStates,\n\tcreateInfoStates,\n\tpublishAllUserStates,\n\tpublishCompanySnapshot,\n\tpublishEventSnapshot,\n\treadCompanySnapshot,\n} from \"./lib/adapter/states\";\nimport { PRESENCE_SUFFIX, handlePresenceState, parsePresenceStateId } from \"./lib/adapter/presence\";\nimport { handleCommand, punchEmployee } 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\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\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\tawait this.refreshStates();\n\n\t\t\t// figures are refreshed regularly (the timer is cleared automatically on unload)\n\t\t\tthis.setInterval(\n\t\t\t\t() => {\n\t\t\t\t\tvoid this.publishInstanceInfo();\n\t\t\t\t\tvoid this.refreshStates();\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\n\t\t\tawait this.runScheduledBackup();\n\t\t\tthis.setInterval(() => void this.runScheduledBackup(), 60 * 60 * 1000);\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 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\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\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\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 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} 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} catch (error) {\n\t\t\tthis.log.warn(`states could not be published: ${(error as Error).message}`);\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 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 (id === COMMAND_IDS.punch || id === COMMAND_IDS.quickPunch || id === COMMAND_IDS.backup) {\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\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\tif (rule.kind === \"clockOut\" || rule.kind === \"clockIn\") {\n\t\t\t\t\t\tconst result = 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}: ${result.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\tdata: { ruleId: rule.id, action, reason: decision.reason },\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 * 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\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\n\t\t\tthis.log.debug(`Ignored state change: ${localId} = ${state.val}`);\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 { randomBytes } from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\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 { 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\tCOMMAND_IDS,\n\tcreateCommandStates,\n\tcreateCompanyStates,\n\tcreateEventStates,\n\tcreateInfoStates,\n\tpublishAllUserStates,\n\tpublishCompanySnapshot,\n\tpublishEventSnapshot,\n\treadCompanySnapshot,\n} from \"./lib/adapter/states\";\nimport { PRESENCE_SUFFIX, handlePresenceState, parsePresenceStateId } from \"./lib/adapter/presence\";\nimport { handleCommand, punchEmployee } 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\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\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\tawait this.refreshStates();\n\n\t\t\t// figures are refreshed regularly (the timer is cleared automatically on unload)\n\t\t\tthis.setInterval(\n\t\t\t\t() => {\n\t\t\t\t\tvoid this.publishInstanceInfo();\n\t\t\t\t\tvoid this.refreshStates();\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\n\t\t\tawait this.runScheduledBackup();\n\t\t\tthis.setInterval(() => void this.runScheduledBackup(), 60 * 60 * 1000);\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 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\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\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\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 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} 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\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 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 (id === COMMAND_IDS.punch || id === COMMAND_IDS.quickPunch || id === COMMAND_IDS.backup) {\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\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\tif (rule.kind === \"clockOut\" || rule.kind === \"clockIn\") {\n\t\t\t\t\t\tconst result = 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}: ${result.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\tdata: { ruleId: rule.id, action, reason: decision.reason },\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 * 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\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\n\t\t\tthis.log.debug(`Ignored state change: ${localId} = ${state.val}`);\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"]
|
|
7
7
|
}
|
package/io-package.json
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "time-tracker",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.4",
|
|
5
5
|
"nogit": true,
|
|
6
6
|
"news": {
|
|
7
|
+
"0.2.4": {
|
|
8
|
+
"en": "`commands.punchUserId` selects the employee the punch buttons apply to now (writing `0` gives the choice back to the adapter) and the current choice is mirrored into that state. The README and `docs/erste-schritte.md` describe all six command states, with examples and the two traps: the buttons act on `true` only, and a wrong period or an unknown employee is answered with a warning in the log",
|
|
9
|
+
"de": "`commands.punchUserId` wählt jetzt den Mitarbeiter, für den die Stempel-Buttons gelten (mit `0` übernimmt der Adapter die Wahl wieder selbst), und die aktuelle Wahl wird in diesen State zurückgespiegelt. README und `docs/erste-schritte.md` beschreiben alle sechs Befehls-States — mit Beispielen und den beiden Stolpersteinen: die Buttons reagieren nur auf `true`, und ein falscher Zeitraum oder unbekannter Mitarbeiter wird als Warnung im Log beantwortet",
|
|
10
|
+
"ru": "`commands.punchUserId` теперь выбирает сотрудника, к которому относятся кнопки отметки (значение `0` возвращает выбор адаптеру), а текущий выбор отображается в этом состоянии. README и `docs/erste-schritte.md` описывают все шесть командных состояний — с примерами и двумя ловушками: кнопки срабатывают только на `true`, а неверный период или неизвестный сотрудник отмечаются предупреждением в журнале",
|
|
11
|
+
"pt": "`commands.punchUserId` passa a escolher o funcionário a que se aplicam os botões de marcação (com `0` a escolha volta para o adaptador) e a escolha atual é refletida nesse estado. README e `docs/erste-schritte.md` descrevem os seis estados de comando — com exemplos e as duas armadilhas: os botões só reagem a `true` e um período errado ou um funcionário desconhecido são respondidos com um aviso no log",
|
|
12
|
+
"nl": "`commands.punchUserId` kiest nu de medewerker waarop de stempelknoppen slaan (`0` geeft de keuze weer aan de adapter) en de huidige keuze wordt in die state teruggespiegeld. README en `docs/erste-schritte.md` beschrijven alle zes opdrachtstates — met voorbeelden en de twee valkuilen: de knoppen reageren alleen op `true` en een verkeerde periode of onbekende medewerker krijgt een waarschuwing in het log",
|
|
13
|
+
"fr": "`commands.punchUserId` sélectionne désormais l'employé auquel s'appliquent les boutons de pointage (`0` rend le choix à l'adaptateur) et le choix actuel est reflété dans cet état. Le README et `docs/erste-schritte.md` décrivent les six états de commande — avec des exemples et les deux pièges : les boutons ne réagissent qu'à `true` et une période erronée ou un employé inconnu reçoit un avertissement dans le log",
|
|
14
|
+
"it": "`commands.punchUserId` ora sceglie il dipendente a cui si applicano i pulsanti di timbratura (con `0` la scelta torna all'adattatore) e la scelta attuale viene rispecchiata in questo stato. README e `docs/erste-schritte.md` descrivono tutti e sei gli stati di comando — con esempi e le due trappole: i pulsanti reagiscono solo a `true` e un periodo errato o un dipendente sconosciuto ricevono un avviso nel log",
|
|
15
|
+
"es": "`commands.punchUserId` ahora elige el empleado al que se aplican los botones de fichaje (con `0` la elección vuelve al adaptador) y la elección actual se refleja en ese estado. El README y `docs/erste-schritte.md` describen los seis estados de comando — con ejemplos y las dos trampas: los botones solo reaccionan a `true` y un periodo erróneo o un empleado desconocido reciben un aviso en el log",
|
|
16
|
+
"pl": "`commands.punchUserId` wybiera teraz pracownika, którego dotyczą przyciski rejestracji (wartość `0` oddaje wybór adapterowi), a bieżący wybór jest odzwierciedlany w tym stanie. README i `docs/erste-schritte.md` opisują wszystkie sześć stanów poleceń — z przykładami i dwiema pułapkami: przyciski reagują tylko na `true`, a błędny okres lub nieznany pracownik kończą się ostrzeżeniem w logu",
|
|
17
|
+
"uk": "`commands.punchUserId` тепер вибирає співробітника, до якого належать кнопки відмітки (значення `0` повертає вибір адаптеру), а поточний вибір відображається в цьому стані. README і `docs/erste-schritte.md` описують усі шість командних станів — з прикладами та двома пастками: кнопки реагують лише на `true`, а хибний період або невідомий співробітник отримують попередження в журналі",
|
|
18
|
+
"zh-cn": "`commands.punchUserId` 现在用于选择打卡按钮所适用的员工(写入 `0` 则由适配器重新选择),当前选择会回写到该状态。README 与 `docs/erste-schritte.md` 描述了全部六个命令状态 —— 包含示例和两个陷阱:按钮只响应 `true`,错误的期间或未知员工会在日志中以警告回应"
|
|
19
|
+
},
|
|
7
20
|
"0.2.3": {
|
|
8
21
|
"en": "fix: a corrected object definition reaches existing installations now — `ensureObject` merges the fields the adapter owns (`name`, `type`, `role`, `read`, `write`, `unit`) instead of only the name, so `commands.punchUserId` receives the role `level` on the next start (checker `E1011`)",
|
|
9
22
|
"de": "fix: eine korrigierte Objektdefinition erreicht jetzt auch bestehende Installationen — `ensureObject` mischt die adapter-eigenen Felder (`name`, `type`, `role`, `read`, `write`, `unit`) statt nur den Namen zusammen, deshalb erhält `commands.punchUserId` beim nächsten Start die Rolle `level` (Checker `E1011`)",
|
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/1bdd58ac2bbbc4fb95eb22f414115992/sw.js"],"sourcesContent":["import {clientsClaim as workbox_core_clientsClaim} from '/home/runner/work/ioBroker.time-tracker/ioBroker.time-tracker/src-pwa/node_modules/workbox-core/clientsClaim.mjs';\nimport {precacheAndRoute as workbox_precaching_precacheAndRoute} from '/home/runner/work/ioBroker.time-tracker/ioBroker.time-tracker/src-pwa/node_modules/workbox-precaching/precacheAndRoute.mjs';\nimport {cleanupOutdatedCaches as workbox_precaching_cleanupOutdatedCaches} from '/home/runner/work/ioBroker.time-tracker/ioBroker.time-tracker/src-pwa/node_modules/workbox-precaching/cleanupOutdatedCaches.mjs';\nimport {registerRoute as workbox_routing_registerRoute} from '/home/runner/work/ioBroker.time-tracker/ioBroker.time-tracker/src-pwa/node_modules/workbox-routing/registerRoute.mjs';\nimport {NavigationRoute as workbox_routing_NavigationRoute} from '/home/runner/work/ioBroker.time-tracker/ioBroker.time-tracker/src-pwa/node_modules/workbox-routing/NavigationRoute.mjs';\nimport {createHandlerBoundToURL as workbox_precaching_createHandlerBoundToURL} from '/home/runner/work/ioBroker.time-tracker/ioBroker.time-tracker/src-pwa/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\": \"45c88f7faf80a0e4c5539908dc6b94f2\"\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-QI3wrEvW.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"}
|