iobroker.time-tracker 0.2.6 → 0.2.7
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 +11 -7
- package/build/lib/adapter/commands.js +18 -3
- package/build/lib/adapter/commands.js.map +2 -2
- package/build/lib/adapter/messages.js +11 -2
- package/build/lib/adapter/messages.js.map +2 -2
- package/build/main.js +38 -3
- package/build/main.js.map +2 -2
- package/io-package.json +14 -14
- package/package.json +1 -1
- package/www/sw.js.map +1 -1
package/README.md
CHANGED
|
@@ -348,6 +348,17 @@ local SQLite file, access is role-based, and every correction is written to an a
|
|
|
348
348
|
|
|
349
349
|
### **WORK IN PROGRESS**
|
|
350
350
|
|
|
351
|
+
### 0.2.7 (2026-09-22)
|
|
352
|
+
|
|
353
|
+
- (Alex) fix: **every** punch path reaches `events.*` and the web app now. Besides the presence state (0.2.6) the
|
|
354
|
+
`commands.punch`/`quickPunch` buttons, a `sendTo` message (`punch`, `present`) and the trigger rules (Actions) wrote
|
|
355
|
+
the punch and refreshed the figures but never published an event — the event states kept the punch before. The
|
|
356
|
+
results carry `userId` and `direction` now, and `punchEvent` (like `presenceEvent`) builds the bus event for them;
|
|
357
|
+
the automation rules send the direction with their `automation.*` event
|
|
358
|
+
- (Alex) test: the end-to-end suite is deterministic again — the correction spec anchors its “forgotten day” inside the
|
|
359
|
+
local day (`now - 8h` fell on the day before when the suite ran shortly after midnight and left the employee
|
|
360
|
+
present for the specs that follow), and the presence spec puts its employee into a known state before it asserts
|
|
361
|
+
|
|
351
362
|
### 0.2.6 (2026-09-22)
|
|
352
363
|
|
|
353
364
|
- (Alex) fix: a punch from the presence state (`users.<id>.present`) is published like every other one now — it shows
|
|
@@ -376,13 +387,6 @@ local SQLite file, access is role-based, and every correction is written to an a
|
|
|
376
387
|
created before 0.2.2 kept its old `role: "value"` on `commands.punchUserId`, so the object structure check still
|
|
377
388
|
reported `E1011`
|
|
378
389
|
|
|
379
|
-
### 0.2.2 (2026-09-20)
|
|
380
|
-
|
|
381
|
-
- (Alex) object structure check: every object name carries all **eleven languages** now (`E6001`) and the employee-id
|
|
382
|
-
command uses the role **`level`** instead of `value` (`E1011`). The names live in one place
|
|
383
|
-
(`src/lib/adapter/stateNames.ts`), a unit test keeps them complete, and existing installations receive the new
|
|
384
|
-
names on the next start (`extendObject`)
|
|
385
|
-
|
|
386
390
|
Older entries are kept in [`CHANGELOG_OLD.md`](CHANGELOG_OLD.md).
|
|
387
391
|
|
|
388
392
|
## Provenance
|
|
@@ -19,7 +19,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
19
19
|
var commands_exports = {};
|
|
20
20
|
__export(commands_exports, {
|
|
21
21
|
handleCommand: () => handleCommand,
|
|
22
|
-
punchEmployee: () => punchEmployee
|
|
22
|
+
punchEmployee: () => punchEmployee,
|
|
23
|
+
punchEvent: () => punchEvent
|
|
23
24
|
});
|
|
24
25
|
module.exports = __toCommonJS(commands_exports);
|
|
25
26
|
var import_errors = require("../errors");
|
|
@@ -86,7 +87,20 @@ function punchEmployee(deps, input) {
|
|
|
86
87
|
return {
|
|
87
88
|
ok: true,
|
|
88
89
|
message: `${target.displayName} punched ${direction} (${day.workedMin} min today, open: ${day.hasOpenEntry})`,
|
|
89
|
-
recalculated: [stored.entry.localDate]
|
|
90
|
+
recalculated: [stored.entry.localDate],
|
|
91
|
+
userId: target.id,
|
|
92
|
+
direction
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function punchEvent(result, source, atUtc) {
|
|
96
|
+
if (!result.ok || !result.direction || result.userId === void 0) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
type: "punch",
|
|
101
|
+
atUtc,
|
|
102
|
+
userId: result.userId,
|
|
103
|
+
data: { direction: result.direction, source }
|
|
90
104
|
};
|
|
91
105
|
}
|
|
92
106
|
function handleCommand(deps, id, value) {
|
|
@@ -175,6 +189,7 @@ function handleCommand(deps, id, value) {
|
|
|
175
189
|
// Annotate the CommonJS export names for ESM import in node:
|
|
176
190
|
0 && (module.exports = {
|
|
177
191
|
handleCommand,
|
|
178
|
-
punchEmployee
|
|
192
|
+
punchEmployee,
|
|
193
|
+
punchEvent
|
|
179
194
|
});
|
|
180
195
|
//# sourceMappingURL=commands.js.map
|
|
@@ -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.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": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;
|
|
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 type { ApiEvent } from \"../web/events\";\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\t/** Employee a written punch belongs to \u2014 unset for every other command */\n\tuserId?: number;\n\t/** Direction of a written punch \u2014 unset for every other command */\n\tdirection?: \"in\" | \"out\";\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\tuserId: target.id,\n\t\tdirection,\n\t};\n}\n\n/**\n * Builds the event a written punch publishes.\n *\n * `punchEmployee` stays pure \u2014 the caller feeds the result into the event bus, exactly like the REST API does it.\n * That is what makes a punch from a command state, a script (`sendTo`) or a trigger rule show up in `events.*`\n * (`lastType`, `lastUser`, \u2026) and in the web app. `null` means: no punch was written.\n *\n * @param result - result of the punch\n * @param source - where the punch came from (`commands.punch`, `sendTo.punch`, `trigger.<id>`, \u2026)\n * @param atUtc - instant of the punch, UTC epoch seconds\n * @returns the event for the bus, or `null` when nothing was written\n */\nexport function punchEvent(result: CommandResult, source: string, atUtc: number): ApiEvent | null {\n\tif (!result.ok || !result.direction || result.userId === undefined) {\n\t\treturn null;\n\t}\n\treturn {\n\t\ttype: \"punch\",\n\t\tatUtc,\n\t\tuserId: result.userId,\n\t\tdata: { direction: result.direction, source },\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
|
}
|
|
@@ -123,7 +123,11 @@ async function handleMessage(deps, command, payload) {
|
|
|
123
123
|
now: now()
|
|
124
124
|
}
|
|
125
125
|
);
|
|
126
|
-
return {
|
|
126
|
+
return {
|
|
127
|
+
ok: true,
|
|
128
|
+
message: result.message,
|
|
129
|
+
data: { userId: user.id, direction: result.direction, message: result.message }
|
|
130
|
+
};
|
|
127
131
|
}
|
|
128
132
|
if (command === "present") {
|
|
129
133
|
const user = targetUser(body, deps.users);
|
|
@@ -139,7 +143,12 @@ async function handleMessage(deps, command, payload) {
|
|
|
139
143
|
return {
|
|
140
144
|
ok: result.ok,
|
|
141
145
|
message: result.message,
|
|
142
|
-
data: {
|
|
146
|
+
data: {
|
|
147
|
+
userId: user.id,
|
|
148
|
+
present: result.present,
|
|
149
|
+
changed: result.changed,
|
|
150
|
+
direction: result.changed ? result.present ? "in" : "out" : null
|
|
151
|
+
}
|
|
143
152
|
};
|
|
144
153
|
}
|
|
145
154
|
if (command === "status") {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/adapter/messages.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Messages of the adapter (`sendTo`).\n *\n * A script adapter, Blockly or another adapter can drive the time tracking without HTTP and without a session:\n * `sendTo(\"time-tracker.0\", \"punch\", { user: \"anna\" }, callback)`. Every command answers with an object, so the\n * caller can react on the outcome.\n *\n * | Command | Payload | Answer |\n * | --------- | ----------------------------------------- | ------------------------------------- |\n * | `punch` | `{ user, quick?, note? }` | `{ userId, message }` |\n * | `present` | `{ user, present }` | `{ userId, present, changed }` |\n * | `status` | `{ user }` | figures of the employee |\n * | `report` | `{ user, period?, format? }` | `{ fileName, mimeType, base64 }` |\n * | `backup` | \u2014 | `{ name, sizeBytes }` |\n *\n * `user` is a user id, a login or the shown name \u2014 the same resolution the trigger rules use.\n */\n\nimport type { AbsencesRepository } from \"../db/repositories/absences\";\nimport type { EntriesRepository } from \"../db/repositories/entries\";\nimport type { SettingsRepository } from \"../db/repositories/settings\";\nimport type { UserRecord, UsersRepository } from \"../db/repositories/users\";\nimport type { AggregationService } from \"../services/aggregation\";\nimport type { BackupService } from \"../services/backup\";\nimport type { SyncService } from \"../services/sync\";\nimport { ValidationError } from \"../errors\";\nimport { buildMonthStatement } from \"../reports/pdf\";\nimport { reportLabels } from \"../reports/labels\";\nimport { reportFileName, type ReportInput } from \"../reports/types\";\nimport { buildMonthReport } from \"../reports/xls\";\nimport { localDate } from \"../util/time\";\nimport { punchEmployee } from \"./commands\";\nimport { handlePresenceState } from \"./presence\";\nimport { readUserSnapshot } from \"./states\";\nimport { resolveTriggerUser } from \"./triggers\";\n\n/** Data a message needs. */\nexport interface MessageDeps {\n\t/** User storage */\n\tusers: UsersRepository;\n\t/** Punch storage */\n\tentries: EntriesRepository;\n\t/** Absences (the monthly statement shows them) */\n\tabsences: AbsencesRepository;\n\t/** Instance settings (quick rounding, font of the statements) */\n\tsettings: SettingsRepository;\n\t/** Aggregation service */\n\taggregation: AggregationService;\n\t/** Synchronisation service (open conflicts) */\n\tsync: SyncService;\n\t/** Backup service, optional: without it the command is refused */\n\tbackup?: Pick<BackupService, \"create\">;\n\t/** Version printed into a generated statement */\n\tversion?: string;\n\t/** Instant source, defaults to the system clock */\n\tnow?: () => number;\n}\n\n/** Answer of a message. */\nexport interface MessageResult {\n\t/** True when the command ran */\n\tok: boolean;\n\t/** Human readable summary */\n\tmessage: string;\n\t/** Command specific answer for the caller of `sendTo` */\n\tdata?: Record<string, unknown>;\n}\n\n/**\n * Reads a text field of a message payload.\n *\n * @param payload - payload of the message\n * @param field - field name\n * @returns the trimmed text or `null`\n */\nfunction textField(payload: Record<string, unknown>, field: string): string | null {\n\tconst value = payload[field];\n\tif (value === undefined || value === null) {\n\t\treturn null;\n\t}\n\tif (typeof value === \"number\" || typeof value === \"boolean\") {\n\t\treturn String(value);\n\t}\n\tif (typeof value !== \"string\") {\n\t\tthrow new ValidationError(`${field} must be a string (got ${typeof value})`);\n\t}\n\treturn value.trim() || null;\n}\n\n/**\n * Reads a boolean field of a message payload.\n *\n * @param payload - payload of the message\n * @param field - field name\n * @returns the boolean, or `null` when the field is missing\n */\nfunction booleanField(payload: Record<string, unknown>, field: string): boolean | null {\n\tconst value = payload[field];\n\tif (value === undefined || value === null) {\n\t\treturn null;\n\t}\n\tif (typeof value === \"boolean\") {\n\t\treturn value;\n\t}\n\tif (typeof value === \"number\") {\n\t\treturn value !== 0;\n\t}\n\tif (typeof value === \"string\") {\n\t\tconst normalized = value.trim().toLowerCase();\n\t\tif ([\"true\", \"1\", \"yes\", \"on\"].includes(normalized)) {\n\t\t\treturn true;\n\t\t}\n\t\tif ([\"false\", \"0\", \"no\", \"off\", \"\"].includes(normalized)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\tthrow new ValidationError(`${field} must be a boolean (got ${JSON.stringify(value)})`);\n}\n\n/**\n * Resolves the employee a message names.\n *\n * @param payload - payload of the message\n * @param users - user storage\n * @returns the employee\n */\nfunction targetUser(payload: Record<string, unknown>, users: UsersRepository): UserRecord {\n\tconst raw = payload.user ?? payload.userId;\n\tif (raw === undefined || raw === null || raw === \"\") {\n\t\tthrow new ValidationError(\"user is required: pass a user id, a login or the shown name\");\n\t}\n\t// a value of another type cannot name an employee; it is only reported back\n\tconst asText = typeof raw === \"string\" || typeof raw === \"number\" ? String(raw) : JSON.stringify(raw);\n\tconst user = resolveTriggerUser(typeof raw === \"number\" ? raw : asText, users);\n\tif (!user) {\n\t\tthrow new ValidationError(`no employee matches ${asText}`);\n\t}\n\treturn user;\n}\n\n/**\n * Builds the input of a monthly statement.\n *\n * @param deps - data sources\n * @param user - employee the statement belongs to\n * @param year - four digit year\n * @param month - month, 1 to 12\n * @returns the report input including the generator line\n */\nfunction statementInput(\n\tdeps: MessageDeps,\n\tuser: UserRecord,\n\tyear: number,\n\tmonth: number,\n): ReportInput & { generator: string } {\n\tconst timestamp = (deps.now ?? (() => Math.floor(Date.now() / 1000)))();\n\tconst prefix = `${year}-${String(month).padStart(2, \"0\")}`;\n\tconst first = `${prefix}-01`;\n\tconst lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();\n\tconst last = `${prefix}-${String(lastDay).padStart(2, \"0\")}`;\n\t// a statement has to show the current state of the month, so the range is recalculated first\n\tdeps.aggregation.recalculateRange(user.id, first, last, { now: timestamp });\n\tconst { labels, language } = reportLabels(user.locale);\n\n\treturn {\n\t\tuser: { displayName: user.displayName, login: user.login, timezone: user.timezone },\n\t\tlabels,\n\t\tlanguage,\n\t\tlocale: user.locale || \"en\",\n\t\tyear,\n\t\tmonth,\n\t\tdays: deps.aggregation.days(user.id, first, last),\n\t\tabsences: deps.absences.withTypesInRange(user.id, first, last).map(absence => ({\n\t\t\ttypeCode: absence.typeCode,\n\t\t\ttypeName: absence.typeName,\n\t\t\tdateFrom: absence.dateFrom,\n\t\t\tdateTo: absence.dateTo,\n\t\t\tdayPortion: absence.dayPortion,\n\t\t\thours: absence.hours,\n\t\t})),\n\t\tgeneratedAt: timestamp,\n\t\tgenerator: `time-tracker ${deps.version ?? \"\"}`.trim(),\n\t};\n}\n\n/**\n * Applies a message of a script, a Blockly block or another adapter.\n *\n * @param deps - data sources\n * @param command - name of the command\n * @param payload - payload of the message\n * @returns the answer for the caller\n */\nexport async function handleMessage(deps: MessageDeps, command: string, payload: unknown): Promise<MessageResult> {\n\tconst body = (payload ?? {}) as Record<string, unknown>;\n\tconst now = deps.now ?? (() => Math.floor(Date.now() / 1000));\n\n\tif (command === \"punch\") {\n\t\tconst user = targetUser(body, deps.users);\n\t\tconst result = punchEmployee(\n\t\t\t{ entries: deps.entries, users: deps.users, settings: deps.settings, aggregation: deps.aggregation },\n\t\t\t{\n\t\t\t\tuserId: user.id,\n\t\t\t\tquick: booleanField(body, \"quick\") === true,\n\t\t\t\tnote: textField(body, \"note\") ?? \"sendTo.punch\",\n\t\t\t\tnow: now(),\n\t\t\t},\n\t\t);\n\t\treturn { ok: true, message: result.message, data: { userId: user.id, message: result.message } };\n\t}\n\n\tif (command === \"present\") {\n\t\tconst user = targetUser(body, deps.users);\n\t\tconst desired = booleanField(body, \"present\");\n\t\tif (desired === null) {\n\t\t\tthrow new ValidationError(\"present is required: pass true or false\");\n\t\t}\n\t\tconst result = handlePresenceState(\n\t\t\t{ entries: deps.entries, users: deps.users, aggregation: deps.aggregation, now },\n\t\t\t`users.${user.id}.present`,\n\t\t\tdesired,\n\t\t);\n\t\treturn {\n\t\t\tok: result.ok,\n\t\t\tmessage: result.message,\n\t\t\tdata: { userId: user.id, present: result.present, changed: result.changed },\n\t\t};\n\t}\n\n\tif (command === \"status\") {\n\t\tconst user = targetUser(body, deps.users);\n\t\tconst snapshot = readUserSnapshot({\n\t\t\taggregation: deps.aggregation,\n\t\t\tusers: deps.users,\n\t\t\tsync: deps.sync,\n\t\t\tuserId: user.id,\n\t\t\tnow: now(),\n\t\t});\n\t\tif (!snapshot) {\n\t\t\tthrow new ValidationError(`employee ${user.id} disappeared`);\n\t\t}\n\t\treturn { ok: true, message: `figures of ${user.displayName}`, data: { ...snapshot } };\n\t}\n\n\tif (command === \"report\") {\n\t\tconst user = targetUser(body, deps.users);\n\t\tconst format = (textField(body, \"format\") ?? \"pdf\").toLowerCase();\n\t\tif (format !== \"pdf\" && format !== \"xlsx\") {\n\t\t\tthrow new ValidationError(`format must be pdf or xlsx (got ${format})`);\n\t\t}\n\t\tconst period = textField(body, \"period\") ?? localDate(now(), user.timezone).slice(0, 7);\n\t\tconst match = /^(\\d{4})-(\\d{2})$/.exec(period);\n\t\tif (!match) {\n\t\t\tthrow new ValidationError(`period must be YYYY-MM (got \"${period}\")`);\n\t\t}\n\t\tconst year = Number(match[1]);\n\t\tconst month = Number(match[2]);\n\t\tif (month < 1 || month > 12) {\n\t\t\tthrow new ValidationError(`month must be between 1 and 12 (got ${month})`);\n\t\t}\n\n\t\tconst input = statementInput(deps, user, year, month);\n\t\tconst file =\n\t\t\tformat === \"pdf\"\n\t\t\t\t? await buildMonthStatement({ ...input, fontPath: deps.settings.get(\"report_font_path\") ?? \"\" })\n\t\t\t\t: await buildMonthReport(input);\n\t\tconst fileName = reportFileName(user.login, year, month, format === \"pdf\" ? \"pdf\" : \"xlsx\");\n\n\t\treturn {\n\t\t\tok: true,\n\t\t\tmessage: `${fileName} (${file.length} bytes)`,\n\t\t\tdata: {\n\t\t\t\tuserId: user.id,\n\t\t\t\tfileName,\n\t\t\t\tmimeType:\n\t\t\t\t\tformat === \"pdf\"\n\t\t\t\t\t\t? \"application/pdf\"\n\t\t\t\t\t\t: \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n\t\t\t\tbytes: file.length,\n\t\t\t\tbase64: file.toString(\"base64\"),\n\t\t\t},\n\t\t};\n\t}\n\n\tif (command === \"backup\") {\n\t\tif (!deps.backup) {\n\t\t\tthrow new ValidationError(\"this instance has no backup service\");\n\t\t}\n\t\tconst created = deps.backup.create({ actorId: 0, reason: \"sendTo\" });\n\t\treturn {\n\t\t\tok: true,\n\t\t\tmessage: `backup ${created.backup.name} written (${created.backup.sizeBytes} bytes)`,\n\t\t\tdata: { name: created.backup.name, sizeBytes: created.backup.sizeBytes, removed: created.removed },\n\t\t};\n\t}\n\n\tthrow new ValidationError(`unknown command \"${command}\": use punch, present, status, report or backup`);\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["/**\n * Messages of the adapter (`sendTo`).\n *\n * A script adapter, Blockly or another adapter can drive the time tracking without HTTP and without a session:\n * `sendTo(\"time-tracker.0\", \"punch\", { user: \"anna\" }, callback)`. Every command answers with an object, so the\n * caller can react on the outcome.\n *\n * | Command | Payload | Answer |\n * | --------- | ----------------------------------------- | ------------------------------------- |\n * | `punch` | `{ user, quick?, note? }` | `{ userId, message }` |\n * | `present` | `{ user, present }` | `{ userId, present, changed }` |\n * | `status` | `{ user }` | figures of the employee |\n * | `report` | `{ user, period?, format? }` | `{ fileName, mimeType, base64 }` |\n * | `backup` | \u2014 | `{ name, sizeBytes }` |\n *\n * `user` is a user id, a login or the shown name \u2014 the same resolution the trigger rules use.\n */\n\nimport type { AbsencesRepository } from \"../db/repositories/absences\";\nimport type { EntriesRepository } from \"../db/repositories/entries\";\nimport type { SettingsRepository } from \"../db/repositories/settings\";\nimport type { UserRecord, UsersRepository } from \"../db/repositories/users\";\nimport type { AggregationService } from \"../services/aggregation\";\nimport type { BackupService } from \"../services/backup\";\nimport type { SyncService } from \"../services/sync\";\nimport { ValidationError } from \"../errors\";\nimport { buildMonthStatement } from \"../reports/pdf\";\nimport { reportLabels } from \"../reports/labels\";\nimport { reportFileName, type ReportInput } from \"../reports/types\";\nimport { buildMonthReport } from \"../reports/xls\";\nimport { localDate } from \"../util/time\";\nimport { punchEmployee } from \"./commands\";\nimport { handlePresenceState } from \"./presence\";\nimport { readUserSnapshot } from \"./states\";\nimport { resolveTriggerUser } from \"./triggers\";\n\n/** Data a message needs. */\nexport interface MessageDeps {\n\t/** User storage */\n\tusers: UsersRepository;\n\t/** Punch storage */\n\tentries: EntriesRepository;\n\t/** Absences (the monthly statement shows them) */\n\tabsences: AbsencesRepository;\n\t/** Instance settings (quick rounding, font of the statements) */\n\tsettings: SettingsRepository;\n\t/** Aggregation service */\n\taggregation: AggregationService;\n\t/** Synchronisation service (open conflicts) */\n\tsync: SyncService;\n\t/** Backup service, optional: without it the command is refused */\n\tbackup?: Pick<BackupService, \"create\">;\n\t/** Version printed into a generated statement */\n\tversion?: string;\n\t/** Instant source, defaults to the system clock */\n\tnow?: () => number;\n}\n\n/** Answer of a message. */\nexport interface MessageResult {\n\t/** True when the command ran */\n\tok: boolean;\n\t/** Human readable summary */\n\tmessage: string;\n\t/** Command specific answer for the caller of `sendTo` */\n\tdata?: Record<string, unknown>;\n}\n\n/**\n * Reads a text field of a message payload.\n *\n * @param payload - payload of the message\n * @param field - field name\n * @returns the trimmed text or `null`\n */\nfunction textField(payload: Record<string, unknown>, field: string): string | null {\n\tconst value = payload[field];\n\tif (value === undefined || value === null) {\n\t\treturn null;\n\t}\n\tif (typeof value === \"number\" || typeof value === \"boolean\") {\n\t\treturn String(value);\n\t}\n\tif (typeof value !== \"string\") {\n\t\tthrow new ValidationError(`${field} must be a string (got ${typeof value})`);\n\t}\n\treturn value.trim() || null;\n}\n\n/**\n * Reads a boolean field of a message payload.\n *\n * @param payload - payload of the message\n * @param field - field name\n * @returns the boolean, or `null` when the field is missing\n */\nfunction booleanField(payload: Record<string, unknown>, field: string): boolean | null {\n\tconst value = payload[field];\n\tif (value === undefined || value === null) {\n\t\treturn null;\n\t}\n\tif (typeof value === \"boolean\") {\n\t\treturn value;\n\t}\n\tif (typeof value === \"number\") {\n\t\treturn value !== 0;\n\t}\n\tif (typeof value === \"string\") {\n\t\tconst normalized = value.trim().toLowerCase();\n\t\tif ([\"true\", \"1\", \"yes\", \"on\"].includes(normalized)) {\n\t\t\treturn true;\n\t\t}\n\t\tif ([\"false\", \"0\", \"no\", \"off\", \"\"].includes(normalized)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\tthrow new ValidationError(`${field} must be a boolean (got ${JSON.stringify(value)})`);\n}\n\n/**\n * Resolves the employee a message names.\n *\n * @param payload - payload of the message\n * @param users - user storage\n * @returns the employee\n */\nfunction targetUser(payload: Record<string, unknown>, users: UsersRepository): UserRecord {\n\tconst raw = payload.user ?? payload.userId;\n\tif (raw === undefined || raw === null || raw === \"\") {\n\t\tthrow new ValidationError(\"user is required: pass a user id, a login or the shown name\");\n\t}\n\t// a value of another type cannot name an employee; it is only reported back\n\tconst asText = typeof raw === \"string\" || typeof raw === \"number\" ? String(raw) : JSON.stringify(raw);\n\tconst user = resolveTriggerUser(typeof raw === \"number\" ? raw : asText, users);\n\tif (!user) {\n\t\tthrow new ValidationError(`no employee matches ${asText}`);\n\t}\n\treturn user;\n}\n\n/**\n * Builds the input of a monthly statement.\n *\n * @param deps - data sources\n * @param user - employee the statement belongs to\n * @param year - four digit year\n * @param month - month, 1 to 12\n * @returns the report input including the generator line\n */\nfunction statementInput(\n\tdeps: MessageDeps,\n\tuser: UserRecord,\n\tyear: number,\n\tmonth: number,\n): ReportInput & { generator: string } {\n\tconst timestamp = (deps.now ?? (() => Math.floor(Date.now() / 1000)))();\n\tconst prefix = `${year}-${String(month).padStart(2, \"0\")}`;\n\tconst first = `${prefix}-01`;\n\tconst lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();\n\tconst last = `${prefix}-${String(lastDay).padStart(2, \"0\")}`;\n\t// a statement has to show the current state of the month, so the range is recalculated first\n\tdeps.aggregation.recalculateRange(user.id, first, last, { now: timestamp });\n\tconst { labels, language } = reportLabels(user.locale);\n\n\treturn {\n\t\tuser: { displayName: user.displayName, login: user.login, timezone: user.timezone },\n\t\tlabels,\n\t\tlanguage,\n\t\tlocale: user.locale || \"en\",\n\t\tyear,\n\t\tmonth,\n\t\tdays: deps.aggregation.days(user.id, first, last),\n\t\tabsences: deps.absences.withTypesInRange(user.id, first, last).map(absence => ({\n\t\t\ttypeCode: absence.typeCode,\n\t\t\ttypeName: absence.typeName,\n\t\t\tdateFrom: absence.dateFrom,\n\t\t\tdateTo: absence.dateTo,\n\t\t\tdayPortion: absence.dayPortion,\n\t\t\thours: absence.hours,\n\t\t})),\n\t\tgeneratedAt: timestamp,\n\t\tgenerator: `time-tracker ${deps.version ?? \"\"}`.trim(),\n\t};\n}\n\n/**\n * Applies a message of a script, a Blockly block or another adapter.\n *\n * @param deps - data sources\n * @param command - name of the command\n * @param payload - payload of the message\n * @returns the answer for the caller\n */\nexport async function handleMessage(deps: MessageDeps, command: string, payload: unknown): Promise<MessageResult> {\n\tconst body = (payload ?? {}) as Record<string, unknown>;\n\tconst now = deps.now ?? (() => Math.floor(Date.now() / 1000));\n\n\tif (command === \"punch\") {\n\t\tconst user = targetUser(body, deps.users);\n\t\tconst result = punchEmployee(\n\t\t\t{ entries: deps.entries, users: deps.users, settings: deps.settings, aggregation: deps.aggregation },\n\t\t\t{\n\t\t\t\tuserId: user.id,\n\t\t\t\tquick: booleanField(body, \"quick\") === true,\n\t\t\t\tnote: textField(body, \"note\") ?? \"sendTo.punch\",\n\t\t\t\tnow: now(),\n\t\t\t},\n\t\t);\n\t\treturn {\n\t\t\tok: true,\n\t\t\tmessage: result.message,\n\t\t\tdata: { userId: user.id, direction: result.direction, message: result.message },\n\t\t};\n\t}\n\n\tif (command === \"present\") {\n\t\tconst user = targetUser(body, deps.users);\n\t\tconst desired = booleanField(body, \"present\");\n\t\tif (desired === null) {\n\t\t\tthrow new ValidationError(\"present is required: pass true or false\");\n\t\t}\n\t\tconst result = handlePresenceState(\n\t\t\t{ entries: deps.entries, users: deps.users, aggregation: deps.aggregation, now },\n\t\t\t`users.${user.id}.present`,\n\t\t\tdesired,\n\t\t);\n\t\treturn {\n\t\t\tok: result.ok,\n\t\t\tmessage: result.message,\n\t\t\tdata: {\n\t\t\t\tuserId: user.id,\n\t\t\t\tpresent: result.present,\n\t\t\t\tchanged: result.changed,\n\t\t\t\tdirection: result.changed ? (result.present ? \"in\" : \"out\") : null,\n\t\t\t},\n\t\t};\n\t}\n\n\tif (command === \"status\") {\n\t\tconst user = targetUser(body, deps.users);\n\t\tconst snapshot = readUserSnapshot({\n\t\t\taggregation: deps.aggregation,\n\t\t\tusers: deps.users,\n\t\t\tsync: deps.sync,\n\t\t\tuserId: user.id,\n\t\t\tnow: now(),\n\t\t});\n\t\tif (!snapshot) {\n\t\t\tthrow new ValidationError(`employee ${user.id} disappeared`);\n\t\t}\n\t\treturn { ok: true, message: `figures of ${user.displayName}`, data: { ...snapshot } };\n\t}\n\n\tif (command === \"report\") {\n\t\tconst user = targetUser(body, deps.users);\n\t\tconst format = (textField(body, \"format\") ?? \"pdf\").toLowerCase();\n\t\tif (format !== \"pdf\" && format !== \"xlsx\") {\n\t\t\tthrow new ValidationError(`format must be pdf or xlsx (got ${format})`);\n\t\t}\n\t\tconst period = textField(body, \"period\") ?? localDate(now(), user.timezone).slice(0, 7);\n\t\tconst match = /^(\\d{4})-(\\d{2})$/.exec(period);\n\t\tif (!match) {\n\t\t\tthrow new ValidationError(`period must be YYYY-MM (got \"${period}\")`);\n\t\t}\n\t\tconst year = Number(match[1]);\n\t\tconst month = Number(match[2]);\n\t\tif (month < 1 || month > 12) {\n\t\t\tthrow new ValidationError(`month must be between 1 and 12 (got ${month})`);\n\t\t}\n\n\t\tconst input = statementInput(deps, user, year, month);\n\t\tconst file =\n\t\t\tformat === \"pdf\"\n\t\t\t\t? await buildMonthStatement({ ...input, fontPath: deps.settings.get(\"report_font_path\") ?? \"\" })\n\t\t\t\t: await buildMonthReport(input);\n\t\tconst fileName = reportFileName(user.login, year, month, format === \"pdf\" ? \"pdf\" : \"xlsx\");\n\n\t\treturn {\n\t\t\tok: true,\n\t\t\tmessage: `${fileName} (${file.length} bytes)`,\n\t\t\tdata: {\n\t\t\t\tuserId: user.id,\n\t\t\t\tfileName,\n\t\t\t\tmimeType:\n\t\t\t\t\tformat === \"pdf\"\n\t\t\t\t\t\t? \"application/pdf\"\n\t\t\t\t\t\t: \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n\t\t\t\tbytes: file.length,\n\t\t\t\tbase64: file.toString(\"base64\"),\n\t\t\t},\n\t\t};\n\t}\n\n\tif (command === \"backup\") {\n\t\tif (!deps.backup) {\n\t\t\tthrow new ValidationError(\"this instance has no backup service\");\n\t\t}\n\t\tconst created = deps.backup.create({ actorId: 0, reason: \"sendTo\" });\n\t\treturn {\n\t\t\tok: true,\n\t\t\tmessage: `backup ${created.backup.name} written (${created.backup.sizeBytes} bytes)`,\n\t\t\tdata: { name: created.backup.name, sizeBytes: created.backup.sizeBytes, removed: created.removed },\n\t\t};\n\t}\n\n\tthrow new ValidationError(`unknown command \"${command}\": use punch, present, status, report or backup`);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/build/main.js
CHANGED
|
@@ -588,6 +588,7 @@ class TimeTracker extends utils.Adapter {
|
|
|
588
588
|
value
|
|
589
589
|
);
|
|
590
590
|
this.log.info(`command ${id}: ${result.message}`);
|
|
591
|
+
this.publishPunch((0, import_commands.punchEvent)(result, `commands.${id}`, Math.floor(Date.now() / 1e3)));
|
|
591
592
|
if (id === import_states.COMMAND_IDS.closeMonth) {
|
|
592
593
|
(_a = this.events) == null ? void 0 : _a.publish({
|
|
593
594
|
type: "month.close",
|
|
@@ -719,6 +720,12 @@ class TimeTracker extends utils.Adapter {
|
|
|
719
720
|
{ userId, quick: rule.action === "quickPunch", note: `trigger.${rule.id}` }
|
|
720
721
|
);
|
|
721
722
|
this.log.info(`trigger ${rule.id} (${rule.sourceState}) \u2192 ${result.message}`);
|
|
723
|
+
const atUtc = Math.floor(Date.now() / 1e3);
|
|
724
|
+
if ("changed" in result) {
|
|
725
|
+
this.publishPunch((0, import_presence.presenceEvent)(result, atUtc));
|
|
726
|
+
} else {
|
|
727
|
+
this.publishPunch((0, import_commands.punchEvent)(result, `trigger.${rule.id}`, atUtc));
|
|
728
|
+
}
|
|
722
729
|
services.triggers.markFired({ id: rule.id });
|
|
723
730
|
await this.refreshStates();
|
|
724
731
|
} catch (error) {
|
|
@@ -817,8 +824,9 @@ class TimeTracker extends utils.Adapter {
|
|
|
817
824
|
continue;
|
|
818
825
|
}
|
|
819
826
|
try {
|
|
827
|
+
let punched = null;
|
|
820
828
|
if (rule.kind === "clockOut" || rule.kind === "clockIn") {
|
|
821
|
-
|
|
829
|
+
punched = (0, import_commands.punchEmployee)(
|
|
822
830
|
{
|
|
823
831
|
entries: services.entries,
|
|
824
832
|
users: services.users,
|
|
@@ -827,7 +835,7 @@ class TimeTracker extends utils.Adapter {
|
|
|
827
835
|
},
|
|
828
836
|
{ userId: user.id, note: `auto.${rule.kind}`, now: nowUtc }
|
|
829
837
|
);
|
|
830
|
-
this.log.info(`automation ${rule.id} for ${user.displayName}: ${
|
|
838
|
+
this.log.info(`automation ${rule.id} for ${user.displayName}: ${punched.message}`);
|
|
831
839
|
} else {
|
|
832
840
|
this.log.info(`automation ${rule.id} for ${user.displayName}: ${action}`);
|
|
833
841
|
}
|
|
@@ -836,7 +844,8 @@ class TimeTracker extends utils.Adapter {
|
|
|
836
844
|
type: `automation.${rule.kind}`,
|
|
837
845
|
atUtc: nowUtc,
|
|
838
846
|
userId: user.id,
|
|
839
|
-
|
|
847
|
+
// the direction belongs into the event when the rule wrote a punch (checker: `events.lastDirection`)
|
|
848
|
+
data: { ruleId: rule.id, action, reason: decision.reason, direction: punched == null ? void 0 : punched.direction }
|
|
840
849
|
});
|
|
841
850
|
} catch (error) {
|
|
842
851
|
services.automations.forgetRun({ ruleId: rule.id, userId: user.id, period });
|
|
@@ -911,6 +920,21 @@ class TimeTracker extends utils.Adapter {
|
|
|
911
920
|
this.log.debug(`event state could not be published: ${error.message}`);
|
|
912
921
|
}
|
|
913
922
|
}
|
|
923
|
+
/**
|
|
924
|
+
* Publishes a punch on the event bus, so it reaches `events.*` and the connected web clients.
|
|
925
|
+
*
|
|
926
|
+
* A punch can be written from several places — the command states, a `sendTo` message, a trigger rule, an
|
|
927
|
+
* automation rule and the presence state. Every one of them passes its result through here; a path that forgets
|
|
928
|
+
* it leaves the event states (`lastType`, `lastUser`, …) on the punch before.
|
|
929
|
+
*
|
|
930
|
+
* @param event - event built from the punch, `null` when nothing was written
|
|
931
|
+
*/
|
|
932
|
+
publishPunch(event) {
|
|
933
|
+
var _a;
|
|
934
|
+
if (event) {
|
|
935
|
+
(_a = this.events) == null ? void 0 : _a.publish(event);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
914
938
|
/**
|
|
915
939
|
* Is called when another adapter sends a message (`sendTo`).
|
|
916
940
|
*
|
|
@@ -918,6 +942,7 @@ class TimeTracker extends utils.Adapter {
|
|
|
918
942
|
*/
|
|
919
943
|
onMessage(obj) {
|
|
920
944
|
void (async () => {
|
|
945
|
+
var _a, _b;
|
|
921
946
|
const services = this.services;
|
|
922
947
|
if (!services) {
|
|
923
948
|
return;
|
|
@@ -938,6 +963,16 @@ class TimeTracker extends utils.Adapter {
|
|
|
938
963
|
obj.message
|
|
939
964
|
);
|
|
940
965
|
this.log.info(`message ${obj.command}: ${answer.message}`);
|
|
966
|
+
const direction = (_a = answer.data) == null ? void 0 : _a.direction;
|
|
967
|
+
const userId = (_b = answer.data) == null ? void 0 : _b.userId;
|
|
968
|
+
if (typeof direction === "string" && typeof userId === "number") {
|
|
969
|
+
this.publishPunch({
|
|
970
|
+
type: "punch",
|
|
971
|
+
atUtc: Math.floor(Date.now() / 1e3),
|
|
972
|
+
userId,
|
|
973
|
+
data: { direction, source: `sendTo.${obj.command}` }
|
|
974
|
+
});
|
|
975
|
+
}
|
|
941
976
|
if (obj.callback) {
|
|
942
977
|
this.sendTo(obj.from, obj.command, answer, obj.callback);
|
|
943
978
|
}
|
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, presenceEvent } 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\t// A punch from the presence state is published like every other one: the subscription in `onReady`\n\t\t\t\t// mirrors it into `events.*` and tells the connected web clients.\n\t\t\t\tconst event = presenceEvent(result, Math.floor(Date.now() / 1000));\n\t\t\t\tif (event) {\n\t\t\t\t\tthis.events?.publish(event);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// a value the state does not understand, an unknown employee: worth a warning, not an error\n\t\t\t\tthis.log.warn(`presence ${id}: ${result.message}`);\n\t\t\t}\n\t\t\tawait this.refreshStates();\n\t\t} catch (error) {\n\t\t\tthis.log.error(`Error in runPresence for ${id}: ${(error as Error).message}`);\n\t\t}\n\t}\n\n\t/**\n\t * Publishes the figures of all employees.\n\t */\n\tprivate async refreshStates(): Promise<void> {\n\t\tconst services = this.services;\n\t\tif (!services) {\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tconst snapshots = await publishAllUserStates({\n\t\t\t\tport: this,\n\t\t\t\taggregation: services.aggregation,\n\t\t\t\tusers: services.users,\n\t\t\t\tsync: services.sync,\n\t\t\t});\n\t\t\tthis.log.debug(`published ${snapshots.length} employee state(s)`);\n\t\t\t// the company figures are derived from the same snapshots, so they never disagree\n\t\t\tawait publishCompanySnapshot(this, readCompanySnapshot(snapshots));\n\t\t\t// the target employee of the punch commands is mirrored into the state the user writes (acknowledged, so\n\t\t\t// the adapter ignores its own write \u2014 see `onStateChange`). `0` means \"the only employee\".\n\t\t\tawait this.setState(COMMAND_IDS.punchUserId, services.settings.getNumber(\"command_punch_user_id\", 0), true);\n\t\t} catch (error) {\n\t\t\tthis.log.warn(`states could not be published: ${(error as Error).message}`);\n\t\t}\n\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, presenceEvent } from \"./lib/adapter/presence\";\nimport { handleCommand, punchEmployee, punchEvent, type CommandResult } from \"./lib/adapter/commands\";\nimport { evaluateTrigger, triggerText } from \"./lib/adapter/triggers\";\nimport { evaluateAutomation, isoWeekday, runPeriod, workBlock } from \"./lib/adapter/automation\";\nimport { handleMessage } from \"./lib/adapter/messages\";\nimport { createApi, MAX_BACKUP_UPLOAD_BYTES } from \"./lib/web/api\";\nimport type { ApiEvent, EventBus } from \"./lib/web/events\";\nimport { startWebServer, type WebServer } from \"./lib/web/server\";\nimport { localDateTime } from \"./lib/util/time\";\nimport { createStaticHandler } from \"./lib/web/static\";\n\nconst SUPPORTED_COUNTRIES: HolidayCountry[] = [\"CH\", \"DE\", \"AT\"];\n\n/** How often expired sessions are removed (minutes). */\nconst SESSION_PURGE_MINUTES = 30;\n\n/** How often the published figures are refreshed (minutes). */\nconst STATE_REFRESH_MINUTES = 5;\n\n/** How old the newest backup may be before the daily check writes a new one. */\nconst BACKUP_MAX_AGE_HOURS = 20;\n\n/** How often the automation rules are checked (minutes). */\nconst AUTOMATION_CHECK_MINUTES = 1;\n\n/** Name of the file that holds a generated badge (HMAC) secret next to the database. */\nconst TAG_SECRET_FILE_NAME = \"tag-secret\";\n\n/**\n * Events of the API that change the published figures.\n *\n * A punch or a correction arrives through the API of this very process, so nothing would republish the states\n * until the five minute timer fires; these events say exactly when data changed.\n */\nconst FIGURES_EVENT_TYPES = [\"punch\", \"terminal.punch\", \"rfid.scan\", \"entry.update\", \"entry.delete\", \"absence.change\"];\n\n/** Services created at startup. */\ninterface AdapterServices {\n\tusers: UsersRepository;\n\tentries: EntriesRepository;\n\tabsences: AbsencesRepository;\n\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\t// A punch from the presence state is published like every other one: the subscription in `onReady`\n\t\t\t\t// mirrors it into `events.*` and tells the connected web clients.\n\t\t\t\tconst event = presenceEvent(result, Math.floor(Date.now() / 1000));\n\t\t\t\tif (event) {\n\t\t\t\t\tthis.events?.publish(event);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// a value the state does not understand, an unknown employee: worth a warning, not an error\n\t\t\t\tthis.log.warn(`presence ${id}: ${result.message}`);\n\t\t\t}\n\t\t\tawait this.refreshStates();\n\t\t} catch (error) {\n\t\t\tthis.log.error(`Error in runPresence for ${id}: ${(error as Error).message}`);\n\t\t}\n\t}\n\n\t/**\n\t * Publishes the figures of all employees.\n\t */\n\tprivate async refreshStates(): Promise<void> {\n\t\tconst services = this.services;\n\t\tif (!services) {\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tconst snapshots = await publishAllUserStates({\n\t\t\t\tport: this,\n\t\t\t\taggregation: services.aggregation,\n\t\t\t\tusers: services.users,\n\t\t\t\tsync: services.sync,\n\t\t\t});\n\t\t\tthis.log.debug(`published ${snapshots.length} employee state(s)`);\n\t\t\t// the company figures are derived from the same snapshots, so they never disagree\n\t\t\tawait publishCompanySnapshot(this, readCompanySnapshot(snapshots));\n\t\t\t// the target employee of the punch commands is mirrored into the state the user writes (acknowledged, so\n\t\t\t// the adapter ignores its own write \u2014 see `onStateChange`). `0` means \"the only employee\".\n\t\t\tawait this.setState(COMMAND_IDS.punchUserId, services.settings.getNumber(\"command_punch_user_id\", 0), true);\n\t\t} catch (error) {\n\t\t\tthis.log.warn(`states could not be published: ${(error as Error).message}`);\n\t\t}\n\t}\n\n\t/**\n\t * Runs a command state and reports the outcome.\n\t *\n\t * @param id - state id without the instance prefix\n\t * @param value - value written by the user or a script\n\t */\n\tprivate async runCommand(id: string, value: ioBroker.StateValue): Promise<void> {\n\t\tconst services = this.services;\n\t\tconst db = this.db;\n\t\tif (!services || !db) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tconst result = handleCommand(\n\t\t\t\t{\n\t\t\t\t\tdb,\n\t\t\t\t\tentries: services.entries,\n\t\t\t\t\tusers: services.users,\n\t\t\t\t\tsettings: services.settings,\n\t\t\t\t\taggregation: services.aggregation,\n\t\t\t\t\tclosing: services.closing,\n\t\t\t\t\tbackup: services.backup,\n\t\t\t\t},\n\t\t\t\tid,\n\t\t\t\tvalue,\n\t\t\t);\n\t\t\tthis.log.info(`command ${id}: ${result.message}`);\n\t\t\t// a punch written by a command state lands in `events.*` and in the web app like every other one\n\t\t\tthis.publishPunch(punchEvent(result, `commands.${id}`, Math.floor(Date.now() / 1000)));\n\t\t\t// a closing is triggered from a state, not from a request: tell the connected clients about it\n\t\t\t// (only the period is published, never the figures of the employee it belongs to)\n\t\t\tif (id === COMMAND_IDS.closeMonth) {\n\t\t\t\tthis.events?.publish({\n\t\t\t\t\ttype: \"month.close\",\n\t\t\t\t\tatUtc: Math.floor(Date.now() / 1000),\n\t\t\t\t\tuserId: null,\n\t\t\t\t\tdata: { period: String(value ?? \"\") },\n\t\t\t\t});\n\t\t\t}\n\t\t\t// a manual backup reports its file the same way the scheduled one does\n\t\t\tif (id === COMMAND_IDS.backup && result.ok) {\n\t\t\t\tconst newest = services.backup.list()[0];\n\t\t\t\tif (newest) {\n\t\t\t\t\tawait this.announceBackup(newest.name, newest.createdAt);\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthis.log.warn(`command ${id} failed: ${(error as Error).message}`);\n\t\t}\n\n\t\t// buttons are stateless: always release them again\n\t\tif (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\t// a punch written by a rule lands in `events.*` too; a presence write that changed nothing has no\n\t\t\t\t// event (the builder answers `null` then)\n\t\t\t\tconst atUtc = Math.floor(Date.now() / 1000);\n\t\t\t\tif (\"changed\" in result) {\n\t\t\t\t\tthis.publishPunch(presenceEvent(result, atUtc));\n\t\t\t\t} else {\n\t\t\t\t\tthis.publishPunch(punchEvent(result, `trigger.${rule.id}`, atUtc));\n\t\t\t\t}\n\t\t\t\tservices.triggers.markFired({ id: rule.id });\n\t\t\t\tawait this.refreshStates();\n\t\t\t} catch (error) {\n\t\t\t\t// a deleted employee, a deactivated one, a value that cannot be used: never take the adapter down\n\t\t\t\tthis.log.warn(`trigger ${rule.id} failed: ${(error as Error).message}`);\n\t\t\t}\n\t\t})();\n\t}\n\n\t/**\n\t * Stops the HTTP server.\n\t */\n\tprivate async stopApi(): Promise<void> {\n\t\tconst server = this.webServer;\n\t\tthis.webServer = null;\n\t\tif (server) {\n\t\t\tawait server.close();\n\t\t\tthis.log.debug(\"API stopped\");\n\t\t}\n\t}\n\n\t/**\n\t * Is called when adapter shuts down - callback has to be called under any circumstances!\n\t *\n\t * @param callback - Callback function\n\t */\n\tprivate onUnload(callback: () => void): void {\n\t\ttry {\n\t\t\t// Timers created with this.setTimeout/this.setInterval are cleared automatically on unload.\n\t\t\tvoid this.stopApi()\n\t\t\t\t.catch(error => this.log.warn(`stopping the API failed: ${(error as Error).message}`))\n\t\t\t\t.finally(() => {\n\t\t\t\t\tthis.db?.close();\n\t\t\t\t\tthis.db = null;\n\t\t\t\t\tcallback();\n\t\t\t\t});\n\t\t} catch (error) {\n\t\t\tthis.log.error(`Error during unloading: ${(error as Error).message}`);\n\t\t\tcallback();\n\t\t}\n\t}\n\n\t/**\n\t * Runs the automation rules that are due.\n\t *\n\t * A rule acts at most once per employee and period \u2014 the local date, or the ISO week for a weekly rule;\n\t * `automation_runs` holds that decision, so a restart\n\t * cannot punch twice. The run is noted **before** the action (a double punch would be worse than a missed\n\t * reminder) and taken back when the action fails, so the next minute retries it.\n\t */\n\tprivate runAutomationRules(): void {\n\t\tconst services = this.services;\n\t\tif (!services) {\n\t\t\treturn;\n\t\t}\n\n\t\tlet rules: AutomationRuleRecord[] = [];\n\t\ttry {\n\t\t\trules = services.automations.list();\n\t\t} catch (error) {\n\t\t\tthis.log.warn(`automation rules could not be read: ${(error as Error).message}`);\n\t\t\treturn;\n\t\t}\n\t\tif (rules.length === 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst nowUtc = Math.floor(Date.now() / 1000);\n\t\tlet acted = false;\n\n\t\tfor (const rule of rules) {\n\t\t\tconst candidates =\n\t\t\t\trule.userId === null\n\t\t\t\t\t? services.users.list()\n\t\t\t\t\t: [services.users.findById(rule.userId)].filter((user): user is UserRecord => user !== null);\n\n\t\t\tfor (const user of candidates) {\n\t\t\t\tif (!user.isActive) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst local = localDateTime(nowUtc, user.timezone);\n\t\t\t\t// a weekly rule is guarded per ISO week, a daily rule per local date\n\t\t\t\tconst period = runPeriod(rule.repeat, local.date);\n\t\t\t\tif (services.automations.hasRun({ ruleId: rule.id, userId: user.id, period })) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst punches = services.entries\n\t\t\t\t\t.listByDate(user.id, local.date)\n\t\t\t\t\t.map(entry => ({ tsUtc: entry.tsUtc, direction: entry.direction }));\n\t\t\t\tconst block = workBlock(punches, nowUtc);\n\t\t\t\tconst decision = evaluateAutomation(rule, {\n\t\t\t\t\tlocalDate: local.date,\n\t\t\t\t\tweekday: isoWeekday(local.date),\n\t\t\t\t\tminuteOfDay: local.minutes,\n\t\t\t\t\thasOpenEntry: block.hasOpenEntry,\n\t\t\t\t\tblockMinutes: block.blockMinutes,\n\t\t\t\t\talreadyRan: false,\n\t\t\t\t});\n\t\t\t\tif (!decision.fire) {\n\t\t\t\t\tthis.log.debug(`automation ${rule.id} (${user.displayName}): ${decision.reason}`);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst action = this.automationAction(rule, block.blockMinutes);\n\t\t\t\tif (\n\t\t\t\t\t!services.automations.recordRun({\n\t\t\t\t\t\truleId: rule.id,\n\t\t\t\t\t\tuserId: user.id,\n\t\t\t\t\t\tperiod,\n\t\t\t\t\t\taction,\n\t\t\t\t\t\tnow: nowUtc,\n\t\t\t\t\t})\n\t\t\t\t) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\ttry {\n\t\t\t\t\tlet punched: CommandResult | null = null;\n\t\t\t\t\tif (rule.kind === \"clockOut\" || rule.kind === \"clockIn\") {\n\t\t\t\t\t\tpunched = punchEmployee(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tentries: services.entries,\n\t\t\t\t\t\t\t\tusers: services.users,\n\t\t\t\t\t\t\t\tsettings: services.settings,\n\t\t\t\t\t\t\t\taggregation: services.aggregation,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{ userId: user.id, note: `auto.${rule.kind}`, now: nowUtc },\n\t\t\t\t\t\t);\n\t\t\t\t\t\tthis.log.info(`automation ${rule.id} for ${user.displayName}: ${punched.message}`);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.log.info(`automation ${rule.id} for ${user.displayName}: ${action}`);\n\t\t\t\t\t}\n\t\t\t\t\tacted = true;\n\t\t\t\t\tthis.events?.publish({\n\t\t\t\t\t\ttype: `automation.${rule.kind}`,\n\t\t\t\t\t\tatUtc: nowUtc,\n\t\t\t\t\t\tuserId: user.id,\n\t\t\t\t\t\t// the direction belongs into the event when the rule wrote a punch (checker: `events.lastDirection`)\n\t\t\t\t\t\tdata: { ruleId: rule.id, action, reason: decision.reason, direction: punched?.direction },\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// give the next check a chance instead of losing the period\n\t\t\t\t\tservices.automations.forgetRun({ ruleId: rule.id, userId: user.id, period });\n\t\t\t\t\tthis.log.warn(`automation ${rule.id} for ${user.displayName} failed: ${(error as Error).message}`);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (acted) {\n\t\t\t// a punch written by a rule changes the published figures\n\t\t\tthis.scheduleStateRefresh();\n\t\t}\n\t}\n\n\t/**\n\t * Short description of what a rule did (the log entry of `automation_runs`).\n\t *\n\t * @param rule - rule that fired\n\t * @param blockMinutes - length of the running work block, `null` when the employee is not clocked in\n\t * @returns text for the log\n\t */\n\tprivate automationAction(rule: AutomationRuleRecord, blockMinutes: number | null): string {\n\t\tif (rule.kind === \"clockOut\") {\n\t\t\treturn \"clocked out\";\n\t\t}\n\t\tif (rule.kind === \"clockIn\") {\n\t\t\treturn \"clocked in\";\n\t\t}\n\t\tif (rule.kind === \"breakReminder\") {\n\t\t\treturn `reminded after ${blockMinutes ?? 0} min without a break`;\n\t\t}\n\t\treturn \"reported a missing punch\";\n\t}\n\n\t/**\n\t * Republishes the figures shortly after data changed.\n\t *\n\t * A punch from the web app, the terminal or a badge arrives through the API of this very process, so without\n\t * this the states would only change with the five minute timer. One action often produces several events, so\n\t * the refresh is collected for a moment instead of running once per event.\n\t */\n\tprivate scheduleStateRefresh(): void {\n\t\tif (this.stateRefreshTimer) {\n\t\t\treturn;\n\t\t}\n\t\tthis.stateRefreshTimer = this.setTimeout(() => {\n\t\t\tthis.stateRefreshTimer = undefined;\n\t\t\tvoid this.refreshStates();\n\t\t}, 1000);\n\t}\n\n\t/**\n\t * Publishes the newest event of the instance into the state tree.\n\t *\n\t * A notification adapter, a dashboard or a Blockly script then only has to watch `events.*` instead of\n\t * following the live stream of the web app.\n\t *\n\t * @param event - event published by the API\n\t */\n\tprivate async publishEventState(event: ApiEvent): Promise<void> {\n\t\tconst services = this.services;\n\t\tif (!services) {\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tconst data = event.data ?? {};\n\t\t\tconst user =\n\t\t\t\tevent.userId === null || event.userId === undefined ? null : services.users.findById(event.userId);\n\t\t\tawait publishEventSnapshot(this, {\n\t\t\t\ttype: event.type,\n\t\t\t\tatUtc: event.atUtc,\n\t\t\t\tuserName: user?.displayName ?? \"\",\n\t\t\t\tdirection: typeof data.direction === \"string\" ? data.direction : \"\",\n\t\t\t\tsource: typeof data.source === \"string\" ? data.source : \"\",\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tthis.log.debug(`event state could not be published: ${(error as Error).message}`);\n\t\t}\n\t}\n\n\t/**\n\t * Publishes a punch on the event bus, so it reaches `events.*` and the connected web clients.\n\t *\n\t * A punch can be written from several places \u2014 the command states, a `sendTo` message, a trigger rule, an\n\t * automation rule and the presence state. Every one of them passes its result through here; a path that forgets\n\t * it leaves the event states (`lastType`, `lastUser`, \u2026) on the punch before.\n\t *\n\t * @param event - event built from the punch, `null` when nothing was written\n\t */\n\tprivate publishPunch(event: ApiEvent | null): void {\n\t\tif (event) {\n\t\t\tthis.events?.publish(event);\n\t\t}\n\t}\n\n\t/**\n\t * Is called when another adapter sends a message (`sendTo`).\n\t *\n\t * @param obj - message object of the adapter framework\n\t */\n\tprivate onMessage(obj: ioBroker.Message): void {\n\t\tvoid (async (): Promise<void> => {\n\t\t\tconst services = this.services;\n\t\t\tif (!services) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst answer = await handleMessage(\n\t\t\t\t\t{\n\t\t\t\t\t\tusers: services.users,\n\t\t\t\t\t\tentries: services.entries,\n\t\t\t\t\t\tabsences: services.absences,\n\t\t\t\t\t\tsettings: services.settings,\n\t\t\t\t\t\taggregation: services.aggregation,\n\t\t\t\t\t\tsync: services.sync,\n\t\t\t\t\t\tbackup: services.backup,\n\t\t\t\t\t\tversion: this.version,\n\t\t\t\t\t},\n\t\t\t\t\tobj.command,\n\t\t\t\t\tobj.message,\n\t\t\t\t);\n\t\t\t\tthis.log.info(`message ${obj.command}: ${answer.message}`);\n\t\t\t\t// a punch answered by a message reaches `events.*` and the web app like every other one\n\t\t\t\tconst direction = answer.data?.direction;\n\t\t\t\tconst userId = answer.data?.userId;\n\t\t\t\tif (typeof direction === \"string\" && typeof userId === \"number\") {\n\t\t\t\t\tthis.publishPunch({\n\t\t\t\t\t\ttype: \"punch\",\n\t\t\t\t\t\tatUtc: Math.floor(Date.now() / 1000),\n\t\t\t\t\t\tuserId,\n\t\t\t\t\t\tdata: { direction, source: `sendTo.${obj.command}` },\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tif (obj.callback) {\n\t\t\t\t\tthis.sendTo(obj.from, obj.command, answer, obj.callback);\n\t\t\t\t}\n\t\t\t\tawait this.refreshStates();\n\t\t\t} catch (error) {\n\t\t\t\tconst message = (error as Error).message;\n\t\t\t\tthis.log.warn(`message ${obj.command} failed: ${message}`);\n\t\t\t\tif (obj.callback) {\n\t\t\t\t\tthis.sendTo(obj.from, obj.command, { ok: false, message }, obj.callback);\n\t\t\t\t}\n\t\t\t}\n\t\t})();\n\t}\n\n\t/**\n\t * Is called if a subscribed state changes\n\t *\n\t * @param id - State ID\n\t * @param state - State object\n\t */\n\tprivate onStateChange(id: string, state: ioBroker.State | null | undefined): void {\n\t\ttry {\n\t\t\tif (!state) {\n\t\t\t\t// deletions carry no value\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// a watched state of another adapter is handled first: such a state is acknowledged by its own\n\t\t\t// adapter, and what counts there is the change of the value\n\t\t\tif (this.handleTriggerState(id, state)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (state.ack) {\n\t\t\t\t// Ignore acknowledged (status) states of this instance\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst prefix = `${this.namespace}.`;\n\t\t\tconst localId = id.startsWith(prefix) ? id.slice(prefix.length) : id;\n\t\t\tif (localId.startsWith(\"commands.\")) {\n\t\t\t\tvoid this.runCommand(localId, state.val);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (parsePresenceStateId(localId) !== null) {\n\t\t\t\tvoid this.runPresence(localId, state.val);\n\t\t\t\treturn;\n\t\t\t}\n\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.7",
|
|
5
5
|
"nogit": true,
|
|
6
6
|
"news": {
|
|
7
|
+
"0.2.7": {
|
|
8
|
+
"en": "fix: every punch path reaches `events.*` and the web app now. Besides the presence state (0.2.6) the `commands.punch`/`quickPunch` buttons, a `sendTo` message (`punch`, `present`) and the trigger rules wrote the punch but never published an event — the event states kept the punch before. The automation rules report the direction with their `automation.*` event now",
|
|
9
|
+
"de": "fix: jeder Stempel-Weg erreicht jetzt `events.*` und die Web-App. Außer dem Anwesenheits-State (0.2.6) schrieben die Buttons `commands.punch`/`quickPunch`, eine `sendTo`-Nachricht (`punch`, `present`) und die Trigger-Regeln die Buchung, veröffentlichten aber kein Ereignis — die Ereignis-States blieben beim vorigen Stempel stehen. Die Automatik-Regeln melden die Richtung jetzt mit ihrem `automation.*`-Ereignis",
|
|
10
|
+
"ru": "исправление: теперь каждый путь отметки доходит до `events.*` и веб-приложения. Кроме состояния присутствия (0.2.6), кнопки `commands.punch`/`quickPunch`, сообщение `sendTo` (`punch`, `present`) и правила-триггеры записывали отметку, но не публиковали событие — состояния событий оставались от предыдущей отметки. Правила автоматики теперь передают направление в своём событии `automation.*`",
|
|
11
|
+
"pt": "correção: todos os caminhos de marcação chegam agora a `events.*` e à aplicação web. Além do estado de presença (0.2.6), os botões `commands.punch`/`quickPunch`, uma mensagem `sendTo` (`punch`, `present`) e as regras de acionamento gravavam a marcação mas não publicavam evento — os estados de evento ficavam na marcação anterior. As regras de automação passam a indicar a direção no seu evento `automation.*`",
|
|
12
|
+
"nl": "fix: elk stempelpad bereikt nu `events.*` en de webapp. Naast de aanwezigheidsstate (0.2.6) schreven de knoppen `commands.punch`/`quickPunch`, een `sendTo`-bericht (`punch`, `present`) en de triggerregels de stempeling, maar publiceerden geen gebeurtenis — de gebeurtenisstates bleven bij de vorige stempeling staan. De automatiseringsregels melden de richting nu met hun `automation.*`-gebeurtenis",
|
|
13
|
+
"fr": "correctif : tous les chemins de pointage atteignent désormais `events.*` et l'application web. Outre l'état de présence (0.2.6), les boutons `commands.punch`/`quickPunch`, un message `sendTo` (`punch`, `present`) et les règles de déclenchement écrivaient le pointage sans publier d'événement — les états d'événement restaient sur le pointage précédent. Les règles d'automatisation indiquent désormais la direction dans leur événement `automation.*`",
|
|
14
|
+
"it": "correzione: ora tutti i percorsi di timbratura raggiungono `events.*` e l'app web. Oltre allo stato di presenza (0.2.6), i pulsanti `commands.punch`/`quickPunch`, un messaggio `sendTo` (`punch`, `present`) e le regole di trigger scrivevano la timbratura senza pubblicare eventi — gli stati degli eventi restavano sulla timbratura precedente. Le regole di automazione ora indicano la direzione nel loro evento `automation.*`",
|
|
15
|
+
"es": "corrección: ahora todos los caminos de fichaje llegan a `events.*` y a la aplicación web. Además del estado de presencia (0.2.6), los botones `commands.punch`/`quickPunch`, un mensaje `sendTo` (`punch`, `present`) y las reglas de disparo escribían el fichaje sin publicar evento — los estados de evento se quedaban en el fichaje anterior. Las reglas de automatización ahora indican la dirección en su evento `automation.*`",
|
|
16
|
+
"pl": "poprawka: teraz każda droga rejestracji dociera do `events.*` i aplikacji web. Poza stanem obecności (0.2.6) przyciski `commands.punch`/`quickPunch`, wiadomość `sendTo` (`punch`, `present`) oraz reguły wyzwalające zapisywały wpis, ale nie publikowały zdarzenia — stany zdarzeń zostawały przy poprzednim wpisie. Reguły automatyki przekazują teraz kierunek w swoim zdarzeniu `automation.*`",
|
|
17
|
+
"uk": "виправлення: тепер кожен шлях відмітки доходить до `events.*` і веб-застосунку. Крім стану присутності (0.2.6), кнопки `commands.punch`/`quickPunch`, повідомлення `sendTo` (`punch`, `present`) і правила-тригери записували відмітку, але не публікували подію — стани подій залишалися від попередньої відмітки. Правила автоматики тепер передають напрямок у своїй події `automation.*`",
|
|
18
|
+
"zh-cn": "修复:现在每个打卡路径都会到达 `events.*` 和 Web 应用。除在岗状态(0.2.6)外,`commands.punch`/`quickPunch` 按钮、`sendTo` 消息(`punch`、`present`)和触发规则虽然写入了打卡,却从未发布事件 —— 事件状态一直停留在上一次打卡。自动化规则现在会在其 `automation.*` 事件中带上方向"
|
|
19
|
+
},
|
|
7
20
|
"0.2.6": {
|
|
8
21
|
"en": "fix: a punch from the presence state (`users.<id>.present`) is published into the event stream now — it appears in `events.lastType`, `lastUser`, `lastDirection` and `lastSource` and reaches the web app live. The punch was stored in the database, but the event was never sent to the bus, so the event states kept the previous punch",
|
|
9
22
|
"de": "fix: ein Stempel über den Anwesenheits-State (`users.<id>.present`) wird jetzt in den Ereignisstrom veröffentlicht — er erscheint in `events.lastType`, `lastUser`, `lastDirection` und `lastSource` und erreicht die Web-App live. Die Buchung landete zwar in der Datenbank, das Ereignis wurde aber nie auf den Bus gegeben, deshalb blieben die Ereignis-States beim vorigen Stempel stehen",
|
|
@@ -81,19 +94,6 @@
|
|
|
81
94
|
"pl": "`common.news` zaczyna się od nowa po zmianie nazwy: nowy pakiet npm `iobroker.time-tracker` zawiera tylko 0.2.0, dlatego wpisy od 0.1.3 do 0.1.9 są ponownie usunięte — kontrola repozytorium zgłaszała je jako `E2004` (“do not exist at NPM”). Informacje o tych wersjach pozostają w changelogu",
|
|
82
95
|
"uk": "`common.news` починається заново після перейменування: у новому пакеті npm `iobroker.time-tracker` є лише 0.2.0, тому записи 0.1.3–0.1.9 знову прибрано — перевірка репозиторію позначала їх як `E2004` («do not exist at NPM»). Нотатки про ці версії залишаються в changelog",
|
|
83
96
|
"zh-cn": "重命名后 `common.news` 重新开始:新的 npm 包 `iobroker.time-tracker` 只有 0.2.0,因此 0.1.3 至 0.1.9 的条目再次移除 —— 仓库检查将这些条目标记为 `E2004`(“do not exist at NPM”)。这些版本的发布说明仍保留在更新日志中"
|
|
84
|
-
},
|
|
85
|
-
"0.2.0": {
|
|
86
|
-
"en": "renamed to `ioBroker.time-tracker`: ioBroker requires English adapter names, so the adapter, its npm package and the repository are called `time-tracker` from now on. The instance becomes `time-tracker.0` and creates the database `time-tracker.sqlite`; existing data keeps working by pointing “Database file” at the old `zeiterfassung.sqlite`. German UI texts stay German, `common.titleLang` keeps all 11 languages",
|
|
87
|
-
"de": "umbenannt in `ioBroker.time-tracker`: ioBroker verlangt englische Adapternamen, deshalb heißen Adapter, npm-Paket und Repository ab jetzt `time-tracker`. Die Instanz wird `time-tracker.0` und legt die Datenbank `time-tracker.sqlite` an; vorhandene Daten bleiben nutzbar, indem „Datenbankdatei“ auf die alte `zeiterfassung.sqlite` zeigt. Deutsche Oberflächentexte bleiben deutsch, `common.titleLang` behält alle 11 Sprachen",
|
|
88
|
-
"ru": "переименован в `ioBroker.time-tracker`: ioBroker требует английских имён адаптеров, поэтому адаптер, пакет npm и репозиторий теперь называются `time-tracker`. Экземпляр становится `time-tracker.0` и создаёт базу данных `time-tracker.sqlite`; существующие данные продолжают работать, если указать в «Файл базы данных» старый `zeiterfassung.sqlite`. Немецкие тексты интерфейса остаются немецкими, `common.titleLang` сохраняет все 11 языков",
|
|
89
|
-
"pt": "renomeado para `ioBroker.time-tracker`: o ioBroker exige nomes de adaptador em inglês, por isso o adaptador, o pacote npm e o repositório passam a chamar-se `time-tracker`. A instância torna-se `time-tracker.0` e cria a base de dados `time-tracker.sqlite`; os dados existentes continuam a funcionar indicando em «Ficheiro de base de dados» o antigo `zeiterfassung.sqlite`. Os textos alemães da interface mantêm-se em alemão, `common.titleLang` mantém os 11 idiomas",
|
|
90
|
-
"nl": "hernoemd naar `ioBroker.time-tracker`: ioBroker vereist Engelse adaptornamen, daarom heten adapter, npm-pakket en repository voortaan `time-tracker`. De instantie wordt `time-tracker.0` en maakt de database `time-tracker.sqlite` aan; bestaande gegevens blijven werken door bij «Databasebestand» naar het oude `zeiterfassung.sqlite` te wijzen. Duitse interfaceteksten blijven Duits, `common.titleLang` behoudt alle 11 talen",
|
|
91
|
-
"fr": "renommé en `ioBroker.time-tracker` : ioBroker exige des noms d'adaptateur en anglais, l'adaptateur, le paquet npm et le dépôt s'appellent donc désormais `time-tracker`. L'instance devient `time-tracker.0` et crée la base de données `time-tracker.sqlite` ; les données existantes restent utilisables en indiquant l'ancien `zeiterfassung.sqlite` dans « Fichier de base de données ». Les textes allemands de l'interface restent en allemand, `common.titleLang` conserve les 11 langues",
|
|
92
|
-
"it": "rinominato in `ioBroker.time-tracker`: ioBroker richiede nomi di adattatore in inglese, quindi adattatore, pacchetto npm e repository ora si chiamano `time-tracker`. L'istanza diventa `time-tracker.0` e crea il database `time-tracker.sqlite`; i dati esistenti continuano a funzionare indicando in «File del database» il vecchio `zeiterfassung.sqlite`. I testi tedeschi dell'interfaccia restano in tedesco, `common.titleLang` mantiene tutte le 11 lingue",
|
|
93
|
-
"es": "renombrado a `ioBroker.time-tracker`: ioBroker exige nombres de adaptador en inglés, así que el adaptador, el paquete npm y el repositorio ahora se llaman `time-tracker`. La instancia pasa a ser `time-tracker.0` y crea la base de datos `time-tracker.sqlite`; los datos existentes siguen funcionando si en «Archivo de base de datos» se indica el antiguo `zeiterfassung.sqlite`. Los textos alemanes de la interfaz siguen en alemán, `common.titleLang` conserva los 11 idiomas",
|
|
94
|
-
"pl": "zmieniono nazwę na `ioBroker.time-tracker`: ioBroker wymaga angielskich nazw adapterów, dlatego adapter, pakiet npm i repozytorium nazywają się teraz `time-tracker`. Instancja staje się `time-tracker.0` i tworzy bazę danych `time-tracker.sqlite`; istniejące dane nadal działają, jeśli w „Plik bazy danych” wskażesz stary `zeiterfassung.sqlite`. Niemieckie teksty interfejsu pozostają niemieckie, `common.titleLang` zachowuje wszystkie 11 języków",
|
|
95
|
-
"uk": "перейменовано на `ioBroker.time-tracker`: ioBroker вимагає англійських назв адаптерів, тому адаптер, пакет npm і репозиторій тепер називаються `time-tracker`. Екземпляр стає `time-tracker.0` і створює базу даних `time-tracker.sqlite`; наявні дані й далі працюють, якщо в «Файл бази даних» вказати старий `zeiterfassung.sqlite`. Німецькі тексти інтерфейсу залишаються німецькими, `common.titleLang` зберігає всі 11 мов",
|
|
96
|
-
"zh-cn": "已重命名为 `ioBroker.time-tracker`:ioBroker 要求适配器名称使用英文,因此适配器、npm 包和仓库现在都叫 `time-tracker`。实例变为 `time-tracker.0`,并创建数据库 `time-tracker.sqlite`;只要在“数据库文件”中指向旧的 `zeiterfassung.sqlite`,现有数据仍然可用。德语界面文本仍为德语,`common.titleLang` 保留全部 11 种语言"
|
|
97
97
|
}
|
|
98
98
|
},
|
|
99
99
|
"titleLang": {
|
package/package.json
CHANGED
package/www/sw.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sw.js","sources":["../../../../../../tmp/
|
|
1
|
+
{"version":3,"file":"sw.js","sources":["../../../../../../tmp/a2457d33bd15e4be81ef410e8f5fa479/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"}
|