iobroker.time-tracker 0.7.0 → 0.7.2
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 +67 -28
- package/admin/i18n/de.json +1 -1
- package/admin/i18n/en.json +1 -1
- package/admin/i18n/es.json +1 -1
- package/admin/i18n/fr.json +1 -1
- package/admin/i18n/it.json +1 -1
- package/admin/i18n/nl.json +1 -1
- package/admin/i18n/pl.json +1 -1
- package/admin/i18n/pt.json +1 -1
- package/admin/i18n/ru.json +1 -1
- package/admin/i18n/uk.json +1 -1
- package/admin/i18n/zh-cn.json +1 -1
- package/admin/jsonConfig.json +1 -1
- package/build/lib/db/migrations.js +39 -0
- package/build/lib/db/migrations.js.map +2 -2
- package/build/lib/db/repositories/dayNotes.js +183 -0
- package/build/lib/db/repositories/dayNotes.js.map +7 -0
- package/build/lib/db/seed.js +1 -1
- package/build/lib/db/seed.js.map +2 -2
- package/build/lib/reports/pdf.js +8 -2
- package/build/lib/reports/pdf.js.map +2 -2
- package/build/lib/services/sync.js +11 -9
- package/build/lib/services/sync.js.map +2 -2
- package/build/lib/web/api.js +112 -16
- package/build/lib/web/api.js.map +2 -2
- package/build/lib/web/events.js.map +2 -2
- package/build/lib/web/problem.js.map +2 -2
- package/build/main.js +4 -0
- package/build/main.js.map +2 -2
- package/io-package.json +27 -27
- package/package.json +1 -1
- package/src-pwa/src/i18n/de.json +16 -1
- package/src-pwa/src/i18n/en.json +16 -1
- package/src-pwa/src/i18n/es.json +16 -1
- package/src-pwa/src/i18n/fr.json +16 -1
- package/src-pwa/src/i18n/it.json +16 -1
- package/src-pwa/src/i18n/nl.json +16 -1
- package/src-pwa/src/i18n/pl.json +16 -1
- package/src-pwa/src/i18n/pt.json +16 -1
- package/src-pwa/src/i18n/ru.json +16 -1
- package/src-pwa/src/i18n/uk.json +16 -1
- package/src-pwa/src/i18n/zh-cn.json +16 -1
- package/www/assets/index-C0z1CkCi.js +119 -0
- package/www/assets/index-C0z1CkCi.js.map +1 -0
- package/www/index.html +1 -1
- package/www/sw.js +1 -1
- package/www/sw.js.map +1 -1
- package/www/assets/index-DIM5i7zh.js +0 -119
- package/www/assets/index-DIM5i7zh.js.map +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/web/events.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Live events of the adapter.\n *\n * Everything that changes data publishes one event here: a punch, a correction, an import, a closed month. The\n * WebSocket endpoint (`/api/stream`) forwards them to connected clients, and the adapter uses the same bus to\n * refresh its own states. The bus is deliberately tiny: an in-memory fan-out without history, because a client\n * that missed an event reloads the data it cares about anyway.\n */\n\n/** Type of a live event. */\nexport type EventType =\n\t| \"punch\"\n\t| \"entry.update\"\n\t| \"entry.delete\"\n\t| \"entries.import\"\n\t| \"absence.change\"\n\t| \"month.close\"\n\t| \"terminal.punch\"\n\t| \"rfid.scan\"\n\t| \"backup.create\"\n\t| \"automation.clockIn\"\n\t| \"automation.clockOut\"\n\t| \"automation.missingPunch\"\n\t| \"automation.breakReminder\";\n\n/** One live event. */\nexport interface ApiEvent {\n\t/** Kind of the change */\n\ttype: EventType;\n\t/** Instant of the change, UTC epoch seconds */\n\tatUtc: number;\n\t/** Employee the change belongs to, `null` for instance wide events */\n\tuserId: number | null;\n\t/** Short, client readable summary (never personal data beyond the id) */\n\tdata?: Record<string, unknown>;\n}\n\n/** A subscriber of the bus. */\nexport type EventListener = (event: ApiEvent) => void;\n\n/** The event bus. */\nexport interface EventBus {\n\t/** Publishes an event to every listener */\n\tpublish(event: ApiEvent): void;\n\t/** Registers a listener and returns the function that removes it again */\n\tsubscribe(listener: EventListener): () => void;\n\t/** Number of connected listeners */\n\tlistenerCount(): number;\n}\n\n/**\n * Creates an event bus.\n *\n * @returns the bus\n */\nexport function createEventBus(): EventBus {\n\tconst listeners = new Set<EventListener>();\n\n\treturn {\n\t\tpublish(event: ApiEvent): void {\n\t\t\t// a listener that throws must not break the others (or the request that published)\n\t\t\tfor (const listener of [...listeners]) {\n\t\t\t\ttry {\n\t\t\t\t\tlistener(event);\n\t\t\t\t} catch {\n\t\t\t\t\t// ignored on purpose: the event is a notification, not a transaction\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\tsubscribe(listener: EventListener): () => void {\n\t\t\tlisteners.add(listener);\n\t\t\treturn () => {\n\t\t\t\tlisteners.delete(listener);\n\t\t\t};\n\t\t},\n\n\t\tlistenerCount(): number {\n\t\t\treturn listeners.size;\n\t\t},\n\t};\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;
|
|
4
|
+
"sourcesContent": ["/**\n * Live events of the adapter.\n *\n * Everything that changes data publishes one event here: a punch, a correction, an import, a closed month. The\n * WebSocket endpoint (`/api/stream`) forwards them to connected clients, and the adapter uses the same bus to\n * refresh its own states. The bus is deliberately tiny: an in-memory fan-out without history, because a client\n * that missed an event reloads the data it cares about anyway.\n */\n\n/** Type of a live event. */\nexport type EventType =\n\t| \"punch\"\n\t| \"entry.update\"\n\t| \"entry.delete\"\n\t| \"dayNote.change\"\n\t| \"entries.import\"\n\t| \"absence.change\"\n\t| \"month.close\"\n\t| \"terminal.punch\"\n\t| \"rfid.scan\"\n\t| \"backup.create\"\n\t| \"automation.clockIn\"\n\t| \"automation.clockOut\"\n\t| \"automation.missingPunch\"\n\t| \"automation.breakReminder\";\n\n/** One live event. */\nexport interface ApiEvent {\n\t/** Kind of the change */\n\ttype: EventType;\n\t/** Instant of the change, UTC epoch seconds */\n\tatUtc: number;\n\t/** Employee the change belongs to, `null` for instance wide events */\n\tuserId: number | null;\n\t/** Short, client readable summary (never personal data beyond the id) */\n\tdata?: Record<string, unknown>;\n}\n\n/** A subscriber of the bus. */\nexport type EventListener = (event: ApiEvent) => void;\n\n/** The event bus. */\nexport interface EventBus {\n\t/** Publishes an event to every listener */\n\tpublish(event: ApiEvent): void;\n\t/** Registers a listener and returns the function that removes it again */\n\tsubscribe(listener: EventListener): () => void;\n\t/** Number of connected listeners */\n\tlistenerCount(): number;\n}\n\n/**\n * Creates an event bus.\n *\n * @returns the bus\n */\nexport function createEventBus(): EventBus {\n\tconst listeners = new Set<EventListener>();\n\n\treturn {\n\t\tpublish(event: ApiEvent): void {\n\t\t\t// a listener that throws must not break the others (or the request that published)\n\t\t\tfor (const listener of [...listeners]) {\n\t\t\t\ttry {\n\t\t\t\t\tlistener(event);\n\t\t\t\t} catch {\n\t\t\t\t\t// ignored on purpose: the event is a notification, not a transaction\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\tsubscribe(listener: EventListener): () => void {\n\t\t\tlisteners.add(listener);\n\t\t\treturn () => {\n\t\t\t\tlisteners.delete(listener);\n\t\t\t};\n\t\t},\n\n\t\tlistenerCount(): number {\n\t\t\treturn listeners.size;\n\t\t},\n\t};\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAwDO,SAAS,iBAA2B;AAC1C,QAAM,YAAY,oBAAI,IAAmB;AAEzC,SAAO;AAAA,IACN,QAAQ,OAAuB;AAE9B,iBAAW,YAAY,CAAC,GAAG,SAAS,GAAG;AACtC,YAAI;AACH,mBAAS,KAAK;AAAA,QACf,QAAQ;AAAA,QAER;AAAA,MACD;AAAA,IACD;AAAA,IAEA,UAAU,UAAqC;AAC9C,gBAAU,IAAI,QAAQ;AACtB,aAAO,MAAM;AACZ,kBAAU,OAAO,QAAQ;AAAA,MAC1B;AAAA,IACD;AAAA,IAEA,gBAAwB;AACvB,aAAO,UAAU;AAAA,IAClB;AAAA,EACD;AACD;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/web/problem.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * `application/problem+json` responses (RFC 7807) with stable machine readable codes.\n *\n * Expected failures are raised as `HttpProblem` by the route handlers. Errors from the domain and storage\n * layers are mapped here, so a client always receives the same shape:\n *\n * ```\n * { \"type\": \"about:blank\", \"title\": \"Not Found\", \"status\": 404, \"code\": \"not_found\", \"detail\": \"\u2026\" }\n * ```\n *\n * Messages of unknown errors are **not** exposed (they may contain internals); they are logged by the caller.\n */\n\nimport { RevisionConflictError } from \"../db/repositories/entries\";\nimport { LoginExistsError, UnknownRoleError } from \"../db/repositories/users\";\nimport { UnknownAbsenceTypeError } from \"../db/repositories/absences\";\nimport { NotFoundError, ValidationError, type FieldIssue } from \"../errors\";\n\n/** The content type every problem document is sent with (RFC 9457). */\nexport const PROBLEM_CONTENT_TYPE = \"application/problem+json; charset=utf-8\";\n\n/** Stable error codes of the API. */\nexport type ProblemCode =\n\t| \"bad_request\"\n\t| \"invalid_credentials\"\n\t| \"locked_out\"\n\t| \"no_session\"\n\t| \"session_expired\"\n\t| \"user_inactive\"\n\t| \"permission_denied\"\n\t| \"csrf_rejected\"\n\t| \"not_found\"\n\t| \"method_not_allowed\"\n\t| \"conflict\"\n\t| \"revision_conflict\"\n\t| \"payload_too_large\"\n\t| \"unsupported_media_type\"\n\t| \"kiosk_disabled\"\n\t| \"not_configured\"\n\t| \"report_font_missing\"\n\t| \"backup_invalid\"\n\t| \"import_mismatch\"\n\t| \"rate_limited\"\n\t// an employee may only change own punches inside the edit window of the instance\n\t| \"edit_window_closed\"\n\t| \"internal_error\";\n\n/** A problem document. */\nexport interface ProblemDetails {\n\t/** URI reference identifying the problem type (always `about:blank` here) */\n\ttype: string;\n\t/** Short, human readable summary */\n\ttitle: string;\n\t/** HTTP status code */\n\tstatus: number;\n\t/** Stable machine readable code */\n\tcode: ProblemCode;\n\t/** Explanation of this occurrence */\n\tdetail?: string;\n\t/** Request path the problem occurred on */\n\tinstance?: string;\n\t/** Invalid fields of a validation failure (RFC 9457) */\n\terrors?: { path: string; message: string }[];\n}\n\nconst TITLES: Record<number, string> = {\n\t400: \"Bad Request\",\n\t401: \"Unauthorized\",\n\t403: \"Forbidden\",\n\t404: \"Not Found\",\n\t405: \"Method Not Allowed\",\n\t409: \"Conflict\",\n\t413: \"Payload Too Large\",\n\t415: \"Unsupported Media Type\",\n\t423: \"Locked\",\n\t429: \"Too Many Requests\",\n\t500: \"Internal Server Error\",\n};\n\n/** An error that is returned to the client as `application/problem+json`. */\nexport class HttpProblem extends Error {\n\t/** Title used in the problem document. */\n\tpublic readonly problemTitle: string;\n\n\t/**\n\t * Creates the error.\n\t *\n\t * @param status - HTTP status code\n\t * @param code - stable error code\n\t * @param detail - optional explanation\n\t * @param title - optional title (defaults to the reason phrase of the status)\n\t * @param fields - optional list of invalid fields\n\t */\n\tconstructor(\n\t\tpublic readonly status: number,\n\t\tpublic readonly code: ProblemCode,\n\t\tpublic readonly detail?: string,\n\t\ttitle?: string,\n\t\tpublic readonly fields: FieldIssue[] = [],\n\t) {\n\t\tsuper(`${status} ${code}${detail ? `: ${detail}` : \"\"}`);\n\t\tthis.name = \"HttpProblem\";\n\t\tthis.problemTitle = title ?? TITLES[status] ?? \"Error\";\n\t}\n\n\t/**\n\t * Converts the error into a problem document.\n\t *\n\t * @param instance - request path the problem occurred on\n\t * @returns problem document\n\t */\n\tpublic toProblem(instance?: string): ProblemDetails {\n\t\treturn {\n\t\t\ttype: \"about:blank\",\n\t\t\ttitle: this.problemTitle,\n\t\t\tstatus: this.status,\n\t\t\tcode: this.code,\n\t\t\t...(this.detail ? { detail: this.detail } : {}),\n\t\t\t...(this.fields.length > 0\n\t\t\t\t? { errors: this.fields.map(field => ({ path: field.path, message: field.message })) }\n\t\t\t\t: {}),\n\t\t\t...(instance ? { instance } : {}),\n\t\t};\n\t}\n}\n\n/**\n * Short helper for the common cases.\n *\n * @param status - HTTP status code\n * @param code - stable error code\n * @param detail - optional explanation\n * @returns the problem error\n */\nexport function problem(status: number, code: ProblemCode, detail?: string): HttpProblem {\n\treturn new HttpProblem(status, code, detail);\n}\n\n/**\n * Maps an arbitrary error onto a problem document.\n *\n * Known domain errors keep their meaning (a stale revision is a `409`, a duplicate login a `409`); messages of\n * unknown errors are replaced by a generic text, so no internals leak into the response.\n *\n * @param error - error thrown by a handler or a service\n * @param instance - request path\n * @returns problem document and a flag telling whether this is an unexpected (server side) failure\n */\nexport function toProblem(error: unknown, instance?: string): { problem: ProblemDetails; unexpected: boolean } {\n\tif (error instanceof HttpProblem) {\n\t\treturn { problem: error.toProblem(instance), unexpected: false };\n\t}\n\tif (error instanceof ValidationError) {\n\t\treturn {\n\t\t\tproblem: new HttpProblem(400, \"bad_request\", error.message, undefined, error.fields).toProblem(instance),\n\t\t\tunexpected: false,\n\t\t};\n\t}\n\tif (error instanceof NotFoundError) {\n\t\treturn { problem: new HttpProblem(404, \"not_found\", error.message).toProblem(instance), unexpected: false };\n\t}\n\tif (error instanceof RevisionConflictError) {\n\t\treturn {\n\t\t\tproblem: new HttpProblem(\n\t\t\t\t409,\n\t\t\t\t\"revision_conflict\",\n\t\t\t\t`the record was changed by someone else (current revision ${error.current.revision})`,\n\t\t\t).toProblem(instance),\n\t\t\tunexpected: false,\n\t\t};\n\t}\n\tif (error instanceof LoginExistsError || error instanceof UnknownRoleError) {\n\t\treturn { problem: new HttpProblem(409, \"conflict\", error.message).toProblem(instance), unexpected: false };\n\t}\n\tif (error instanceof UnknownAbsenceTypeError) {\n\t\treturn { problem: new HttpProblem(400, \"bad_request\", error.message).toProblem(instance), unexpected: false };\n\t}\n\n\treturn {\n\t\tproblem: new HttpProblem(500, \"internal_error\", \"the request could not be processed\").toProblem(instance),\n\t\tunexpected: true,\n\t};\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAaA,qBAAsC;AACtC,mBAAmD;AACnD,sBAAwC;AACxC,oBAAgE;AAGzD,MAAM,uBAAuB;
|
|
4
|
+
"sourcesContent": ["/**\n * `application/problem+json` responses (RFC 7807) with stable machine readable codes.\n *\n * Expected failures are raised as `HttpProblem` by the route handlers. Errors from the domain and storage\n * layers are mapped here, so a client always receives the same shape:\n *\n * ```\n * { \"type\": \"about:blank\", \"title\": \"Not Found\", \"status\": 404, \"code\": \"not_found\", \"detail\": \"\u2026\" }\n * ```\n *\n * Messages of unknown errors are **not** exposed (they may contain internals); they are logged by the caller.\n */\n\nimport { RevisionConflictError } from \"../db/repositories/entries\";\nimport { LoginExistsError, UnknownRoleError } from \"../db/repositories/users\";\nimport { UnknownAbsenceTypeError } from \"../db/repositories/absences\";\nimport { NotFoundError, ValidationError, type FieldIssue } from \"../errors\";\n\n/** The content type every problem document is sent with (RFC 9457). */\nexport const PROBLEM_CONTENT_TYPE = \"application/problem+json; charset=utf-8\";\n\n/** Stable error codes of the API. */\nexport type ProblemCode =\n\t| \"bad_request\"\n\t| \"invalid_credentials\"\n\t| \"locked_out\"\n\t| \"no_session\"\n\t| \"session_expired\"\n\t| \"user_inactive\"\n\t| \"permission_denied\"\n\t| \"csrf_rejected\"\n\t| \"not_found\"\n\t| \"method_not_allowed\"\n\t| \"conflict\"\n\t| \"revision_conflict\"\n\t| \"payload_too_large\"\n\t| \"unsupported_media_type\"\n\t| \"kiosk_disabled\"\n\t| \"not_configured\"\n\t| \"report_font_missing\"\n\t| \"backup_invalid\"\n\t| \"import_mismatch\"\n\t| \"rate_limited\"\n\t// an employee may only change own punches inside the edit window of the instance\n\t| \"edit_window_closed\"\n\t// the installation always keeps one active administrator\n\t| \"last_administrator\"\n\t| \"internal_error\";\n\n/** A problem document. */\nexport interface ProblemDetails {\n\t/** URI reference identifying the problem type (always `about:blank` here) */\n\ttype: string;\n\t/** Short, human readable summary */\n\ttitle: string;\n\t/** HTTP status code */\n\tstatus: number;\n\t/** Stable machine readable code */\n\tcode: ProblemCode;\n\t/** Explanation of this occurrence */\n\tdetail?: string;\n\t/** Request path the problem occurred on */\n\tinstance?: string;\n\t/** Invalid fields of a validation failure (RFC 9457) */\n\terrors?: { path: string; message: string }[];\n}\n\nconst TITLES: Record<number, string> = {\n\t400: \"Bad Request\",\n\t401: \"Unauthorized\",\n\t403: \"Forbidden\",\n\t404: \"Not Found\",\n\t405: \"Method Not Allowed\",\n\t409: \"Conflict\",\n\t413: \"Payload Too Large\",\n\t415: \"Unsupported Media Type\",\n\t423: \"Locked\",\n\t429: \"Too Many Requests\",\n\t500: \"Internal Server Error\",\n};\n\n/** An error that is returned to the client as `application/problem+json`. */\nexport class HttpProblem extends Error {\n\t/** Title used in the problem document. */\n\tpublic readonly problemTitle: string;\n\n\t/**\n\t * Creates the error.\n\t *\n\t * @param status - HTTP status code\n\t * @param code - stable error code\n\t * @param detail - optional explanation\n\t * @param title - optional title (defaults to the reason phrase of the status)\n\t * @param fields - optional list of invalid fields\n\t */\n\tconstructor(\n\t\tpublic readonly status: number,\n\t\tpublic readonly code: ProblemCode,\n\t\tpublic readonly detail?: string,\n\t\ttitle?: string,\n\t\tpublic readonly fields: FieldIssue[] = [],\n\t) {\n\t\tsuper(`${status} ${code}${detail ? `: ${detail}` : \"\"}`);\n\t\tthis.name = \"HttpProblem\";\n\t\tthis.problemTitle = title ?? TITLES[status] ?? \"Error\";\n\t}\n\n\t/**\n\t * Converts the error into a problem document.\n\t *\n\t * @param instance - request path the problem occurred on\n\t * @returns problem document\n\t */\n\tpublic toProblem(instance?: string): ProblemDetails {\n\t\treturn {\n\t\t\ttype: \"about:blank\",\n\t\t\ttitle: this.problemTitle,\n\t\t\tstatus: this.status,\n\t\t\tcode: this.code,\n\t\t\t...(this.detail ? { detail: this.detail } : {}),\n\t\t\t...(this.fields.length > 0\n\t\t\t\t? { errors: this.fields.map(field => ({ path: field.path, message: field.message })) }\n\t\t\t\t: {}),\n\t\t\t...(instance ? { instance } : {}),\n\t\t};\n\t}\n}\n\n/**\n * Short helper for the common cases.\n *\n * @param status - HTTP status code\n * @param code - stable error code\n * @param detail - optional explanation\n * @returns the problem error\n */\nexport function problem(status: number, code: ProblemCode, detail?: string): HttpProblem {\n\treturn new HttpProblem(status, code, detail);\n}\n\n/**\n * Maps an arbitrary error onto a problem document.\n *\n * Known domain errors keep their meaning (a stale revision is a `409`, a duplicate login a `409`); messages of\n * unknown errors are replaced by a generic text, so no internals leak into the response.\n *\n * @param error - error thrown by a handler or a service\n * @param instance - request path\n * @returns problem document and a flag telling whether this is an unexpected (server side) failure\n */\nexport function toProblem(error: unknown, instance?: string): { problem: ProblemDetails; unexpected: boolean } {\n\tif (error instanceof HttpProblem) {\n\t\treturn { problem: error.toProblem(instance), unexpected: false };\n\t}\n\tif (error instanceof ValidationError) {\n\t\treturn {\n\t\t\tproblem: new HttpProblem(400, \"bad_request\", error.message, undefined, error.fields).toProblem(instance),\n\t\t\tunexpected: false,\n\t\t};\n\t}\n\tif (error instanceof NotFoundError) {\n\t\treturn { problem: new HttpProblem(404, \"not_found\", error.message).toProblem(instance), unexpected: false };\n\t}\n\tif (error instanceof RevisionConflictError) {\n\t\treturn {\n\t\t\tproblem: new HttpProblem(\n\t\t\t\t409,\n\t\t\t\t\"revision_conflict\",\n\t\t\t\t`the record was changed by someone else (current revision ${error.current.revision})`,\n\t\t\t).toProblem(instance),\n\t\t\tunexpected: false,\n\t\t};\n\t}\n\tif (error instanceof LoginExistsError || error instanceof UnknownRoleError) {\n\t\treturn { problem: new HttpProblem(409, \"conflict\", error.message).toProblem(instance), unexpected: false };\n\t}\n\tif (error instanceof UnknownAbsenceTypeError) {\n\t\treturn { problem: new HttpProblem(400, \"bad_request\", error.message).toProblem(instance), unexpected: false };\n\t}\n\n\treturn {\n\t\tproblem: new HttpProblem(500, \"internal_error\", \"the request could not be processed\").toProblem(instance),\n\t\tunexpected: true,\n\t};\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAaA,qBAAsC;AACtC,mBAAmD;AACnD,sBAAwC;AACxC,oBAAgE;AAGzD,MAAM,uBAAuB;AAgDpC,MAAM,SAAiC;AAAA,EACtC,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AACN;AAGO,MAAM,oBAAoB,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAatC,YACiB,QACA,MACA,QAChB,OACgB,SAAuB,CAAC,GACvC;AArGH;AAsGE,UAAM,GAAG,MAAM,IAAI,IAAI,GAAG,SAAS,KAAK,MAAM,KAAK,EAAE,EAAE;AANvC;AACA;AACA;AAEA;AAGhB,SAAK,OAAO;AACZ,SAAK,gBAAe,6BAAS,OAAO,MAAM,MAAtB,YAA2B;AAAA,EAChD;AAAA;AAAA,EArBgB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA6BT,UAAU,UAAmC;AACnD,WAAO;AAAA,MACN,MAAM;AAAA,MACN,OAAO,KAAK;AAAA,MACZ,QAAQ,KAAK;AAAA,MACb,MAAM,KAAK;AAAA,MACX,GAAI,KAAK,SAAS,EAAE,QAAQ,KAAK,OAAO,IAAI,CAAC;AAAA,MAC7C,GAAI,KAAK,OAAO,SAAS,IACtB,EAAE,QAAQ,KAAK,OAAO,IAAI,YAAU,EAAE,MAAM,MAAM,MAAM,SAAS,MAAM,QAAQ,EAAE,EAAE,IACnF,CAAC;AAAA,MACJ,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;AAAA,IAChC;AAAA,EACD;AACD;AAUO,SAAS,QAAQ,QAAgB,MAAmB,QAA8B;AACxF,SAAO,IAAI,YAAY,QAAQ,MAAM,MAAM;AAC5C;AAYO,SAAS,UAAU,OAAgB,UAAqE;AAC9G,MAAI,iBAAiB,aAAa;AACjC,WAAO,EAAE,SAAS,MAAM,UAAU,QAAQ,GAAG,YAAY,MAAM;AAAA,EAChE;AACA,MAAI,iBAAiB,+BAAiB;AACrC,WAAO;AAAA,MACN,SAAS,IAAI,YAAY,KAAK,eAAe,MAAM,SAAS,QAAW,MAAM,MAAM,EAAE,UAAU,QAAQ;AAAA,MACvG,YAAY;AAAA,IACb;AAAA,EACD;AACA,MAAI,iBAAiB,6BAAe;AACnC,WAAO,EAAE,SAAS,IAAI,YAAY,KAAK,aAAa,MAAM,OAAO,EAAE,UAAU,QAAQ,GAAG,YAAY,MAAM;AAAA,EAC3G;AACA,MAAI,iBAAiB,sCAAuB;AAC3C,WAAO;AAAA,MACN,SAAS,IAAI;AAAA,QACZ;AAAA,QACA;AAAA,QACA,4DAA4D,MAAM,QAAQ,QAAQ;AAAA,MACnF,EAAE,UAAU,QAAQ;AAAA,MACpB,YAAY;AAAA,IACb;AAAA,EACD;AACA,MAAI,iBAAiB,iCAAoB,iBAAiB,+BAAkB;AAC3E,WAAO,EAAE,SAAS,IAAI,YAAY,KAAK,YAAY,MAAM,OAAO,EAAE,UAAU,QAAQ,GAAG,YAAY,MAAM;AAAA,EAC1G;AACA,MAAI,iBAAiB,yCAAyB;AAC7C,WAAO,EAAE,SAAS,IAAI,YAAY,KAAK,eAAe,MAAM,OAAO,EAAE,UAAU,QAAQ,GAAG,YAAY,MAAM;AAAA,EAC7G;AAEA,SAAO;AAAA,IACN,SAAS,IAAI,YAAY,KAAK,kBAAkB,oCAAoC,EAAE,UAAU,QAAQ;AAAA,IACxG,YAAY;AAAA,EACb;AACD;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/build/main.js
CHANGED
|
@@ -28,6 +28,7 @@ var utils = __toESM(require("@iobroker/adapter-core"));
|
|
|
28
28
|
var import_database = require("./lib/db/database");
|
|
29
29
|
var import_seed = require("./lib/db/seed");
|
|
30
30
|
var import_absences = require("./lib/db/repositories/absences");
|
|
31
|
+
var import_dayNotes = require("./lib/db/repositories/dayNotes");
|
|
31
32
|
var import_entries = require("./lib/db/repositories/entries");
|
|
32
33
|
var import_holidays = require("./lib/db/repositories/holidays");
|
|
33
34
|
var import_payouts = require("./lib/db/repositories/payouts");
|
|
@@ -311,6 +312,7 @@ class TimeTracker extends utils.Adapter {
|
|
|
311
312
|
const users = (0, import_users.createUsersRepository)(db);
|
|
312
313
|
const entries = (0, import_entries.createEntriesRepository)(db);
|
|
313
314
|
const absences = (0, import_absences.createAbsencesRepository)(db);
|
|
315
|
+
const dayNotes = (0, import_dayNotes.createDayNotesRepository)(db);
|
|
314
316
|
const holidays = (0, import_holidays.createHolidaysRepository)(db);
|
|
315
317
|
const rules = (0, import_rules.createRulesRepository)(db);
|
|
316
318
|
const payouts = (0, import_payouts.createPayoutsRepository)(db);
|
|
@@ -348,6 +350,7 @@ class TimeTracker extends utils.Adapter {
|
|
|
348
350
|
users,
|
|
349
351
|
entries,
|
|
350
352
|
absences,
|
|
353
|
+
dayNotes,
|
|
351
354
|
holidays,
|
|
352
355
|
rules,
|
|
353
356
|
payouts,
|
|
@@ -372,6 +375,7 @@ class TimeTracker extends utils.Adapter {
|
|
|
372
375
|
users,
|
|
373
376
|
entries,
|
|
374
377
|
absences,
|
|
378
|
+
dayNotes,
|
|
375
379
|
settings,
|
|
376
380
|
aggregation,
|
|
377
381
|
sync,
|
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, 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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",
|
|
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 { createDayNotesRepository, type DayNotesRepository } from \"./lib/db/repositories/dayNotes\";\nimport { createEntriesRepository } from \"./lib/db/repositories/entries\";\nimport { createHolidaysRepository } from \"./lib/db/repositories/holidays\";\nimport { createPayoutsRepository } from \"./lib/db/repositories/payouts\";\nimport { createRulesRepository } from \"./lib/db/repositories/rules\";\nimport { createSettingsRepository } from \"./lib/db/repositories/settings\";\nimport { createUsersRepository, type UserRecord, type UsersRepository } from \"./lib/db/repositories/users\";\nimport {\n\tcreateAutomationsRepository,\n\ttype AutomationRuleRecord,\n\ttype AutomationsRepository,\n} from \"./lib/db/repositories/automations\";\nimport { createTerminalsRepository } from \"./lib/db/repositories/terminals\";\nimport { createRfidRepository } from \"./lib/db/repositories/rfid\";\nimport {\n\tcreateTriggersRepository,\n\ttype TriggerRuleRecord,\n\ttype TriggersRepository,\n} from \"./lib/db/repositories/triggers\";\nimport type { EntriesRepository } from \"./lib/db/repositories/entries\";\nimport type { AbsencesRepository } from \"./lib/db/repositories/absences\";\nimport type { SettingsRepository } from \"./lib/db/repositories/settings\";\nimport { createAggregationService, type AggregationService } from \"./lib/services/aggregation\";\nimport { createAuthService, hashPassword } from \"./lib/services/auth\";\nimport { SECRET_FILE_NAME, resolveSessionSecret } from \"./lib/services/sessionSecret\";\nimport { createClosingService, type ClosingService } from \"./lib/services/closing\";\nimport {\n\tapplyPendingRestore,\n\tcreateBackupService,\n\trecordRestore,\n\ttype AppliedRestore,\n\ttype BackupService,\n} from \"./lib/services/backup\";\nimport { createSyncService, type SyncService } from \"./lib/services/sync\";\nimport {\n\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\tdayNotes: DayNotesRepository;\n\tsettings: SettingsRepository;\n\taggregation: AggregationService;\n\tsync: SyncService;\n\tclosing: ClosingService;\n\tbackup: BackupService;\n\ttriggers: TriggersRepository;\n\tautomations: AutomationsRepository;\n}\n\nclass TimeTracker extends utils.Adapter {\n\tprivate db: Db | null = null;\n\tprivate webServer: WebServer | null = null;\n\tprivate services: AdapterServices | null = null;\n\t/** Bus of the API; `null` until the API is created */\n\tprivate events: EventBus | null = null;\n\t/** Last value seen per watched trigger state: only a change fires a rule */\n\tprivate readonly triggerValues = new Map<string, string>();\n\t/** Trigger states the adapter currently subscribes to */\n\tprivate readonly triggerStates = new Set<string>();\n\t/** Pending refresh of the figures after a change through the API */\n\tprivate stateRefreshTimer: ioBroker.Timeout | undefined = undefined;\n\n\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 dayNotes = createDayNotesRepository(db);\n\t\tconst holidays = createHolidaysRepository(db);\n\t\tconst rules = createRulesRepository(db);\n\t\tconst payouts = createPayoutsRepository(db);\n\t\tconst terminals = createTerminalsRepository(db);\n\t\tconst rfid = createRfidRepository(db);\n\t\tconst settings = createSettingsRepository(db);\n\t\tconst auth = createAuthService({\n\t\t\tdb,\n\t\t\tusers,\n\t\t\tsettings,\n\t\t\tsecret: this.sessionSecret(),\n\t\t\tdefaultTtlMinutes: this.config.sessionTtlMinutes || 720,\n\t\t});\n\t\tconst aggregation = createAggregationService({\n\t\t\tdb,\n\t\t\tusers,\n\t\t\tentries,\n\t\t\tabsences,\n\t\t\tholidays,\n\t\t\trules,\n\t\t\tsettings,\n\t\t});\n\t\tconst triggers = createTriggersRepository(db);\n\t\tconst automations = createAutomationsRepository(db);\n\t\tconst sync = createSyncService({ db, entries, users, aggregation });\n\t\tconst closing = createClosingService({ db, aggregation, payouts });\n\t\t// backups live next to the database file: `<data dir>/backups/time-tracker-<timestamp>.sqlite`\n\t\tconst backup = createBackupService({\n\t\t\tdb,\n\t\t\tdir: path.join(path.dirname(this.databaseFile()), \"backups\"),\n\t\t\tretentionDays: Math.max(0, Number(this.config.backupRetentionDays ?? 30) || 0),\n\t\t});\n\t\tconst api = createApi({\n\t\t\tdb,\n\t\t\tauth,\n\t\t\tusers,\n\t\t\tentries,\n\t\t\tabsences,\n\t\t\tdayNotes,\n\t\t\tholidays,\n\t\t\trules,\n\t\t\tpayouts,\n\t\t\tterminals,\n\t\t\trfid,\n\t\t\ttriggers,\n\t\t\tautomations,\n\t\t\taggregation,\n\t\t\tsync,\n\t\t\tsettings,\n\t\t\tbackup,\n\t\t\tkioskEnabled: this.config.kioskEnabled === true,\n\t\t\t// a reverse proxy in front is the normal case for HTTPS; without the switch the forwarded\n\t\t\t// headers are ignored, so a client cannot choose its own address or the HTTPS flag\n\t\t\ttrustProxy: this.config.trustProxy === true,\n\t\t\thmacSecret: this.tagSecret(),\n\t\t\tversion: this.version,\n\t\t\t// a saved trigger rule changes the states the adapter has to watch\n\t\t\tonTriggerRulesChanged: () => void this.refreshTriggerSubscriptions(),\n\t\t});\n\n\t\tthis.services = {\n\t\t\tusers,\n\t\t\tentries,\n\t\t\tabsences,\n\t\t\tdayNotes,\n\t\t\tsettings,\n\t\t\taggregation,\n\t\t\tsync,\n\t\t\tclosing,\n\t\t\tbackup,\n\t\t\ttriggers,\n\t\t\tautomations,\n\t\t};\n\t\tthis.events = api.events;\n\t\t// the newest event is mirrored into the state tree, so a notification only has to watch `events.*`;\n\t\t// a punch, a correction or an absence changes the figures, so they are republished right away\n\t\tapi.events.subscribe(event => {\n\t\t\tvoid this.publishEventState(event);\n\t\t\tif (FIGURES_EVENT_TYPES.includes(event.type)) {\n\t\t\t\tthis.scheduleStateRefresh();\n\t\t\t}\n\t\t});\n\t\tthis.log.debug(`API routes: ${api.routes().length}`);\n\n\t\t// the web interface is delivered from `www/` next to the compiled code (built by the PWA project);\n\t\t// without it the adapter still provides the API, e.g. for the terminal\n\t\tconst webDir = path.join(__dirname, \"..\", \"www\");\n\t\tconst staticFiles = fs.existsSync(path.join(webDir, \"index.html\"))\n\t\t\t? createStaticHandler({ root: webDir })\n\t\t\t: undefined;\n\t\tthis.log.info(\n\t\t\tstaticFiles\n\t\t\t\t? `web interface found at ${webDir}`\n\t\t\t\t: `no web interface at ${webDir} - only the API (and the terminal) is available`,\n\t\t);\n\n\t\ttry {\n\t\t\tthis.webServer = await startWebServer({\n\t\t\t\trouter: api.router,\n\t\t\t\tport: this.config.port || 8092,\n\t\t\t\tbind: this.config.bind || \"127.0.0.1\",\n\t\t\t\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,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "time-tracker",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.2",
|
|
5
5
|
"nogit": true,
|
|
6
6
|
"news": {
|
|
7
|
+
"0.7.2": {
|
|
8
|
+
"en": "security: the administration cannot lock itself out any more. Deactivating or taking the admin role away from the last active administrator is refused (409 `last_administrator`), and the web app warns before it is tried on the own account — the switch of the own account explains why an account cannot deactivate itself. The PDF statement writes the absences left aligned on lines of their own, and the list of the absence types shows “visible for everybody” on a line of its own so that “reduces vacation” cannot push it out of the row any more.",
|
|
9
|
+
"de": "Sicherheit: Die Verwaltung kann sich nicht mehr aussperren – das Deaktivieren und das Entziehen der Administrator-Rolle wird abgelehnt, wenn es den letzten aktiven Administrator treffen würde (409 `last_administrator`), und die App warnt, wenn man es beim eigenen Konto versucht – der Schalter des eigenen Kontos erklärt, warum ein Konto sich nicht selbst deaktivieren kann. Der PDF-Stundennachweis schreibt die Abwesenheiten linksbündig in eigenen Zeilen, und die Liste der Abwesenheitsarten zeigt „Für alle sichtbar“ in einer eigenen Zeile, damit „Zieht vom Urlaub ab“ sie nicht mehr aus der Zeile drängt.",
|
|
10
|
+
"ru": "безопасность: администрация больше не может запереть себя — деактивация или снятие роли администратора отклоняется, если это последний активный администратор (409 `last_administrator`), а приложение предупреждает при попытке сделать это со своей учётной записью — переключатель своей учётной записи объясняет, почему она не может деактивировать себя сама. PDF-отчёт пишет отсутствия с выравниванием по левому краю отдельными строками, а список видов отсутствия показывает «видно всем» отдельной строкой, чтобы «уменьшает отпуск» больше не вытеснял её из строки.",
|
|
11
|
+
"pt": "segurança: a administração já não se pode bloquear — desativar ou retirar o papel de administrador ao último administrador ativo é recusado (409 `last_administrator`) e a aplicação avisa antes de o tentar na própria conta — o interruptor da própria conta explica porque é que ela não se pode desativar a si mesma. O extrato em PDF escreve as ausências alinhadas à esquerda em linhas próprias, e a lista dos tipos de ausência mostra «visível para todos» numa linha própria, para que «reduz as férias» já não a empurre para fora da linha.",
|
|
12
|
+
"nl": "veiligheid: de administratie kan zichzelf niet meer buitensluiten — het deactiveren of afnemen van de beheerdersrol wordt geweigerd als het de laatste actieve beheerder zou treffen (409 `last_administrator`), en de app waarschuwt voordat het op het eigen account wordt geprobeerd — de schakelaar van het eigen account legt uit waarom een account zichzelf niet kan deactiveren. De PDF-afrekening schrijft de afwezigheden links uitgelijnd in eigen regels, en de lijst met afwezigheidstypes toont „zichtbaar voor iedereen“ op een eigen regel, zodat „trekt vakantie af“ die niet meer uit de regel duwt.",
|
|
13
|
+
"fr": "sécurité : l'administration ne peut plus se verrouiller — la désactivation ou le retrait du rôle d'administrateur est refusé s'il touche le dernier administrateur actif (409 `last_administrator`), et l'application avertit avant de l'essayer sur le propre compte — l'interrupteur du propre compte explique pourquoi un compte ne peut pas se désactiver lui-même. Le relevé PDF écrit les absences alignées à gauche sur des lignes propres, et la liste des types d'absence affiche « visible pour tout le monde » sur une ligne à part, pour que « déduit des vacances » ne la repousse plus hors de la ligne.",
|
|
14
|
+
"it": "sicurezza: l'amministrazione non può più chiudersi fuori — la disattivazione o la rimozione del ruolo di amministratore viene rifiutata se riguarda l'ultimo amministratore attivo (409 `last_administrator`) e l'app avvisa prima di provarla sul proprio account — l'interruttore del proprio account spiega perché un account non può disattivarsi da solo. Il rendiconto PDF scrive le assenze allineate a sinistra su righe proprie, e l'elenco dei tipi di assenza mostra «visibile a tutti» su una riga propria, così «riduce le ferie» non la spinge più fuori riga.",
|
|
15
|
+
"es": "seguridad: la administración ya no puede bloquearse a sí misma — desactivar o retirar el rol de administrador se rechaza si afecta al último administrador activo (409 `last_administrator`), y la aplicación avisa antes de intentarlo en la cuenta propia — el interruptor de la cuenta propia explica por qué una cuenta no puede desactivarse a sí misma. El extracto en PDF escribe las ausencias alineadas a la izquierda en líneas propias, y la lista de tipos de ausencia muestra «visible para todos» en una línea propia, para que «descuenta vacaciones» no la empuje fuera de la línea.",
|
|
16
|
+
"pl": "bezpieczeństwo: administracja nie może już sama się zablokować — dezaktywacja lub odebranie roli administratora jest odrzucane, jeśli dotyczy ostatniego aktywnego administratora (409 `last_administrator`), a aplikacja ostrzega przed próbą wykonania tego na własnym koncie — przełącznik własnego konta wyjaśnia, dlaczego nie może się ono samo dezaktywować. Zestawienie PDF zapisuje nieobecności wyrównane do lewej w osobnych wierszach, a lista typów nieobecności pokazuje „widoczne dla wszystkich” w osobnym wierszu, aby „odejmuje urlop” nie wypychało jej z wiersza.",
|
|
17
|
+
"uk": "безпека: адміністрація більше не може заблокувати себе — деактивацію або позбавлення ролі адміністратора відхилено, якщо це останній активний адміністратор (409 `last_administrator`), а застосунок попереджає перед спробою зробити це з власним обліковим записом — перемикач власного облікового запису пояснює, чому він не може деактивувати себе сам. PDF-звіт пише відсутності з вирівнюванням ліворуч окремими рядками, а список видів відсутності показує «видно всім» окремим рядком, щоб «зменшує відпустку» не витісняло його з рядка.",
|
|
18
|
+
"zh-cn": "安全:管理方不会再把自己锁在外面 —— 若涉及最后一位在职管理员,停用或移除管理员角色会被拒绝(409 `last_administrator`),而应用在自己的账户上尝试时也会先提示 —— 自己的账户开关会说明为什么账户无法停用自己。PDF 报表将缺勤改为左对齐、独立成行;缺勤类型列表把「所有人可见」单独放在一行,因此「扣减假期」不会再把它挤出该行。"
|
|
19
|
+
},
|
|
20
|
+
"0.7.1": {
|
|
21
|
+
"en": "fix and change: the day dialog in the month view of an employee shows and changes his punches - before it acted on the own account, so a punch added there went to the wrong person. Times belong to the administration: an employee no longer changes own times but leaves a note for the day (forgot to clock out), which the office books and marks as handled. The administrator account belongs to nobody and does not punch any more. The list of the absence types writes “visible for everybody” at the types the employees may really pick.",
|
|
22
|
+
"de": "Korrektur und Änderung: Der Tages-Dialog im Monat eines Mitarbeiters zeigt und ändert dessen Stempel – vorher arbeitete er auf dem eigenen Konto, ein dort ergänzter Stempel landete bei der falschen Person. Zeiten gehören der Verwaltung: Ein Mitarbeiter ändert keine eigenen Zeiten mehr, sondern hinterlässt eine Notiz zum Tag („An-/Ausstempeln vergessen“), die die Verwaltung bucht und als erledigt abhakt. Das Administrator-Konto gehört niemandem und stempelt nicht mehr. Die Liste der Abwesenheitsarten schreibt „Für alle sichtbar“ nur noch an den Arten, die die Mitarbeiter wirklich wählen dürfen.",
|
|
23
|
+
"ru": "исправление и изменение: диалог дня в месяце сотрудника показывает и изменяет его отметки — раньше он работал с собственной учётной записью, и добавленная там отметка попадала не к тому человеку. Время принадлежит администрации: сотрудник больше не меняет собственные времена, а оставляет заметку к дню («забыл отметиться»), которую администрация оформляет и отмечает как обработанную. Учётная запись администратора не принадлежит никому и больше не отмечается. Список видов отсутствия пишет «видно всем» только у тех видов, которые сотрудники действительно могут выбрать.",
|
|
24
|
+
"pt": "correção e alteração: o diálogo do dia no mês de um funcionário mostra e altera os registos dele — antes atuava na própria conta, pelo que um registo aí acrescentado ia para a pessoa errada. Os tempos pertencem à administração: o funcionário já não altera os seus próprios tempos, mas deixa uma nota para o dia («esqueci-me de registar»), que a administração regista e marca como resolvida. A conta de administrador não pertence a ninguém e já não regista. A lista dos tipos de ausência escreve «visível para todos» apenas nos tipos que os funcionários podem realmente escolher.",
|
|
25
|
+
"nl": "correctie en wijziging: het dagvenster in de maand van een medewerker toont en wijzigt zijn stempels — eerder werkte het op het eigen account, waardoor een daar toegevoegde stempel bij de verkeerde persoon terechtkwam. Tijden zijn van de administratie: een medewerker wijzigt zijn eigen tijden niet meer, maar laat een notitie bij de dag achter („vergeten te stempelen“), die de administratie boekt en als afgehandeld markeert. Het beheerdersaccount hoort bij niemand en stempelt niet meer. De lijst met afwezigheidstypes vermeldt „zichtbaar voor iedereen“ alleen bij de types die medewerkers echt kunnen kiezen.",
|
|
26
|
+
"fr": "correction et changement : la fenêtre du jour dans le mois d'un employé affiche et modifie ses pointages — auparavant elle agissait sur le propre compte, si bien qu'un pointage ajouté là allait à la mauvaise personne. Les horaires appartiennent à l'administration : un employé ne modifie plus ses propres horaires mais laisse une note du jour (« oubli de pointage »), que l'administration saisit et marque comme traitée. Le compte administrateur n'appartient à personne et ne pointe plus. La liste des types d'absence n'affiche « visible pour tout le monde » que pour les types que les employés peuvent réellement choisir.",
|
|
27
|
+
"it": "correzione e modifica: la finestra del giorno nel mese di un dipendente mostra e modifica le sue timbrature — prima agiva sul proprio account, quindi una timbratura aggiunta lì finiva alla persona sbagliata. Gli orari appartengono all'amministrazione: un dipendente non modifica più i propri orari ma lascia una nota del giorno («dimenticato di timbrare»), che l'amministrazione registra e segna come gestita. L'account amministratore non appartiene a nessuno e non timbra più. L'elenco dei tipi di assenza scrive «visibile a tutti» solo per i tipi che i dipendenti possono davvero scegliere.",
|
|
28
|
+
"es": "corrección y cambio: el diálogo del día en el mes de un empleado muestra y modifica sus fichajes — antes actuaba sobre la propia cuenta, así que un fichaje añadido allí iba a la persona equivocada. Los horarios pertenecen a la administración: el empleado ya no cambia sus propios horarios, sino que deja una nota del día («olvidé fichar»), que la administración registra y marca como resuelta. La cuenta de administrador no pertenece a nadie y ya no ficha. La lista de los tipos de ausencia escribe «visible para todos» solo en los tipos que los empleados pueden elegir de verdad.",
|
|
29
|
+
"pl": "poprawka i zmiana: okno dnia w miesiącu pracownika pokazuje i zmienia jego stemple — wcześniej działało na własnym koncie, więc dodany tam stempel trafiał do niewłaściwej osoby. Czasy należą do administracji: pracownik nie zmienia już własnych czasów, lecz zostawia notatkę do dnia („zapomniałem się zastemplować”), którą administracja księguje i oznacza jako załatwioną. Konto administratora nie należy do nikogo i nie stempluje. Lista rodzajów nieobecności zapisuje „widoczne dla wszystkich” tylko przy tych rodzajach, które pracownicy naprawdę mogą wybrać.",
|
|
30
|
+
"uk": "виправлення та зміна: вікно дня в місяці працівника показує та змінює його відмітки — раніше воно працювало з власним обліковим записом, тож додана там відмітка потрапляла не тій людині. Час належить адміністрації: працівник більше не змінює власний час, а залишає нотатку до дня («забув відмітитися»), яку адміністрація проводить і позначає як опрацьовану. Обліковий запис адміністратора нікому не належить і більше не відмічається. Список видів відсутності пише «видно всім» лише для тих видів, які працівники справді можуть обрати.",
|
|
31
|
+
"zh-cn": "修复与变更:员工月份中的当日窗口现在显示并修改该员工的打卡记录——此前它作用于自己的账户,因此在那里补的打卡会记到错误的人名下。时间归管理层负责:员工不再修改自己的时间,而是为当天留下备注(“忘记打卡”),由管理层入账并标记为已处理。管理员账户不属于任何人,不再打卡。缺勤类型列表只在员工真正可以选择的类型上写出「所有人可见」。"
|
|
32
|
+
},
|
|
7
33
|
"0.7.0": {
|
|
8
34
|
"en": "new: an absence type can carry a colour, and the calendar paints a day with it — vacation and sickness are told apart at a glance. The editor offers a colour picker and “remove the colour”, the list shows a dot per type, and the seeded types come with colours (vacation green, sickness red, accident orange, military slate, internal blue, training violet, external brown). An open request keeps its colour but is drawn faded",
|
|
9
35
|
"de": "neu: eine Abwesenheitsart kann eine Farbe tragen, und der Kalender malt den Tag damit — Urlaub und Krankheit sind auf einen Blick zu unterscheiden. Der Editor bietet eine Farbwahl und „Farbe entfernen“, die Liste zeigt einen Punkt je Art, und die Standardarten bringen Farben mit (Ferien grün, Krankheit rot, Unfall orange, Militär blaugrau, Intern blau, Weiterbildung violett, Extern braun). Ein offener Antrag behält seine Farbe, wird aber blasser gezeichnet",
|
|
@@ -68,32 +94,6 @@
|
|
|
68
94
|
"pl": "zmiana: „aktywny” rodzaju nieobecności nazywa się teraz „widoczny dla wszystkich” i dokładnie to oznacza. Włączony — pracownicy wybierają rodzaj w aplikacji i składają wniosek (urlop, szkolenie); wyłączony — rodzaj należy tylko do administracji (choroba, wypadek, służba wojskowa — takie zgłoszenie trafia do firmy tego samego dnia i jest księgowane, nie wnioskowane). Serwer odmawia pracownikowi niepublicznego rodzaju, choroba, wypadek i służba wojskowa startują wyłączone (migracja 22), a podpowiedź na karcie wyjaśnia przełącznik",
|
|
69
95
|
"uk": "зміна: «активно» для виду відсутності тепер називається «видно всім» і означає саме це. Увімкнено — працівники вибирають вид у застосунку й запитують його (відпустка, підвищення кваліфікації); вимкнено — вид належить лише адмініструванню (хвороба, нещасний випадок, військова служба — така довідка надходить на підприємство того ж дня й оформлюється, а не запитується). Сервер відмовляє працівникові в непублічному виді, хвороба, нещасний випадок і військова служба стартують вимкненими (міграція 22), а підказка на картці пояснює перемикач",
|
|
70
96
|
"zh-cn": "变更:缺勤类型的「启用」现在称为「所有人可见」,并且正是指这一点。开启时,员工可在应用中选择该类型并申请(如休假、进修);关闭时该类型仅属于管理方(如疾病、事故、兵役 —— 此类通知当天就到达企业,是登记而非申请)。服务器会拒绝员工使用非公开类型;疾病、事故与兵役默认为关闭(迁移 22),卡片中的提示说明了该开关"
|
|
71
|
-
},
|
|
72
|
-
"0.4.2": {
|
|
73
|
-
"en": "fix: the roles appear in the language of the display (the database keeps “Administrator”, “Manager” and “Employee”, the app translates them now), the two date fields of “enter an absence” no longer overlap with their label, an absence type can be removed again as long as no absence uses it (new DELETE /absence-types/:id with a confirmation that names the type), and an inactive type stays in the lists of the administration while the employees no longer see it in their picker",
|
|
74
|
-
"de": "fix: die Rollen erscheinen in der Sprache der Anzeige (die Datenbank behält „Administrator“, „Manager“ und „Employee“, die App übersetzt sie jetzt), die beiden Datumsfelder von „Abwesenheit erfassen“ überlappen nicht mehr mit ihrer Beschriftung, eine Abwesenheitsart lässt sich wieder entfernen, solange keine Abwesenheit sie nutzt (neues DELETE /absence-types/:id mit einer Bestätigung, die die Art nennt), und eine inaktive Art bleibt in den Listen der Verwaltung, während die Mitarbeiter sie nicht mehr in ihrer Auswahl sehen",
|
|
75
|
-
"ru": "исправление: роли отображаются на языке интерфейса (база данных хранит «Administrator», «Manager» и «Employee», приложение теперь переводит их), два поля даты в «внести отсутствие» больше не перекрываются с подписью, вид отсутствия снова можно удалить, пока его не использует ни одно отсутствие (новый DELETE /absence-types/:id с подтверждением, называющим вид), а неактивный вид остаётся в списках администрирования, тогда как сотрудники его в выборе больше не видят",
|
|
76
|
-
"pt": "correção: as funções aparecem na língua do ecrã (a base de dados mantém «Administrator», «Manager» e «Employee», a aplicação traduz-as agora), os dois campos de data de «registar uma ausência» já não se sobrepõem à etiqueta, um tipo de ausência pode voltar a ser eliminado enquanto nenhuma ausência o usar (novo DELETE /absence-types/:id com uma confirmação que nomeia o tipo), e um tipo inativo permanece nas listas da administração enquanto os funcionários já não o veem na sua escolha",
|
|
77
|
-
"nl": "fix: de rollen verschijnen in de taal van de weergave (de database houdt “Administrator”, “Manager” en “Employee”, de app vertaalt ze nu), de twee datumvelden van “een afwezigheid invoeren” overlappen niet meer met hun label, een afwezigheidstype kan weer worden verwijderd zolang geen afwezigheid het gebruikt (nieuw DELETE /absence-types/:id met een bevestiging die het type noemt), en een inactief type blijft in de lijsten van het beheer terwijl de medewerkers het niet meer in hun keuzelijst zien",
|
|
78
|
-
"fr": "correctif : les rôles apparaissent dans la langue de l'affichage (la base de données garde « Administrator », « Manager » et « Employee », l'application les traduit désormais), les deux champs de date de « saisir une absence » ne chevauchent plus leur libellé, un type d'absence peut à nouveau être supprimé tant qu'aucune absence ne l'utilise (nouveau DELETE /absence-types/:id avec une confirmation qui nomme le type), et un type inactif reste dans les listes de l'administration tandis que les employés ne le voient plus dans leur choix",
|
|
79
|
-
"it": "correzione: i ruoli appaiono nella lingua dell'interfaccia (il database conserva «Administrator», «Manager» e «Employee», l'app li traduce ora), i due campi data di «inserisci un'assenza» non si sovrappongono più all'etichetta, un tipo di assenza può essere di nuovo rimosso finché nessuna assenza lo usa (nuovo DELETE /absence-types/:id con una conferma che nomina il tipo), e un tipo inattivo resta negli elenchi dell'amministrazione mentre i dipendenti non lo vedono più nella loro scelta",
|
|
80
|
-
"es": "corrección: los roles aparecen en el idioma de la pantalla (la base de datos conserva «Administrator», «Manager» y «Employee», la aplicación los traduce ahora), los dos campos de fecha de «registrar una ausencia» ya no se solapan con su etiqueta, un tipo de ausencia se puede volver a eliminar mientras ninguna ausencia lo use (nuevo DELETE /absence-types/:id con una confirmación que nombra el tipo), y un tipo inactivo permanece en las listas de la administración mientras los empleados ya no lo ven en su elección",
|
|
81
|
-
"pl": "poprawka: role pojawiają się w języku interfejsu (baza danych zachowuje „Administrator”, „Manager” i „Employee”, aplikacja tłumaczy je teraz), dwa pola daty w „wpisz nieobecność” nie nachodzą już na etykietę, rodzaj nieobecności można znów usunąć, dopóki nie używa go żadna nieobecność (nowy DELETE /absence-types/:id z potwierdzeniem nazywającym rodzaj), a nieaktywny rodzaj pozostaje na listach administracji, gdy pracownicy nie widzą go już w swoim wyborze",
|
|
82
|
-
"uk": "виправлення: ролі відображаються мовою інтерфейсу (база даних зберігає «Administrator», «Manager» і «Employee», застосунок тепер перекладає їх), два поля дати у «внести відсутність» більше не перекриваються з підписом, вид відсутності знову можна видалити, доки його не використовує жодна відсутність (новий DELETE /absence-types/:id із підтвердженням, що називає вид), а неактивний вид залишається у списках адміністрування, тоді як працівники його у своєму виборі більше не бачать",
|
|
83
|
-
"zh-cn": "修复:角色现在以界面语言显示(数据库保留「Administrator」「Manager」「Employee」,应用现在会翻译它们);「登记缺勤」的两个日期字段不再与标签重叠;只要没有缺勤使用该类型,就可以再次删除它(新增 DELETE /absence-types/:id,并有指明该类型的确认提示);停用的类型仍保留在管理列表中,而员工在选择中不再看到它"
|
|
84
|
-
},
|
|
85
|
-
"0.4.1": {
|
|
86
|
-
"en": "docs: the installation chapter no longer carries the build steps for work on the sources — they live in `CONTRIBUTING.md` and `docs/entwicklung.md`, so the README stays a manual for users",
|
|
87
|
-
"de": "docs: das Installationskapitel der README enthält nicht mehr die Build-Schritte für die Arbeit an den Quellen — sie stehen in `CONTRIBUTING.md` und `docs/entwicklung.md`, die README bleibt eine Anleitung für Anwender",
|
|
88
|
-
"ru": "док: раздел установки в README больше не содержит шагов сборки для работы с исходниками — они описаны в `CONTRIBUTING.md` и `docs/entwicklung.md`, а README остаётся руководством для пользователей",
|
|
89
|
-
"pt": "docs: o capítulo de instalação do README já não contém os passos de compilação para trabalhar nos fontes — estão em `CONTRIBUTING.md` e `docs/entwicklung.md`, e o README fica um manual para utilizadores",
|
|
90
|
-
"nl": "docs: het installatiehoofdstuk van de README bevat niet langer de buildstappen voor werk aan de bron — die staan in `CONTRIBUTING.md` en `docs/entwicklung.md`, zodat de README een handleiding voor gebruikers blijft",
|
|
91
|
-
"fr": "docs : le chapitre d'installation du README ne contient plus les étapes de compilation pour travailler sur les sources — elles se trouvent dans `CONTRIBUTING.md` et `docs/entwicklung.md`, le README reste un manuel pour les utilisateurs",
|
|
92
|
-
"it": "docs: il capitolo di installazione del README non contiene più i passaggi di build per lavorare sui sorgenti — sono in `CONTRIBUTING.md` e `docs/entwicklung.md`, così il README resta un manuale per gli utenti",
|
|
93
|
-
"es": "docs: el capítulo de instalación del README ya no incluye los pasos de compilación para trabajar en las fuentes — están en `CONTRIBUTING.md` y `docs/entwicklung.md`, así el README sigue siendo un manual para usuarios",
|
|
94
|
-
"pl": "dok: rozdział instalacji w README nie zawiera już kroków budowania do pracy nad źródłami — są one w `CONTRIBUTING.md` i `docs/entwicklung.md`, a README pozostaje instrukcją dla użytkowników",
|
|
95
|
-
"uk": "док: розділ встановлення в README більше не містить кроків збірки для роботи з джерелами — вони в `CONTRIBUTING.md` та `docs/entwicklung.md`, а README залишається посібником для користувачів",
|
|
96
|
-
"zh-cn": "文档:README 的安装章节不再包含针对源码开发的构建步骤 —— 它们在 `CONTRIBUTING.md` 和 `docs/entwicklung.md` 中,README 保持为面向使用者的手册"
|
|
97
97
|
}
|
|
98
98
|
},
|
|
99
99
|
"titleLang": {
|