iobroker.time-tracker 0.2.0
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/LICENSE +21 -0
- package/README.md +386 -0
- package/admin/i18n/de.json +34 -0
- package/admin/i18n/en.json +1 -0
- package/admin/i18n/es.json +34 -0
- package/admin/i18n/fr.json +34 -0
- package/admin/i18n/it.json +34 -0
- package/admin/i18n/nl.json +34 -0
- package/admin/i18n/pl.json +34 -0
- package/admin/i18n/pt.json +34 -0
- package/admin/i18n/ru.json +34 -0
- package/admin/i18n/uk.json +34 -0
- package/admin/i18n/zh-cn.json +34 -0
- package/admin/jsonConfig.json +266 -0
- package/admin/time-tracker.png +0 -0
- package/build/lib/adapter/automation.js +100 -0
- package/build/lib/adapter/automation.js.map +7 -0
- package/build/lib/adapter/commands.js +160 -0
- package/build/lib/adapter/commands.js.map +7 -0
- package/build/lib/adapter/messages.js +207 -0
- package/build/lib/adapter/messages.js.map +7 -0
- package/build/lib/adapter/presence.js +129 -0
- package/build/lib/adapter/presence.js.map +7 -0
- package/build/lib/adapter/states.js +330 -0
- package/build/lib/adapter/states.js.map +7 -0
- package/build/lib/adapter/triggers.js +92 -0
- package/build/lib/adapter/triggers.js.map +7 -0
- package/build/lib/db/database.js +96 -0
- package/build/lib/db/database.js.map +7 -0
- package/build/lib/db/migrations.js +603 -0
- package/build/lib/db/migrations.js.map +7 -0
- package/build/lib/db/repositories/absences.js +432 -0
- package/build/lib/db/repositories/absences.js.map +7 -0
- package/build/lib/db/repositories/audit.js +90 -0
- package/build/lib/db/repositories/audit.js.map +7 -0
- package/build/lib/db/repositories/automations.js +326 -0
- package/build/lib/db/repositories/automations.js.map +7 -0
- package/build/lib/db/repositories/entries.js +335 -0
- package/build/lib/db/repositories/entries.js.map +7 -0
- package/build/lib/db/repositories/holidays.js +156 -0
- package/build/lib/db/repositories/holidays.js.map +7 -0
- package/build/lib/db/repositories/payouts.js +221 -0
- package/build/lib/db/repositories/payouts.js.map +7 -0
- package/build/lib/db/repositories/rfid.js +264 -0
- package/build/lib/db/repositories/rfid.js.map +7 -0
- package/build/lib/db/repositories/rules.js +356 -0
- package/build/lib/db/repositories/rules.js.map +7 -0
- package/build/lib/db/repositories/settings.js +91 -0
- package/build/lib/db/repositories/settings.js.map +7 -0
- package/build/lib/db/repositories/terminals.js +243 -0
- package/build/lib/db/repositories/terminals.js.map +7 -0
- package/build/lib/db/repositories/triggers.js +240 -0
- package/build/lib/db/repositories/triggers.js.map +7 -0
- package/build/lib/db/repositories/users.js +491 -0
- package/build/lib/db/repositories/users.js.map +7 -0
- package/build/lib/db/seed.js +180 -0
- package/build/lib/db/seed.js.map +7 -0
- package/build/lib/domain/avatar.js +63 -0
- package/build/lib/domain/avatar.js.map +7 -0
- package/build/lib/domain/breaks.js +70 -0
- package/build/lib/domain/breaks.js.map +7 -0
- package/build/lib/domain/calculation.js +150 -0
- package/build/lib/domain/calculation.js.map +7 -0
- package/build/lib/domain/holidays.js +113 -0
- package/build/lib/domain/holidays.js.map +7 -0
- package/build/lib/domain/punch.js +99 -0
- package/build/lib/domain/punch.js.map +7 -0
- package/build/lib/domain/target.js +89 -0
- package/build/lib/domain/target.js.map +7 -0
- package/build/lib/errors.js +56 -0
- package/build/lib/errors.js.map +7 -0
- package/build/lib/reports/labels.js +358 -0
- package/build/lib/reports/labels.js.map +7 -0
- package/build/lib/reports/pdf.js +308 -0
- package/build/lib/reports/pdf.js.map +7 -0
- package/build/lib/reports/types.js +32 -0
- package/build/lib/reports/types.js.map +7 -0
- package/build/lib/reports/xls.js +211 -0
- package/build/lib/reports/xls.js.map +7 -0
- package/build/lib/services/aggregation.js +495 -0
- package/build/lib/services/aggregation.js.map +7 -0
- package/build/lib/services/auth.js +381 -0
- package/build/lib/services/auth.js.map +7 -0
- package/build/lib/services/backup.js +372 -0
- package/build/lib/services/backup.js.map +7 -0
- package/build/lib/services/closing.js +106 -0
- package/build/lib/services/closing.js.map +7 -0
- package/build/lib/services/sessionSecret.js +100 -0
- package/build/lib/services/sessionSecret.js.map +7 -0
- package/build/lib/services/sync.js +278 -0
- package/build/lib/services/sync.js.map +7 -0
- package/build/lib/util/time.js +125 -0
- package/build/lib/util/time.js.map +7 -0
- package/build/lib/web/api.js +2589 -0
- package/build/lib/web/api.js.map +7 -0
- package/build/lib/web/cookies.js +80 -0
- package/build/lib/web/cookies.js.map +7 -0
- package/build/lib/web/events.js +50 -0
- package/build/lib/web/events.js.map +7 -0
- package/build/lib/web/pin-guard.js +86 -0
- package/build/lib/web/pin-guard.js.map +7 -0
- package/build/lib/web/problem.js +129 -0
- package/build/lib/web/problem.js.map +7 -0
- package/build/lib/web/rate-limit.js +65 -0
- package/build/lib/web/rate-limit.js.map +7 -0
- package/build/lib/web/router.js +274 -0
- package/build/lib/web/router.js.map +7 -0
- package/build/lib/web/server.js +189 -0
- package/build/lib/web/server.js.map +7 -0
- package/build/lib/web/static.js +151 -0
- package/build/lib/web/static.js.map +7 -0
- package/build/lib/web/stream.js +135 -0
- package/build/lib/web/stream.js.map +7 -0
- package/build/main.js +986 -0
- package/build/main.js.map +7 -0
- package/io-package.json +221 -0
- package/package.json +131 -0
- package/src-pwa/src/i18n/de.json +359 -0
- package/src-pwa/src/i18n/en.json +359 -0
- package/src-pwa/src/i18n/es.json +359 -0
- package/src-pwa/src/i18n/fr.json +359 -0
- package/src-pwa/src/i18n/index.ts +96 -0
- package/src-pwa/src/i18n/it.json +359 -0
- package/src-pwa/src/i18n/nl.json +359 -0
- package/src-pwa/src/i18n/pl.json +359 -0
- package/src-pwa/src/i18n/pt.json +359 -0
- package/src-pwa/src/i18n/ru.json +359 -0
- package/src-pwa/src/i18n/uk.json +359 -0
- package/src-pwa/src/i18n/zh-cn.json +359 -0
- package/www/assets/index-QI3wrEvW.js +119 -0
- package/www/assets/index-QI3wrEvW.js.map +1 -0
- package/www/assets/workbox-window.prod.es5-Bd17z0YL.js +3 -0
- package/www/assets/workbox-window.prod.es5-Bd17z0YL.js.map +1 -0
- package/www/favicon.svg +16 -0
- package/www/icon-192.png +0 -0
- package/www/icon-512.png +0 -0
- package/www/index.html +38 -0
- package/www/manifest.webmanifest +1 -0
- package/www/person.png +0 -0
- package/www/sw.js +3 -0
- package/www/sw.js.map +1 -0
- package/www/workbox-9c191d2f.js +3 -0
- package/www/workbox-9c191d2f.js.map +1 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/main.ts"],
|
|
4
|
+
"sourcesContent": ["/*\n * ioBroker adapter \"time-tracker\" \u2013 time tracking\n * Scaffolded with @iobroker/create-adapter v3.1.5\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as utils from \"@iobroker/adapter-core\";\nimport { currentSchemaVersion, openAndMigrate, type Db } from \"./lib/db/database\";\nimport { seed } from \"./lib/db/seed\";\nimport type { HolidayCountry } from \"./lib/domain/holidays\";\nimport { createAbsencesRepository } from \"./lib/db/repositories/absences\";\nimport { createEntriesRepository } from \"./lib/db/repositories/entries\";\nimport { createHolidaysRepository } from \"./lib/db/repositories/holidays\";\nimport { createPayoutsRepository } from \"./lib/db/repositories/payouts\";\nimport { createRulesRepository } from \"./lib/db/repositories/rules\";\nimport { createSettingsRepository } from \"./lib/db/repositories/settings\";\nimport { createUsersRepository, type UserRecord, type UsersRepository } from \"./lib/db/repositories/users\";\nimport {\n\tcreateAutomationsRepository,\n\ttype AutomationRuleRecord,\n\ttype AutomationsRepository,\n} from \"./lib/db/repositories/automations\";\nimport { createTerminalsRepository } from \"./lib/db/repositories/terminals\";\nimport { createRfidRepository } from \"./lib/db/repositories/rfid\";\nimport {\n\tcreateTriggersRepository,\n\ttype TriggerRuleRecord,\n\ttype TriggersRepository,\n} from \"./lib/db/repositories/triggers\";\nimport type { EntriesRepository } from \"./lib/db/repositories/entries\";\nimport type { AbsencesRepository } from \"./lib/db/repositories/absences\";\nimport type { SettingsRepository } from \"./lib/db/repositories/settings\";\nimport { createAggregationService, type AggregationService } from \"./lib/services/aggregation\";\nimport { createAuthService, hashPassword } from \"./lib/services/auth\";\nimport { SECRET_FILE_NAME, resolveSessionSecret } from \"./lib/services/sessionSecret\";\nimport { createClosingService, type ClosingService } from \"./lib/services/closing\";\nimport {\n\tapplyPendingRestore,\n\tcreateBackupService,\n\trecordRestore,\n\ttype AppliedRestore,\n\ttype BackupService,\n} from \"./lib/services/backup\";\nimport { createSyncService, type SyncService } from \"./lib/services/sync\";\nimport {\n\tCOMMAND_IDS,\n\tcreateCommandStates,\n\tcreateCompanyStates,\n\tcreateEventStates,\n\tcreateInfoStates,\n\tpublishAllUserStates,\n\tpublishCompanySnapshot,\n\tpublishEventSnapshot,\n\treadCompanySnapshot,\n} from \"./lib/adapter/states\";\nimport { PRESENCE_SUFFIX, handlePresenceState, parsePresenceStateId } from \"./lib/adapter/presence\";\nimport { handleCommand, punchEmployee } from \"./lib/adapter/commands\";\nimport { evaluateTrigger, triggerText } from \"./lib/adapter/triggers\";\nimport { evaluateAutomation, isoWeekday, runPeriod, workBlock } from \"./lib/adapter/automation\";\nimport { handleMessage } from \"./lib/adapter/messages\";\nimport { createApi, MAX_BACKUP_UPLOAD_BYTES } from \"./lib/web/api\";\nimport type { ApiEvent, EventBus } from \"./lib/web/events\";\nimport { startWebServer, type WebServer } from \"./lib/web/server\";\nimport { localDateTime } from \"./lib/util/time\";\nimport { createStaticHandler } from \"./lib/web/static\";\n\nconst SUPPORTED_COUNTRIES: HolidayCountry[] = [\"CH\", \"DE\", \"AT\"];\n\n/** How often expired sessions are removed (minutes). */\nconst SESSION_PURGE_MINUTES = 30;\n\n/** How often the published figures are refreshed (minutes). */\nconst STATE_REFRESH_MINUTES = 5;\n\n/** How old the newest backup may be before the daily check writes a new one. */\nconst BACKUP_MAX_AGE_HOURS = 20;\n\n/** How often the automation rules are checked (minutes). */\nconst AUTOMATION_CHECK_MINUTES = 1;\n\n/** Name of the file that holds a generated badge (HMAC) secret next to the database. */\nconst TAG_SECRET_FILE_NAME = \"tag-secret\";\n\n/**\n * Events of the API that change the published figures.\n *\n * A punch or a correction arrives through the API of this very process, so nothing would republish the states\n * until the five minute timer fires; these events say exactly when data changed.\n */\nconst FIGURES_EVENT_TYPES = [\"punch\", \"terminal.punch\", \"rfid.scan\", \"entry.update\", \"entry.delete\", \"absence.change\"];\n\n/** Services created at startup. */\ninterface AdapterServices {\n\tusers: UsersRepository;\n\tentries: EntriesRepository;\n\tabsences: AbsencesRepository;\n\tsettings: SettingsRepository;\n\taggregation: AggregationService;\n\tsync: SyncService;\n\tclosing: ClosingService;\n\tbackup: BackupService;\n\ttriggers: TriggersRepository;\n\tautomations: AutomationsRepository;\n}\n\nclass TimeTracker extends utils.Adapter {\n\tprivate db: Db | null = null;\n\tprivate webServer: WebServer | null = null;\n\tprivate services: AdapterServices | null = null;\n\t/** Bus of the API; `null` until the API is created */\n\tprivate events: EventBus | null = null;\n\t/** Last value seen per watched trigger state: only a change fires a rule */\n\tprivate readonly triggerValues = new Map<string, string>();\n\t/** Trigger states the adapter currently subscribes to */\n\tprivate readonly triggerStates = new Set<string>();\n\t/** Pending refresh of the figures after a change through the API */\n\tprivate stateRefreshTimer: ioBroker.Timeout | undefined = undefined;\n\n\tpublic constructor(options: Partial<utils.AdapterOptions> = {}) {\n\t\tsuper({\n\t\t\t...options,\n\t\t\tname: \"time-tracker\",\n\t\t});\n\t\tthis.on(\"ready\", this.onReady.bind(this));\n\t\tthis.on(\"stateChange\", this.onStateChange.bind(this));\n\t\t// this.on(\"objectChange\", this.onObjectChange.bind(this));\n\t\t// scripts, Blockly and other adapters reach the instance through `sendTo` messages\n\t\tthis.on(\"message\", this.onMessage.bind(this));\n\t\tthis.on(\"unload\", this.onUnload.bind(this));\n\t}\n\n\t/** Database file: configured path or `<adapter instance data dir>/time-tracker.sqlite`. */\n\tprivate databaseFile(): string {\n\t\tconst configured = (this.config.dbPath ?? \"\").trim();\n\t\tif (configured) {\n\t\t\treturn path.resolve(configured);\n\t\t}\n\n\t\tconst dir = utils.getAbsoluteInstanceDataDir(this);\n\t\tfs.mkdirSync(dir, { recursive: true });\n\t\treturn path.join(dir, \"time-tracker.sqlite\");\n\t}\n\n\t/** Configured holiday country, falling back to Switzerland. */\n\tprivate holidayCountry(): HolidayCountry {\n\t\tconst configured = (this.config.holidayCountry ?? \"\").toUpperCase() as HolidayCountry;\n\t\treturn SUPPORTED_COUNTRIES.includes(configured) ? configured : \"CH\";\n\t}\n\n\t/**\n\t * Is called when databases are connected and adapter received configuration.\n\t */\n\tprivate async onReady(): Promise<void> {\n\t\ttry {\n\t\t\t// Reset the connection indicator during startup\n\t\t\tawait this.setState(\"info.connection\", false, true);\n\n\t\t\tconst file = this.databaseFile();\n\t\t\t// A restore that the administration queued is applied before the database is opened: the swap needs a\n\t\t\t// closed file, and that is the only moment the adapter can offer it (see the backup service).\n\t\t\tlet applied: AppliedRestore | null = null;\n\t\t\ttry {\n\t\t\t\tapplied = applyPendingRestore(file);\n\t\t\t\tif (applied) {\n\t\t\t\t\tthis.log.info(\n\t\t\t\t\t\t`restored the database from ${applied.restored.name}: ${applied.restored.users} users, ${applied.restored.entries} punches, previous file ${applied.previous ?? \"(none)\"}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\t// the queued files stay in place, so a working backup can be handed in afterwards\n\t\t\t\tthis.log.error(`the queued restore was refused: ${(error as Error).message}`);\n\t\t\t}\n\t\t\tthis.db = openAndMigrate(file, message => this.log.debug(message));\n\t\t\tif (applied) {\n\t\t\t\t// the audit entry can only be written once the restored database is open\n\t\t\t\trecordRestore(this.db, applied.restored, applied.actorId);\n\t\t\t}\n\n\t\t\tconst year = new Date().getUTCFullYear();\n\t\t\tconst country = this.holidayCountry();\n\t\t\tconst result = seed(this.db, { holidayCountry: country, holidayYears: [year, year + 1] });\n\t\t\tthis.log.info(\n\t\t\t\t`database ready at ${file} (${result.permissions} permissions, ${result.holidays} holidays added for ${country})`,\n\t\t\t);\n\n\t\t\t// Without an account nobody can log in, so the first administrator is created here (Phase 2 of the\n\t\t\t// specification: \"Standard-Admin, Passwort mit Pflichtwechsel\").\n\t\t\tthis.ensureAdministrator(this.db);\n\n\t\t\t// the settings of the admin UI win over the stored values (before the services are built)\n\t\t\tthis.applyConfiguration(createSettingsRepository(this.db));\n\n\t\t\t// NOTE: The implementation follows the internal specification (not part of this repository):\n\t\t\t// Phase 1\u20133 \u2013 storage, punch logic, target time, breaks, overtime, vacation\n\t\t\t// Phase 4 \u2013 REST API on an own port (router, sessions, RBAC)\n\t\t\t// Phase 5 \u2013 PWA delivered from www/\n\t\t\t// Phase 8 \u2013 publish aggregates as states and react to command.*\n\n\t\t\tawait this.startApi();\n\t\t\t// creates the command and info states, so the instance information can be published afterwards\n\t\t\tawait this.subscribeCommands();\n\t\t\t// watches the states the trigger rules name (fingerprint reader, button, door contact, \u2026)\n\t\t\tawait this.refreshTriggerSubscriptions();\n\t\t\t// the automation rules look at the clock, so they are checked once a minute\n\t\t\tthis.runAutomationRules();\n\t\t\tthis.setInterval(() => this.runAutomationRules(), AUTOMATION_CHECK_MINUTES * 60 * 1000);\n\t\t\tawait this.publishInstanceInfo();\n\t\t\tawait this.refreshStates();\n\n\t\t\t// figures are refreshed regularly (the timer is cleared automatically on unload)\n\t\t\tthis.setInterval(\n\t\t\t\t() => {\n\t\t\t\t\tvoid this.publishInstanceInfo();\n\t\t\t\t\tvoid this.refreshStates();\n\t\t\t\t},\n\t\t\t\tSTATE_REFRESH_MINUTES * 60 * 1000,\n\t\t\t);\n\n\t\t\t// one backup per day: the check runs every hour, so a missed run is caught up after a restart\n\t\t\tawait this.runScheduledBackup();\n\t\t\tthis.setInterval(() => void this.runScheduledBackup(), 60 * 60 * 1000);\n\n\t\t\t// Service is ready\n\t\t\tawait this.setState(\"info.connection\", true, true);\n\t\t} catch (error) {\n\t\t\tconst message = (error as Error).message;\n\t\t\tthis.log.error(`Startup failed: ${message}`);\n\t\t\tawait this.setState(\"info.lastError\", message, true);\n\t\t}\n\t}\n\n\t/**\n\t * Creates the first administrator when the instance has none.\n\t *\n\t * The account is created with `must_change_pw = 1`, so the start password opens the door exactly once.\n\t * Login and password can be configured; without a configured password a random one is generated and written\n\t * to the log, because there is no other way to hand it to the operator.\n\t *\n\t * @param db - open database handle\n\t */\n\tprivate ensureAdministrator(db: Db): void {\n\t\tconst users = createUsersRepository(db);\n\t\tconst administrators = users.list().filter(user => users.roles(user.id).includes(\"admin\"));\n\t\tif (administrators.length > 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst login = (this.config.adminLogin ?? \"\").trim() || \"admin\";\n\t\tconst configured = (this.config.adminPassword ?? \"\").trim();\n\t\tconst password = configured || `Zf-${randomBytes(9).toString(\"base64url\")}-7`;\n\n\t\ttry {\n\t\t\tusers.create({\n\t\t\t\tlogin,\n\t\t\t\tdisplayName: login,\n\t\t\t\tpasswordHash: hashPassword(password),\n\t\t\t\tmustChangePw: true,\n\t\t\t\ttimezone: this.config.timezone || \"Europe/Berlin\",\n\t\t\t\troleKeys: [\"admin\"],\n\t\t\t\tnow: Math.floor(Date.now() / 1000),\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tthis.log.warn(`the administrator \"${login}\" could not be created: ${(error as Error).message}`);\n\t\t\treturn;\n\t\t}\n\n\t\tif (configured) {\n\t\t\tthis.log.info(\n\t\t\t\t`administrator \"${login}\" created with the configured start password - it has to be changed at the first login`,\n\t\t\t);\n\t\t} else {\n\t\t\tthis.log.warn(\n\t\t\t\t`administrator \"${login}\" created with the start password \"${password}\" - change it at the first login`,\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Copies the instance configuration into the stored settings and publishes the instance information.\n\t *\n\t * The ioBroker admin UI is where an operator configures the instance, so a configured value wins over what\n\t * is stored in the database; `PUT /api/settings` stays for the keys the admin UI does not offer. Empty\n\t * fields are ignored, so clearing a field never wipes a stored setting.\n\t *\n\t * @param settings - instance settings\n\t */\n\tprivate applyConfiguration(settings: SettingsRepository): void {\n\t\tconst text = (value: string | undefined): string | null => (value?.trim() ? value.trim() : null);\n\t\tconst number = (value: number | undefined): string | null =>\n\t\t\ttypeof value === \"number\" && Number.isFinite(value) ? String(value) : null;\n\t\tconst flag = (value: boolean | undefined): string | null => (value === undefined ? null : value ? \"1\" : \"0\");\n\n\t\tconst mapped: [string, string | null][] = [\n\t\t\t[\"holiday_country\", text(this.config.holidayCountry)?.toUpperCase() ?? null],\n\t\t\t[\"timezone\", text(this.config.timezone)],\n\t\t\t[\"default_language\", text(this.config.defaultLanguage)],\n\t\t\t[\"edit_window_days\", number(this.config.editWindowDays)],\n\t\t\t[\"quick_round_minutes\", number(this.config.quickRoundMinutes)],\n\t\t\t[\"session_ttl_minutes\", number(this.config.sessionTtlMinutes)],\n\t\t\t[\"backup_retention_days\", number(this.config.backupRetentionDays)],\n\t\t\t[\"absence_calc_until_today\", flag(this.config.absenceCalcUntilToday)],\n\t\t\t[\"absence_deduct_worktime\", flag(this.config.absenceDeductWorktime)],\n\t\t];\n\n\t\tconst now = Math.floor(Date.now() / 1000);\n\t\tfor (const [key, value] of mapped) {\n\t\t\tif (value !== null && settings.get(key) !== value) {\n\t\t\t\tsettings.set(key, value, null, now);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Publishes version, database size and schema version as states (specification 5.1).\n\t *\n\t * The objects are created by `createInfoStates`, so this runs after the command states were set up.\n\t */\n\tprivate async publishInstanceInfo(): Promise<void> {\n\t\tawait this.setState(\"info.version\", this.version ?? \"0.0.0\", true);\n\t\tconst file = this.db ? this.databaseFile() : \"\";\n\t\tawait this.setState(\"info.dbSizeBytes\", file && fs.existsSync(file) ? fs.statSync(file).size : 0, true);\n\t\tif (this.db) {\n\t\t\tawait this.setState(\"info.schemaVersion\", String(currentSchemaVersion(this.db)), true);\n\t\t}\n\t}\n\n\t/**\n\t * Secret used to sign CSRF tokens. A value from the instance settings wins; without one the adapter generates a\n\t * secret on the first start and stores it next to the database, so a restart keeps the CSRF tokens of clients\n\t * that are already open valid.\n\t */\n\tprivate sessionSecret(): string {\n\t\tconst resolved = resolveSessionSecret({\n\t\t\tconfigured: this.config.sessionSecret,\n\t\t\tfile: path.join(path.dirname(this.databaseFile()), SECRET_FILE_NAME),\n\t\t});\n\n\t\tif (resolved.source === \"configured\") {\n\t\t\tthis.log.debug(\"session secret taken from the instance settings\");\n\t\t} else if (resolved.source === \"stored\") {\n\t\t\tthis.log.debug(`session secret taken from ${resolved.file}`);\n\t\t} else if (resolved.file) {\n\t\t\tthis.log.info(\n\t\t\t\t`no session secret configured - generated one and stored it at ${resolved.file}, so sessions and CSRF tokens survive restarts`,\n\t\t\t);\n\t\t} else {\n\t\t\tthis.log.warn(\n\t\t\t\t`no session secret configured and it could not be stored (${resolved.error ?? \"unknown\"}) - a temporary one is used, so all sessions end with the next restart`,\n\t\t\t);\n\t\t}\n\n\t\treturn resolved.secret;\n\t}\n\n\t/**\n\t * Secret that signs the badge/NFC links.\n\t *\n\t * A value from the instance settings wins. Without one a secret is generated on the first start and stored next\n\t * to the database \u2014 otherwise a fresh installation could not create a badge at all and the administration would\n\t * only report \u201Cnot configured\u201D.\n\t */\n\tprivate tagSecret(): string {\n\t\tconst resolved = resolveSessionSecret({\n\t\t\tconfigured: this.config.hmacSecret,\n\t\t\tfile: path.join(path.dirname(this.databaseFile()), TAG_SECRET_FILE_NAME),\n\t\t});\n\n\t\tif (resolved.source === \"configured\") {\n\t\t\tthis.log.debug(\"badge link secret taken from the instance settings\");\n\t\t} else if (resolved.source === \"stored\") {\n\t\t\tthis.log.debug(`badge link secret taken from ${resolved.file}`);\n\t\t} else if (resolved.file) {\n\t\t\tthis.log.info(\n\t\t\t\t`no badge link secret configured - generated one and stored it at ${resolved.file}, so badges stay valid across restarts`,\n\t\t\t);\n\t\t} else {\n\t\t\tthis.log.warn(\n\t\t\t\t`no badge link secret configured and it could not be stored (${resolved.error ?? \"unknown\"}) - a temporary one is used, so badges stop working with the next restart`,\n\t\t\t);\n\t\t}\n\n\t\treturn resolved.secret;\n\t}\n\n\t/**\n\t * Builds the services and starts the HTTP server on the configured port.\n\t *\n\t * Binding can fail (port in use); that must never stop the adapter, so the failure is only logged.\n\t */\n\tprivate async startApi(): Promise<void> {\n\t\tconst db = this.db;\n\t\tif (!db) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst users = createUsersRepository(db);\n\t\tconst entries = createEntriesRepository(db);\n\t\tconst absences = createAbsencesRepository(db);\n\t\tconst holidays = createHolidaysRepository(db);\n\t\tconst rules = createRulesRepository(db);\n\t\tconst payouts = createPayoutsRepository(db);\n\t\tconst terminals = createTerminalsRepository(db);\n\t\tconst rfid = createRfidRepository(db);\n\t\tconst settings = createSettingsRepository(db);\n\t\tconst auth = createAuthService({\n\t\t\tdb,\n\t\t\tusers,\n\t\t\tsettings,\n\t\t\tsecret: this.sessionSecret(),\n\t\t\tdefaultTtlMinutes: this.config.sessionTtlMinutes || 720,\n\t\t});\n\t\tconst aggregation = createAggregationService({\n\t\t\tdb,\n\t\t\tusers,\n\t\t\tentries,\n\t\t\tabsences,\n\t\t\tholidays,\n\t\t\trules,\n\t\t\tsettings,\n\t\t});\n\t\tconst triggers = createTriggersRepository(db);\n\t\tconst automations = createAutomationsRepository(db);\n\t\tconst sync = createSyncService({ db, entries, users, aggregation });\n\t\tconst closing = createClosingService({ db, aggregation, payouts });\n\t\t// backups live next to the database file: `<data dir>/backups/time-tracker-<timestamp>.sqlite`\n\t\tconst backup = createBackupService({\n\t\t\tdb,\n\t\t\tdir: path.join(path.dirname(this.databaseFile()), \"backups\"),\n\t\t\tretentionDays: Math.max(0, Number(this.config.backupRetentionDays ?? 30) || 0),\n\t\t});\n\t\tconst api = createApi({\n\t\t\tdb,\n\t\t\tauth,\n\t\t\tusers,\n\t\t\tentries,\n\t\t\tabsences,\n\t\t\tholidays,\n\t\t\trules,\n\t\t\tpayouts,\n\t\t\tterminals,\n\t\t\trfid,\n\t\t\ttriggers,\n\t\t\tautomations,\n\t\t\taggregation,\n\t\t\tsync,\n\t\t\tsettings,\n\t\t\tbackup,\n\t\t\tkioskEnabled: this.config.kioskEnabled === true,\n\t\t\t// a reverse proxy in front is the normal case for HTTPS; without the switch the forwarded\n\t\t\t// headers are ignored, so a client cannot choose its own address or the HTTPS flag\n\t\t\ttrustProxy: this.config.trustProxy === true,\n\t\t\thmacSecret: this.tagSecret(),\n\t\t\tversion: this.version,\n\t\t\t// a saved trigger rule changes the states the adapter has to watch\n\t\t\tonTriggerRulesChanged: () => void this.refreshTriggerSubscriptions(),\n\t\t});\n\n\t\tthis.services = {\n\t\t\tusers,\n\t\t\tentries,\n\t\t\tabsences,\n\t\t\tsettings,\n\t\t\taggregation,\n\t\t\tsync,\n\t\t\tclosing,\n\t\t\tbackup,\n\t\t\ttriggers,\n\t\t\tautomations,\n\t\t};\n\t\tthis.events = api.events;\n\t\t// the newest event is mirrored into the state tree, so a notification only has to watch `events.*`;\n\t\t// a punch, a correction or an absence changes the figures, so they are republished right away\n\t\tapi.events.subscribe(event => {\n\t\t\tvoid this.publishEventState(event);\n\t\t\tif (FIGURES_EVENT_TYPES.includes(event.type)) {\n\t\t\t\tthis.scheduleStateRefresh();\n\t\t\t}\n\t\t});\n\t\tthis.log.debug(`API routes: ${api.routes().length}`);\n\n\t\t// the web interface is delivered from `www/` next to the compiled code (built by the PWA project);\n\t\t// without it the adapter still provides the API, e.g. for the terminal\n\t\tconst webDir = path.join(__dirname, \"..\", \"www\");\n\t\tconst staticFiles = fs.existsSync(path.join(webDir, \"index.html\"))\n\t\t\t? createStaticHandler({ root: webDir })\n\t\t\t: undefined;\n\t\tthis.log.info(\n\t\t\tstaticFiles\n\t\t\t\t? `web interface found at ${webDir}`\n\t\t\t\t: `no web interface at ${webDir} - only the API (and the terminal) is available`,\n\t\t);\n\n\t\ttry {\n\t\t\tthis.webServer = await startWebServer({\n\t\t\t\trouter: api.router,\n\t\t\t\tport: this.config.port || 8092,\n\t\t\t\tbind: this.config.bind || \"127.0.0.1\",\n\t\t\t\tstaticFiles,\n\t\t\t\t// an uploaded backup is bigger than the default limit; the route carries the same bound\n\t\t\t\tmaxBodyBytes: MAX_BACKUP_UPLOAD_BYTES,\n\t\t\t\t// live events for the PWA and the terminal: `/api/stream?token=...`\n\t\t\t\tstream: {\n\t\t\t\t\tauth,\n\t\t\t\t\tevents: api.events,\n\t\t\t\t\tversion: this.version,\n\t\t\t\t\ttimers: {\n\t\t\t\t\t\t// the adapter's own timer functions: they are cleared with the adapter on unload\n\t\t\t\t\t\tsetInterval: (handler, milliseconds) => this.setInterval(handler, milliseconds),\n\t\t\t\t\t\tclearInterval: handle => this.clearInterval(handle as ioBroker.Interval),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tlog: {\n\t\t\t\t\tinfo: message => this.log.info(message),\n\t\t\t\t\twarn: message => this.log.warn(message),\n\t\t\t\t\terror: message => this.log.error(message),\n\t\t\t\t},\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tthis.log.error(\n\t\t\t\t`API could not be started on port ${this.config.port}: ${(error as Error).message} - the web interface and the terminal stay unavailable`,\n\t\t\t);\n\t\t\tthis.webServer = null;\n\t\t\treturn;\n\t\t}\n\n\t\t// expired sessions are removed regularly; the timer is cleared automatically on unload\n\t\tthis.setInterval(\n\t\t\t() => {\n\t\t\t\ttry {\n\t\t\t\t\tconst removed = auth.purge();\n\t\t\t\t\tif (removed > 0) {\n\t\t\t\t\t\tthis.log.debug(`removed ${removed} expired session(s)`);\n\t\t\t\t\t}\n\t\t\t\t} catch (error) {\n\t\t\t\t\tthis.log.warn(`session cleanup failed: ${(error as Error).message}`);\n\t\t\t\t}\n\t\t\t},\n\t\t\tSESSION_PURGE_MINUTES * 60 * 1000,\n\t\t);\n\t}\n\n\t/**\n\t * Creates the command states and subscribes to them.\n\t */\n\tprivate async subscribeCommands(): Promise<void> {\n\t\ttry {\n\t\t\tawait createCommandStates(this);\n\t\t\tawait createInfoStates(this);\n\t\t\tawait createCompanyStates(this);\n\t\t\tawait createEventStates(this);\n\t\t\tawait this.subscribeStatesAsync(\"commands.*\");\n\t\t\t// the presence switch of every employee: written by a script, a fingerprint reader or a dashboard\n\t\t\tawait this.subscribeStatesAsync(`users.*.${PRESENCE_SUFFIX}`);\n\t\t\tthis.log.debug(\"command states ready\");\n\t\t} catch (error) {\n\t\t\tthis.log.warn(`command states could not be created: ${(error as Error).message}`);\n\t\t}\n\t}\n\n\t/**\n\t * Writes a backup unless a recent one exists.\n\t *\n\t * The check runs hourly and once at start: a nightly backup therefore happens within an hour at the latest,\n\t * and an instance that was down for days does not write a burst of copies.\n\t */\n\tprivate async runScheduledBackup(): Promise<void> {\n\t\tconst backup = this.services?.backup;\n\t\tif (!backup) {\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tconst newest = backup.list()[0];\n\t\t\tconst ageHours = newest ? (Date.now() / 1000 - newest.createdAt) / 3600 : Number.POSITIVE_INFINITY;\n\t\t\tif (ageHours < BACKUP_MAX_AGE_HOURS) {\n\t\t\t\tthis.log.debug(`backup ${newest?.name ?? \"\"} is ${ageHours.toFixed(1)} h old, nothing to do`);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst created = backup.create({ actorId: null, reason: \"scheduled\" });\n\t\t\tthis.log.info(\n\t\t\t\t`backup ${created.backup.name} written (${created.backup.sizeBytes} bytes, ${created.backup.users} employees, ${created.backup.entries} punches), ${created.removed.length} old file(s) removed`,\n\t\t\t);\n\t\t\tawait this.announceBackup(created.backup.name, created.backup.createdAt);\n\t\t} catch (error) {\n\t\t\tthis.log.warn(`backup failed: ${(error as Error).message}`);\n\t\t}\n\t}\n\n\t/**\n\t * Publishes the instant of a written backup.\n\t *\n\t * @param name - file name of the backup\n\t * @param createdAt - instant the backup belongs to\n\t */\n\tprivate async announceBackup(name: string, createdAt: number): Promise<void> {\n\t\tawait this.setState(\"info.lastBackup\", createdAt, true);\n\t\tthis.events?.publish({\n\t\t\ttype: \"backup.create\",\n\t\t\tatUtc: createdAt,\n\t\t\tuserId: null,\n\t\t\tdata: { backup: name },\n\t\t});\n\t}\n\n\t/**\n\t * Applies a write on the presence state of an employee.\n\t *\n\t * `users.<id>.present` is the writable twin of `users.<id>.hasOpenEntry`: `true` opens an entry, `false` closes\n\t * it. Afterwards the figures of all employees are republished, so a dashboard sees the new state right away.\n\t *\n\t * @param id - state id without the instance prefix\n\t * @param value - value written by the user or a script\n\t */\n\tprivate async runPresence(id: string, value: ioBroker.StateValue): Promise<void> {\n\t\tconst services = this.services;\n\t\tif (!services) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tconst result = handlePresenceState(\n\t\t\t\t{\n\t\t\t\t\tentries: services.entries,\n\t\t\t\t\tusers: services.users,\n\t\t\t\t\taggregation: services.aggregation,\n\t\t\t\t},\n\t\t\t\tid,\n\t\t\t\tvalue,\n\t\t\t);\n\t\t\tif (result.ok) {\n\t\t\t\tthis.log.info(`presence ${id}: ${result.message}`);\n\t\t\t} else {\n\t\t\t\t// a value the state does not understand, an unknown employee: worth a warning, not an error\n\t\t\t\tthis.log.warn(`presence ${id}: ${result.message}`);\n\t\t\t}\n\t\t\tawait this.refreshStates();\n\t\t} catch (error) {\n\t\t\tthis.log.error(`Error in runPresence for ${id}: ${(error as Error).message}`);\n\t\t}\n\t}\n\n\t/**\n\t * Publishes the figures of all employees.\n\t */\n\tprivate async refreshStates(): Promise<void> {\n\t\tconst services = this.services;\n\t\tif (!services) {\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tconst snapshots = await publishAllUserStates({\n\t\t\t\tport: this,\n\t\t\t\taggregation: services.aggregation,\n\t\t\t\tusers: services.users,\n\t\t\t\tsync: services.sync,\n\t\t\t});\n\t\t\tthis.log.debug(`published ${snapshots.length} employee state(s)`);\n\t\t\t// the company figures are derived from the same snapshots, so they never disagree\n\t\t\tawait publishCompanySnapshot(this, readCompanySnapshot(snapshots));\n\t\t} catch (error) {\n\t\t\tthis.log.warn(`states could not be published: ${(error as Error).message}`);\n\t\t}\n\t}\n\n\t/**\n\t * Runs a command state and reports the outcome.\n\t *\n\t * @param id - state id without the instance prefix\n\t * @param value - value written by the user or a script\n\t */\n\tprivate async runCommand(id: string, value: ioBroker.StateValue): Promise<void> {\n\t\tconst services = this.services;\n\t\tconst db = this.db;\n\t\tif (!services || !db) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tconst result = handleCommand(\n\t\t\t\t{\n\t\t\t\t\tdb,\n\t\t\t\t\tentries: services.entries,\n\t\t\t\t\tusers: services.users,\n\t\t\t\t\tsettings: services.settings,\n\t\t\t\t\taggregation: services.aggregation,\n\t\t\t\t\tclosing: services.closing,\n\t\t\t\t\tbackup: services.backup,\n\t\t\t\t},\n\t\t\t\tid,\n\t\t\t\tvalue,\n\t\t\t);\n\t\t\tthis.log.info(`command ${id}: ${result.message}`);\n\t\t\t// a closing is triggered from a state, not from a request: tell the connected clients about it\n\t\t\t// (only the period is published, never the figures of the employee it belongs to)\n\t\t\tif (id === COMMAND_IDS.closeMonth) {\n\t\t\t\tthis.events?.publish({\n\t\t\t\t\ttype: \"month.close\",\n\t\t\t\t\tatUtc: Math.floor(Date.now() / 1000),\n\t\t\t\t\tuserId: null,\n\t\t\t\t\tdata: { period: String(value ?? \"\") },\n\t\t\t\t});\n\t\t\t}\n\t\t\t// a manual backup reports its file the same way the scheduled one does\n\t\t\tif (id === COMMAND_IDS.backup && result.ok) {\n\t\t\t\tconst newest = services.backup.list()[0];\n\t\t\t\tif (newest) {\n\t\t\t\t\tawait this.announceBackup(newest.name, newest.createdAt);\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthis.log.warn(`command ${id} failed: ${(error as Error).message}`);\n\t\t}\n\n\t\t// buttons are stateless: always release them again\n\t\tif (id === COMMAND_IDS.punch || id === COMMAND_IDS.quickPunch || id === COMMAND_IDS.backup) {\n\t\t\tawait this.setState(id, false, true);\n\t\t}\n\t\tawait this.refreshStates();\n\t}\n\n\t/**\n\t * Watches exactly the states the active trigger rules name.\n\t *\n\t * Runs at startup and whenever the administration saves the rules, so an added, changed or removed rule takes\n\t * effect without a restart. A state that no rule names any more is dropped from the watch list.\n\t */\n\tprivate async refreshTriggerSubscriptions(): Promise<void> {\n\t\tconst services = this.services;\n\t\tif (!services) {\n\t\t\treturn;\n\t\t}\n\n\t\tlet rules: TriggerRuleRecord[] = [];\n\t\ttry {\n\t\t\trules = services.triggers.list();\n\t\t} catch (error) {\n\t\t\tthis.log.warn(`trigger rules could not be read: ${(error as Error).message}`);\n\t\t\treturn;\n\t\t}\n\n\t\tconst wanted = new Set(rules.map(rule => rule.sourceState));\n\t\tfor (const id of [...this.triggerStates]) {\n\t\t\tif (wanted.has(id)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tawait this.unsubscribeForeignStatesAsync(id);\n\t\t\t} catch (error) {\n\t\t\t\tthis.log.debug(`trigger state ${id} could not be released: ${(error as Error).message}`);\n\t\t\t}\n\t\t\tthis.triggerStates.delete(id);\n\t\t\tthis.triggerValues.delete(id);\n\t\t}\n\n\t\tfor (const id of wanted) {\n\t\t\tif (this.triggerStates.has(id)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tawait this.subscribeForeignStatesAsync(id);\n\t\t\t\tthis.triggerStates.add(id);\n\t\t\t} catch (error) {\n\t\t\t\tthis.log.warn(`trigger state ${id} could not be watched: ${(error as Error).message}`);\n\t\t\t}\n\t\t}\n\n\t\tthis.log.debug(`watching ${this.triggerStates.size} trigger state(s) for ${rules.length} active rule(s)`);\n\t}\n\n\t/**\n\t * Applies a write on a state that trigger rules watch.\n\t *\n\t * Such a state belongs to another adapter, so its `ack` says nothing about whether the value was set by a\n\t * script or by the device. What counts is that the value **changed**: a reader that repeats itself, a\n\t * dashboard that refreshes or a script that writes the same value twice does not punch twice. Together with\n\t * the cooldown of a rule even a rapidly blinking state stays harmless.\n\t *\n\t * @param id - full state id\n\t * @param state - state object of the write\n\t * @returns true when the state belongs to a rule and was handled here\n\t */\n\tprivate handleTriggerState(id: string, state: ioBroker.State): boolean {\n\t\tconst services = this.services;\n\t\tif (!services) {\n\t\t\treturn false;\n\t\t}\n\n\t\tconst previous = this.triggerValues.get(id) ?? null;\n\t\tthis.triggerValues.set(id, triggerText(state.val));\n\n\t\tconst rules = services.triggers.list().filter(rule => rule.sourceState === id);\n\t\tif (rules.length === 0) {\n\t\t\treturn false;\n\t\t}\n\n\t\tconst now = Math.floor(Date.now() / 1000);\n\t\tfor (const rule of rules) {\n\t\t\tconst decision = evaluateTrigger(rule, { value: state.val, previous, now }, services.users);\n\t\t\tif (!decision.fire || decision.userId === null) {\n\t\t\t\tthis.log.debug(`trigger ${rule.id} (${rule.sourceState}) did not fire: ${decision.reason}`);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tthis.runTrigger(rule, decision.userId);\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * Runs the action of a rule that fired.\n\t *\n\t * The punch goes through the same path as the command state and a presence change through the same handler as\n\t * the `users.<id>.present` state, so direction, rounding, notes and the recalculation behave identically.\n\t *\n\t * @param rule - rule that fired\n\t * @param userId - employee the action applies to\n\t */\n\tprivate runTrigger(rule: TriggerRuleRecord, userId: number): void {\n\t\tvoid (async (): Promise<void> => {\n\t\t\tconst services = this.services;\n\t\t\tif (!services) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst result =\n\t\t\t\t\trule.action === \"present\" || rule.action === \"absent\"\n\t\t\t\t\t\t? handlePresenceState(\n\t\t\t\t\t\t\t\t{ entries: services.entries, users: services.users, aggregation: services.aggregation },\n\t\t\t\t\t\t\t\t`users.${userId}.${PRESENCE_SUFFIX}`,\n\t\t\t\t\t\t\t\trule.action === \"present\",\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t: punchEmployee(\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tentries: services.entries,\n\t\t\t\t\t\t\t\t\tusers: services.users,\n\t\t\t\t\t\t\t\t\tsettings: services.settings,\n\t\t\t\t\t\t\t\t\taggregation: services.aggregation,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{ userId, quick: rule.action === \"quickPunch\", note: `trigger.${rule.id}` },\n\t\t\t\t\t\t\t);\n\t\t\t\tthis.log.info(`trigger ${rule.id} (${rule.sourceState}) \u2192 ${result.message}`);\n\t\t\t\tservices.triggers.markFired({ id: rule.id });\n\t\t\t\tawait this.refreshStates();\n\t\t\t} catch (error) {\n\t\t\t\t// a deleted employee, a deactivated one, a value that cannot be used: never take the adapter down\n\t\t\t\tthis.log.warn(`trigger ${rule.id} failed: ${(error as Error).message}`);\n\t\t\t}\n\t\t})();\n\t}\n\n\t/**\n\t * Stops the HTTP server.\n\t */\n\tprivate async stopApi(): Promise<void> {\n\t\tconst server = this.webServer;\n\t\tthis.webServer = null;\n\t\tif (server) {\n\t\t\tawait server.close();\n\t\t\tthis.log.debug(\"API stopped\");\n\t\t}\n\t}\n\n\t/**\n\t * Is called when adapter shuts down - callback has to be called under any circumstances!\n\t *\n\t * @param callback - Callback function\n\t */\n\tprivate onUnload(callback: () => void): void {\n\t\ttry {\n\t\t\t// Timers created with this.setTimeout/this.setInterval are cleared automatically on unload.\n\t\t\tvoid this.stopApi()\n\t\t\t\t.catch(error => this.log.warn(`stopping the API failed: ${(error as Error).message}`))\n\t\t\t\t.finally(() => {\n\t\t\t\t\tthis.db?.close();\n\t\t\t\t\tthis.db = null;\n\t\t\t\t\tcallback();\n\t\t\t\t});\n\t\t} catch (error) {\n\t\t\tthis.log.error(`Error during unloading: ${(error as Error).message}`);\n\t\t\tcallback();\n\t\t}\n\t}\n\n\t/**\n\t * Runs the automation rules that are due.\n\t *\n\t * A rule acts at most once per employee and period \u2014 the local date, or the ISO week for a weekly rule;\n\t * `automation_runs` holds that decision, so a restart\n\t * cannot punch twice. The run is noted **before** the action (a double punch would be worse than a missed\n\t * reminder) and taken back when the action fails, so the next minute retries it.\n\t */\n\tprivate runAutomationRules(): void {\n\t\tconst services = this.services;\n\t\tif (!services) {\n\t\t\treturn;\n\t\t}\n\n\t\tlet rules: AutomationRuleRecord[] = [];\n\t\ttry {\n\t\t\trules = services.automations.list();\n\t\t} catch (error) {\n\t\t\tthis.log.warn(`automation rules could not be read: ${(error as Error).message}`);\n\t\t\treturn;\n\t\t}\n\t\tif (rules.length === 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst nowUtc = Math.floor(Date.now() / 1000);\n\t\tlet acted = false;\n\n\t\tfor (const rule of rules) {\n\t\t\tconst candidates =\n\t\t\t\trule.userId === null\n\t\t\t\t\t? services.users.list()\n\t\t\t\t\t: [services.users.findById(rule.userId)].filter((user): user is UserRecord => user !== null);\n\n\t\t\tfor (const user of candidates) {\n\t\t\t\tif (!user.isActive) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst local = localDateTime(nowUtc, user.timezone);\n\t\t\t\t// a weekly rule is guarded per ISO week, a daily rule per local date\n\t\t\t\tconst period = runPeriod(rule.repeat, local.date);\n\t\t\t\tif (services.automations.hasRun({ ruleId: rule.id, userId: user.id, period })) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst punches = services.entries\n\t\t\t\t\t.listByDate(user.id, local.date)\n\t\t\t\t\t.map(entry => ({ tsUtc: entry.tsUtc, direction: entry.direction }));\n\t\t\t\tconst block = workBlock(punches, nowUtc);\n\t\t\t\tconst decision = evaluateAutomation(rule, {\n\t\t\t\t\tlocalDate: local.date,\n\t\t\t\t\tweekday: isoWeekday(local.date),\n\t\t\t\t\tminuteOfDay: local.minutes,\n\t\t\t\t\thasOpenEntry: block.hasOpenEntry,\n\t\t\t\t\tblockMinutes: block.blockMinutes,\n\t\t\t\t\talreadyRan: false,\n\t\t\t\t});\n\t\t\t\tif (!decision.fire) {\n\t\t\t\t\tthis.log.debug(`automation ${rule.id} (${user.displayName}): ${decision.reason}`);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst action = this.automationAction(rule, block.blockMinutes);\n\t\t\t\tif (\n\t\t\t\t\t!services.automations.recordRun({\n\t\t\t\t\t\truleId: rule.id,\n\t\t\t\t\t\tuserId: user.id,\n\t\t\t\t\t\tperiod,\n\t\t\t\t\t\taction,\n\t\t\t\t\t\tnow: nowUtc,\n\t\t\t\t\t})\n\t\t\t\t) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\ttry {\n\t\t\t\t\tif (rule.kind === \"clockOut\" || rule.kind === \"clockIn\") {\n\t\t\t\t\t\tconst result = punchEmployee(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tentries: services.entries,\n\t\t\t\t\t\t\t\tusers: services.users,\n\t\t\t\t\t\t\t\tsettings: services.settings,\n\t\t\t\t\t\t\t\taggregation: services.aggregation,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{ userId: user.id, note: `auto.${rule.kind}`, now: nowUtc },\n\t\t\t\t\t\t);\n\t\t\t\t\t\tthis.log.info(`automation ${rule.id} for ${user.displayName}: ${result.message}`);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.log.info(`automation ${rule.id} for ${user.displayName}: ${action}`);\n\t\t\t\t\t}\n\t\t\t\t\tacted = true;\n\t\t\t\t\tthis.events?.publish({\n\t\t\t\t\t\ttype: `automation.${rule.kind}`,\n\t\t\t\t\t\tatUtc: nowUtc,\n\t\t\t\t\t\tuserId: user.id,\n\t\t\t\t\t\tdata: { ruleId: rule.id, action, reason: decision.reason },\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// give the next check a chance instead of losing the period\n\t\t\t\t\tservices.automations.forgetRun({ ruleId: rule.id, userId: user.id, period });\n\t\t\t\t\tthis.log.warn(`automation ${rule.id} for ${user.displayName} failed: ${(error as Error).message}`);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (acted) {\n\t\t\t// a punch written by a rule changes the published figures\n\t\t\tthis.scheduleStateRefresh();\n\t\t}\n\t}\n\n\t/**\n\t * Short description of what a rule did (the log entry of `automation_runs`).\n\t *\n\t * @param rule - rule that fired\n\t * @param blockMinutes - length of the running work block, `null` when the employee is not clocked in\n\t * @returns text for the log\n\t */\n\tprivate automationAction(rule: AutomationRuleRecord, blockMinutes: number | null): string {\n\t\tif (rule.kind === \"clockOut\") {\n\t\t\treturn \"clocked out\";\n\t\t}\n\t\tif (rule.kind === \"clockIn\") {\n\t\t\treturn \"clocked in\";\n\t\t}\n\t\tif (rule.kind === \"breakReminder\") {\n\t\t\treturn `reminded after ${blockMinutes ?? 0} min without a break`;\n\t\t}\n\t\treturn \"reported a missing punch\";\n\t}\n\n\t/**\n\t * Republishes the figures shortly after data changed.\n\t *\n\t * A punch from the web app, the terminal or a badge arrives through the API of this very process, so without\n\t * this the states would only change with the five minute timer. One action often produces several events, so\n\t * the refresh is collected for a moment instead of running once per event.\n\t */\n\tprivate scheduleStateRefresh(): void {\n\t\tif (this.stateRefreshTimer) {\n\t\t\treturn;\n\t\t}\n\t\tthis.stateRefreshTimer = this.setTimeout(() => {\n\t\t\tthis.stateRefreshTimer = undefined;\n\t\t\tvoid this.refreshStates();\n\t\t}, 1000);\n\t}\n\n\t/**\n\t * Publishes the newest event of the instance into the state tree.\n\t *\n\t * A notification adapter, a dashboard or a Blockly script then only has to watch `events.*` instead of\n\t * following the live stream of the web app.\n\t *\n\t * @param event - event published by the API\n\t */\n\tprivate async publishEventState(event: ApiEvent): Promise<void> {\n\t\tconst services = this.services;\n\t\tif (!services) {\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tconst data = event.data ?? {};\n\t\t\tconst user =\n\t\t\t\tevent.userId === null || event.userId === undefined ? null : services.users.findById(event.userId);\n\t\t\tawait publishEventSnapshot(this, {\n\t\t\t\ttype: event.type,\n\t\t\t\tatUtc: event.atUtc,\n\t\t\t\tuserName: user?.displayName ?? \"\",\n\t\t\t\tdirection: typeof data.direction === \"string\" ? data.direction : \"\",\n\t\t\t\tsource: typeof data.source === \"string\" ? data.source : \"\",\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tthis.log.debug(`event state could not be published: ${(error as Error).message}`);\n\t\t}\n\t}\n\n\t/**\n\t * Is called when another adapter sends a message (`sendTo`).\n\t *\n\t * @param obj - message object of the adapter framework\n\t */\n\tprivate onMessage(obj: ioBroker.Message): void {\n\t\tvoid (async (): Promise<void> => {\n\t\t\tconst services = this.services;\n\t\t\tif (!services) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst answer = await handleMessage(\n\t\t\t\t\t{\n\t\t\t\t\t\tusers: services.users,\n\t\t\t\t\t\tentries: services.entries,\n\t\t\t\t\t\tabsences: services.absences,\n\t\t\t\t\t\tsettings: services.settings,\n\t\t\t\t\t\taggregation: services.aggregation,\n\t\t\t\t\t\tsync: services.sync,\n\t\t\t\t\t\tbackup: services.backup,\n\t\t\t\t\t\tversion: this.version,\n\t\t\t\t\t},\n\t\t\t\t\tobj.command,\n\t\t\t\t\tobj.message,\n\t\t\t\t);\n\t\t\t\tthis.log.info(`message ${obj.command}: ${answer.message}`);\n\t\t\t\tif (obj.callback) {\n\t\t\t\t\tthis.sendTo(obj.from, obj.command, answer, obj.callback);\n\t\t\t\t}\n\t\t\t\tawait this.refreshStates();\n\t\t\t} catch (error) {\n\t\t\t\tconst message = (error as Error).message;\n\t\t\t\tthis.log.warn(`message ${obj.command} failed: ${message}`);\n\t\t\t\tif (obj.callback) {\n\t\t\t\t\tthis.sendTo(obj.from, obj.command, { ok: false, message }, obj.callback);\n\t\t\t\t}\n\t\t\t}\n\t\t})();\n\t}\n\n\t/**\n\t * Is called if a subscribed state changes\n\t *\n\t * @param id - State ID\n\t * @param state - State object\n\t */\n\tprivate onStateChange(id: string, state: ioBroker.State | null | undefined): void {\n\t\ttry {\n\t\t\tif (!state) {\n\t\t\t\t// deletions carry no value\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// a watched state of another adapter is handled first: such a state is acknowledged by its own\n\t\t\t// adapter, and what counts there is the change of the value\n\t\t\tif (this.handleTriggerState(id, state)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (state.ack) {\n\t\t\t\t// Ignore acknowledged (status) states of this instance\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst prefix = `${this.namespace}.`;\n\t\t\tconst localId = id.startsWith(prefix) ? id.slice(prefix.length) : id;\n\t\t\tif (localId.startsWith(\"commands.\")) {\n\t\t\t\tvoid this.runCommand(localId, state.val);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (parsePresenceStateId(localId) !== null) {\n\t\t\t\tvoid this.runPresence(localId, state.val);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tthis.log.debug(`Ignored state change: ${localId} = ${state.val}`);\n\t\t} catch (error) {\n\t\t\tthis.log.error(`Error in onStateChange for ${id}: ${(error as Error).message}`);\n\t\t}\n\t}\n}\n\nif (require.main !== module) {\n\t// Export the constructor in compact mode\n\tmodule.exports = (options: Partial<utils.AdapterOptions> | undefined) => new TimeTracker(options);\n} else {\n\t// otherwise start the instance directly\n\t(() => new TimeTracker())();\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["import_triggers"]
|
|
7
|
+
}
|
package/io-package.json
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
{
|
|
2
|
+
"common": {
|
|
3
|
+
"name": "time-tracker",
|
|
4
|
+
"version": "0.2.0",
|
|
5
|
+
"nogit": true,
|
|
6
|
+
"news": {
|
|
7
|
+
"0.2.0": {
|
|
8
|
+
"en": "renamed to `ioBroker.time-tracker`: ioBroker requires English adapter names, so the adapter, its npm package and the repository are called `time-tracker` from now on. The instance becomes `time-tracker.0` and creates the database `time-tracker.sqlite`; existing data keeps working by pointing “Database file” at the old `zeiterfassung.sqlite`. German UI texts stay German, `common.titleLang` keeps all 11 languages",
|
|
9
|
+
"de": "umbenannt in `ioBroker.time-tracker`: ioBroker verlangt englische Adapternamen, deshalb heißen Adapter, npm-Paket und Repository ab jetzt `time-tracker`. Die Instanz wird `time-tracker.0` und legt die Datenbank `time-tracker.sqlite` an; vorhandene Daten bleiben nutzbar, indem „Datenbankdatei“ auf die alte `zeiterfassung.sqlite` zeigt. Deutsche Oberflächentexte bleiben deutsch, `common.titleLang` behält alle 11 Sprachen",
|
|
10
|
+
"ru": "переименован в `ioBroker.time-tracker`: ioBroker требует английских имён адаптеров, поэтому адаптер, пакет npm и репозиторий теперь называются `time-tracker`. Экземпляр становится `time-tracker.0` и создаёт базу данных `time-tracker.sqlite`; существующие данные продолжают работать, если указать в «Файл базы данных» старый `zeiterfassung.sqlite`. Немецкие тексты интерфейса остаются немецкими, `common.titleLang` сохраняет все 11 языков",
|
|
11
|
+
"pt": "renomeado para `ioBroker.time-tracker`: o ioBroker exige nomes de adaptador em inglês, por isso o adaptador, o pacote npm e o repositório passam a chamar-se `time-tracker`. A instância torna-se `time-tracker.0` e cria a base de dados `time-tracker.sqlite`; os dados existentes continuam a funcionar indicando em «Ficheiro de base de dados» o antigo `zeiterfassung.sqlite`. Os textos alemães da interface mantêm-se em alemão, `common.titleLang` mantém os 11 idiomas",
|
|
12
|
+
"nl": "hernoemd naar `ioBroker.time-tracker`: ioBroker vereist Engelse adaptornamen, daarom heten adapter, npm-pakket en repository voortaan `time-tracker`. De instantie wordt `time-tracker.0` en maakt de database `time-tracker.sqlite` aan; bestaande gegevens blijven werken door bij «Databasebestand» naar het oude `zeiterfassung.sqlite` te wijzen. Duitse interfaceteksten blijven Duits, `common.titleLang` behoudt alle 11 talen",
|
|
13
|
+
"fr": "renommé en `ioBroker.time-tracker` : ioBroker exige des noms d'adaptateur en anglais, l'adaptateur, le paquet npm et le dépôt s'appellent donc désormais `time-tracker`. L'instance devient `time-tracker.0` et crée la base de données `time-tracker.sqlite` ; les données existantes restent utilisables en indiquant l'ancien `zeiterfassung.sqlite` dans « Fichier de base de données ». Les textes allemands de l'interface restent en allemand, `common.titleLang` conserve les 11 langues",
|
|
14
|
+
"it": "rinominato in `ioBroker.time-tracker`: ioBroker richiede nomi di adattatore in inglese, quindi adattatore, pacchetto npm e repository ora si chiamano `time-tracker`. L'istanza diventa `time-tracker.0` e crea il database `time-tracker.sqlite`; i dati esistenti continuano a funzionare indicando in «File del database» il vecchio `zeiterfassung.sqlite`. I testi tedeschi dell'interfaccia restano in tedesco, `common.titleLang` mantiene tutte le 11 lingue",
|
|
15
|
+
"es": "renombrado a `ioBroker.time-tracker`: ioBroker exige nombres de adaptador en inglés, así que el adaptador, el paquete npm y el repositorio ahora se llaman `time-tracker`. La instancia pasa a ser `time-tracker.0` y crea la base de datos `time-tracker.sqlite`; los datos existentes siguen funcionando si en «Archivo de base de datos» se indica el antiguo `zeiterfassung.sqlite`. Los textos alemanes de la interfaz siguen en alemán, `common.titleLang` conserva los 11 idiomas",
|
|
16
|
+
"pl": "zmieniono nazwę na `ioBroker.time-tracker`: ioBroker wymaga angielskich nazw adapterów, dlatego adapter, pakiet npm i repozytorium nazywają się teraz `time-tracker`. Instancja staje się `time-tracker.0` i tworzy bazę danych `time-tracker.sqlite`; istniejące dane nadal działają, jeśli w „Plik bazy danych” wskażesz stary `zeiterfassung.sqlite`. Niemieckie teksty interfejsu pozostają niemieckie, `common.titleLang` zachowuje wszystkie 11 języków",
|
|
17
|
+
"uk": "перейменовано на `ioBroker.time-tracker`: ioBroker вимагає англійських назв адаптерів, тому адаптер, пакет npm і репозиторій тепер називаються `time-tracker`. Екземпляр стає `time-tracker.0` і створює базу даних `time-tracker.sqlite`; наявні дані й далі працюють, якщо в «Файл бази даних» вказати старий `zeiterfassung.sqlite`. Німецькі тексти інтерфейсу залишаються німецькими, `common.titleLang` зберігає всі 11 мов",
|
|
18
|
+
"zh-cn": "已重命名为 `ioBroker.time-tracker`:ioBroker 要求适配器名称使用英文,因此适配器、npm 包和仓库现在都叫 `time-tracker`。实例变为 `time-tracker.0`,并创建数据库 `time-tracker.sqlite`;只要在“数据库文件”中指向旧的 `zeiterfassung.sqlite`,现有数据仍然可用。德语界面文本仍为德语,`common.titleLang` 保留全部 11 种语言"
|
|
19
|
+
},
|
|
20
|
+
"0.1.9": {
|
|
21
|
+
"en": "two findings of the ioBroker repository checker are fixed: `common.title` is gone again (it is deprecated — `common.titleLang` replaces it, E1084) and the `common.news` entry of 0.1.7 is removed, because that version never reached npm (E2004, its pipeline was red). `npm run version:check` watches both from now on: it refuses `common.title` and asks npm whether every version of the news list is really published",
|
|
22
|
+
"de": "zwei Befunde des ioBroker-Repocheckers sind behoben: `common.title` ist wieder entfernt (veraltet — `common.titleLang` ersetzt es, E1084) und der `common.news`-Eintrag von 0.1.7 ist weg, weil diese Version nie auf npm gelandet ist (E2004, ihre Pipeline war rot). `npm run version:check` prüft beides ab jetzt: es lehnt `common.title` ab und fragt npm, ob jede Version der News-Liste wirklich veröffentlicht ist",
|
|
23
|
+
"ru": "исправлены два замечания проверки репозитория ioBroker: `common.title` снова удалён (он устарел — его заменяет `common.titleLang`, E1084), а запись `common.news` для 0.1.7 убрана, потому что эта версия так и не попала на npm (E2004, её конвейер был красным). Теперь `npm run version:check` следит за обоими: отклоняет `common.title` и спрашивает npm, действительно ли опубликована каждая версия из списка новостей",
|
|
24
|
+
"pt": "dois achados da verificação do repositório ioBroker estão corrigidos: `common.title` voltou a ser removido (está obsoleto — `common.titleLang` substitui-o, E1084) e a entrada `common.news` da 0.1.7 desapareceu, porque essa versão nunca chegou ao npm (E2004, a sua pipeline estava vermelha). `npm run version:check` vigia ambos a partir de agora: recusa `common.title` e pergunta ao npm se cada versão da lista de novidades está realmente publicada",
|
|
25
|
+
"nl": "twee bevindingen van de ioBroker-repositorychecker zijn opgelost: `common.title` is weer weg (het is verouderd — `common.titleLang` vervangt het, E1084) en de `common.news`-vermelding van 0.1.7 is verwijderd, omdat die versie nooit op npm is gekomen (E2004, de pipeline was rood). `npm run version:check` let vanaf nu op beide: het weigert `common.title` en vraagt npm of elke versie uit de nieuwslijst echt gepubliceerd is",
|
|
26
|
+
"fr": "deux remarques du contrôle du dépôt ioBroker sont corrigées : `common.title` a de nouveau disparu (il est obsolète — `common.titleLang` le remplace, E1084) et l'entrée `common.news` de 0.1.7 est supprimée, car cette version n'est jamais arrivée sur npm (E2004, son pipeline était rouge). `npm run version:check` surveille désormais les deux : il refuse `common.title` et demande à npm si chaque version de la liste des nouveautés est réellement publiée",
|
|
27
|
+
"it": "due rilievi del controllo del repository ioBroker sono risolti: `common.title` è di nuovo rimosso (è obsoleto — `common.titleLang` lo sostituisce, E1084) e la voce `common.news` della 0.1.7 è sparita, perché quella versione non è mai arrivata su npm (E2004, la pipeline era rossa). `npm run version:check` ora controlla entrambe: rifiuta `common.title` e chiede a npm se ogni versione dell'elenco novità è davvero pubblicata",
|
|
28
|
+
"es": "dos hallazgos de la comprobación del repositorio ioBroker están corregidos: `common.title` ha vuelto a desaparecer (está obsoleto — `common.titleLang` lo sustituye, E1084) y la entrada `common.news` de 0.1.7 se ha quitado, porque esa versión nunca llegó a npm (E2004, su pipeline estaba en rojo). `npm run version:check` vigila ambas desde ahora: rechaza `common.title` y pregunta a npm si cada versión de la lista de novedades está publicada de verdad",
|
|
29
|
+
"pl": "dwa ustalenia kontroli repozytorium ioBroker są naprawione: `common.title` zniknął ponownie (jest przestarzały — zastępuje go `common.titleLang`, E1084), a wpis `common.news` dla 0.1.7 usunięto, bo ta wersja nigdy nie trafiła na npm (E2004, jej pipeline był czerwony). `npm run version:check` pilnuje teraz obu rzeczy: odrzuca `common.title` i pyta npm, czy każda wersja z listy nowości jest naprawdę opublikowana",
|
|
30
|
+
"uk": "виправлено два зауваження перевірки репозиторію ioBroker: `common.title` знову прибрано (він застарілий — його замінює `common.titleLang`, E1084), а запис `common.news` для 0.1.7 видалено, бо ця версія так і не потрапила на npm (E2004, її конвеєр був червоним). Тепер `npm run version:check` стежить за обома: відхиляє `common.title` і питає npm, чи справді опубліковано кожну версію зі списку новин",
|
|
31
|
+
"zh-cn": "已修复 ioBroker 仓库检查的两处问题:`common.title` 再次移除(它已废弃 —— 由 `common.titleLang` 取代,E1084),0.1.7 的 `common.news` 条目也已删除,因为该版本从未发布到 npm(E2004,其流水线为红色)。`npm run version:check` 现在同时检查这两点:拒绝 `common.title`,并向 npm 查询新闻列表中的每个版本是否确实已发布"
|
|
32
|
+
},
|
|
33
|
+
"0.1.8": {
|
|
34
|
+
"en": "the pipeline of 0.1.7 stopped in the ioBroker package test: `common.license` was added next to the existing `common.licenseInformation` in `io-package.json`, and the test refuses both together — the field is removed again and `common.title` (“Time tracking”) stays. `npm run test:package` is part of the release checklist now, and `docs/entwicklung.md` records the rule",
|
|
35
|
+
"de": "die Pipeline von 0.1.7 blieb im ioBroker-Pakettest stehen: `common.license` war neben dem vorhandenen `common.licenseInformation` in `io-package.json` gesetzt, und der Test lehnt beides zusammen ab — das Feld ist wieder entfernt, `common.title` („Time tracking“) bleibt. `npm run test:package` gehört jetzt zur Prüfliste vor jedem Release, und `docs/entwicklung.md` hält die Regel fest",
|
|
36
|
+
"ru": "конвейер 0.1.7 остановился на тесте пакета ioBroker: в `io-package.json` рядом с существующим `common.licenseInformation` было добавлено `common.license`, а тест не допускает их вместе — поле снова удалено, `common.title` («Time tracking») остаётся. Теперь `npm run test:package` входит в список проверок перед каждым релизом, а правило записано в `docs/entwicklung.md`",
|
|
37
|
+
"pt": "a pipeline da 0.1.7 parou no teste do pacote ioBroker: `common.license` foi adicionado ao lado do `common.licenseInformation` existente em `io-package.json` e o teste recusa ambos juntos — o campo foi removido de novo e `common.title` («Time tracking») fica. `npm run test:package` passou a fazer parte da lista de verificações antes de cada publicação, e `docs/entwicklung.md` registra a regra",
|
|
38
|
+
"nl": "de pipeline van 0.1.7 bleef steken in de ioBroker-pakkettest: `common.license` stond naast het bestaande `common.licenseInformation` in `io-package.json` en de test weigert beide samen — het veld is weer verwijderd en `common.title` (“Time tracking”) blijft. `npm run test:package` hoort nu bij de controlelijst voor elke release, en `docs/entwicklung.md` legt de regel vast",
|
|
39
|
+
"fr": "le pipeline de 0.1.7 s'est arrêté dans le test de paquet ioBroker : `common.license` avait été ajouté à côté du `common.licenseInformation` existant dans `io-package.json` et le test refuse les deux ensemble — le champ est retiré et `common.title` (« Time tracking ») reste. `npm run test:package` fait désormais partie de la liste de contrôle avant chaque publication, et `docs/entwicklung.md` consigne la règle",
|
|
40
|
+
"it": "la pipeline della 0.1.7 si è fermata nel test del pacchetto ioBroker: `common.license` era stato aggiunto accanto al `common.licenseInformation` esistente in `io-package.json` e il test rifiuta entrambi insieme — il campo è di nuovo rimosso e `common.title` («Time tracking») resta. `npm run test:package` ora fa parte della lista di controllo prima di ogni pubblicazione, e `docs/entwicklung.md` registra la regola",
|
|
41
|
+
"es": "la pipeline de 0.1.7 se detuvo en la prueba del paquete ioBroker: `common.license` se había añadido junto al `common.licenseInformation` existente en `io-package.json` y la prueba rechaza ambos juntos — el campo se ha quitado de nuevo y `common.title` («Time tracking») se mantiene. `npm run test:package` forma parte ahora de la lista de comprobación antes de cada publicación, y `docs/entwicklung.md` recoge la regla",
|
|
42
|
+
"pl": "pipeline 0.1.7 zatrzymał się na teście pakietu ioBroker: `common.license` został dodany obok istniejącego `common.licenseInformation` w `io-package.json`, a test odrzuca oba razem — pole zostało ponownie usunięte, a `common.title` („Time tracking”) zostaje. `npm run test:package` jest teraz częścią listy kontrolnej przed każdą publikacją, a `docs/entwicklung.md` opisuje tę zasadę",
|
|
43
|
+
"uk": "конвеєр 0.1.7 зупинився на тесті пакета ioBroker: у `io-package.json` поряд із наявним `common.licenseInformation` було додано `common.license`, а тест не допускає їх разом — поле знову прибрано, `common.title` («Time tracking») залишається. `npm run test:package` тепер входить до списку перевірок перед кожним релізом, а правило зафіксовано в `docs/entwicklung.md`",
|
|
44
|
+
"zh-cn": "0.1.7 的流水线在 ioBroker 包测试中停止:`io-package.json` 中在已有的 `common.licenseInformation` 旁又加了 `common.license`,而测试不允许两者并存 —— 该字段已再次移除,`common.title`(「Time tracking」)保留。现在每次发布前的检查清单包含 `npm run test:package`,规则记录在 `docs/entwicklung.md`"
|
|
45
|
+
},
|
|
46
|
+
"0.1.6": {
|
|
47
|
+
"en": "the automatic rules know a fourth kind: **“Clock in automatically”** punches in an employee who is still missing at the configured time (and keeps quiet when somebody is clocked in already). The seeded **holidays** are shown in the language of the display — each carries a stable key (`newYear`, `goodFriday`, …) and a day added by hand keeps its own name. In the administration the block **“All instance settings”** is much shorter (three fields per line, the time zone on a line of its own) and every field shows a readable label in the language of the display with the technical name small below it; the two “first start” hints are easier to read as well",
|
|
48
|
+
"de": "die Automatik-Regeln kennen eine vierte Art: **„Automatisch anstempeln“** stempelt einen Mitarbeiter ein, der zur eingestellten Zeit noch fehlt (und schweigt, wenn bereits jemand eingestempelt ist). Die gesäten **Feiertage** stehen in der Sprache der Anzeige — jeder trägt einen stabilen Schlüssel (`newYear`, `goodFriday`, …), und ein von Hand ergänzter Tag behält seinen eigenen Namen. In der Verwaltung ist der Block **„Alle Instanzeinstellungen“** deutlich kürzer (drei Felder pro Zeile, die Zeitzone auf einer eigenen Zeile) und jedes Feld zeigt eine lesbare Beschriftung in der Sprache der Anzeige mit dem technischen Namen klein darunter; auch die beiden Hinweise zum Erststart sind besser lesbar",
|
|
49
|
+
"ru": "у правил автоматики появился четвёртый вид: **«Автоматически отметить приход»** отмечает приход сотрудника, которого в заданное время ещё нет (и молчит, если кто-то уже отметился). Посеянные **праздники** показываются на языке интерфейса — у каждого есть стабильный ключ (`newYear`, `goodFriday`, …), а день, добавленный вручную, сохраняет своё название. В администрации блок **«Все настройки инстанса»** стал заметно короче (три поля в строке, часовой пояс — отдельной строкой), и каждое поле показывает понятную подпись на языке интерфейса, а техническое имя — мелко под ней; две подсказки о первом запуске тоже читаются легче",
|
|
50
|
+
"pt": "as regras automáticas conhecem um quarto tipo: **«Marcar entrada automaticamente»** marca a entrada de um funcionário que ainda falta à hora configurada (e cala-se quando alguém já está marcado). Os **feriados** semeados aparecem no idioma do ecrã — cada um leva uma chave estável (`newYear`, `goodFriday`, …) e um dia adicionado à mão mantém o seu nome. Na administração, o bloco **«Todas as definições da instância»** é muito mais curto (três campos por linha, o fuso horário numa linha própria) e cada campo mostra uma etiqueta legível no idioma do ecrã com o nome técnico pequeno por baixo; as duas dicas do primeiro arranque também se leem melhor",
|
|
51
|
+
"nl": "de automatische regels kennen een vierde soort: **“Automatisch inklokken”** klokt een medewerker in die op het ingestelde tijdstip nog ontbreekt (en zwijgt als er al iemand is ingeklokt). De gezaaide **feestdagen** staan in de taal van de weergave — elk heeft een stabiele sleutel (`newYear`, `goodFriday`, …) en een handmatig toegevoegde dag houdt zijn eigen naam. In de administratie is het blok **“Alle instantie-instellingen”** veel korter (drie velden per regel, de tijdzone op een eigen regel) en elk veld toont een leesbaar label in de taal van de weergave met de technische naam klein eronder; ook de twee hints over de eerste start lezen prettiger",
|
|
52
|
+
"fr": "les règles automatiques connaissent un quatrième type : **« Pointer l'arrivée automatiquement »** pointe l'arrivée d'un employé encore absent à l'heure configurée (et reste silencieux si quelqu'un a déjà pointé). Les **jours fériés** semés s'affichent dans la langue de l'interface — chacun porte une clé stable (`newYear`, `goodFriday`, …) et un jour ajouté à la main garde son propre nom. Dans l'administration, le bloc **« Tous les réglages de l'instance »** est bien plus court (trois champs par ligne, le fuseau horaire sur une ligne à lui) et chaque champ montre un libellé lisible dans la langue de l'interface avec le nom technique en petit dessous ; les deux indications du premier démarrage se lisent mieux aussi",
|
|
53
|
+
"it": "le regole automatiche conoscono un quarto tipo: **«Timbra l'entrata automaticamente»** timbra l'entrata di un dipendente ancora assente all'ora configurata (e tace se qualcuno ha già timbrato). Le **festività** generate sono mostrate nella lingua dell'interfaccia — ognuna porta una chiave stabile (`newYear`, `goodFriday`, …) e un giorno aggiunto a mano mantiene il proprio nome. Nell'amministrazione il blocco **«Tutte le impostazioni dell'istanza»** è molto più corto (tre campi per riga, il fuso orario su una riga propria) e ogni campo mostra un'etichetta leggibile nella lingua dell'interfaccia con il nome tecnico in piccolo sotto; anche le due indicazioni del primo avvio si leggono meglio",
|
|
54
|
+
"es": "las reglas automáticas conocen un cuarto tipo: **«Fichar la entrada automáticamente»** ficha la entrada de un empleado que aún falta a la hora configurada (y calla si alguien ya ha fichado). Los **festivos** sembrados se muestran en el idioma de la pantalla — cada uno lleva una clave estable (`newYear`, `goodFriday`, …) y un día añadido a mano conserva su nombre. En la administración, el bloque **«Todos los ajustes de la instancia»** es mucho más corto (tres campos por línea, la zona horaria en su propia línea) y cada campo muestra una etiqueta legible en el idioma de la pantalla con el nombre técnico pequeño debajo; las dos indicaciones del primer arranque también se leen mejor",
|
|
55
|
+
"pl": "reguły automatyczne znają czwarty rodzaj: **„Automatycznie zastempluj wejście”** stempluje wejście pracownika, którego o ustawionej godzinie jeszcze nie ma (i milczy, gdy ktoś już jest zastemplowany). Zasiane **święta** są pokazywane w języku interfejsu — każde ma stabilny klucz (`newYear`, `goodFriday`, …), a dzień dodany ręcznie zachowuje własną nazwę. W administracji blok **„Wszystkie ustawienia instancji”** jest znacznie krótszy (trzy pola w wierszu, strefa czasowa w osobnym wierszu), a każde pole pokazuje czytelną etykietę w języku interfejsu, z techniczną nazwą drobnym drukiem pod spodem; oba komunikaty o pierwszym starcie też czyta się lepiej",
|
|
56
|
+
"uk": "правила автоматики знають четвертий вид: **«Автоматично відмітити прихід»** відмічає прихід працівника, якого у вказаний час ще немає (і мовчить, якщо хтось уже відмітився). Засіяні **свята** показуються мовою інтерфейсу — кожне має стабільний ключ (`newYear`, `goodFriday`, …), а день, доданий вручну, зберігає власну назву. В адміністрації блок **«Усі налаштування інстансу»** став значно коротшим (три поля в рядку, часовий пояс — окремим рядком), і кожне поле показує зрозумілий підпис мовою інтерфейсу, а технічна назва — дрібно під ним; дві підказки про перший запуск теж читаються легше",
|
|
57
|
+
"zh-cn": "自动规则增加了第四种:**「自动上班打卡」** 会在设定时间给尚未打卡的员工打上班卡(若已有人打卡则保持安静)。内置**节假日**按界面语言显示 —— 每个都有稳定键(`newYear`、`goodFriday` 等),手动添加的日期保留自己的名称。管理界面中**「实例全部设置」**区块明显变短(每行三个字段,时区单独一行),每个字段显示按界面语言的可读标签,下方以小字显示技术名称;两条首次启动提示也更易阅读"
|
|
58
|
+
},
|
|
59
|
+
"0.1.5": {
|
|
60
|
+
"en": "the automatic rules know **weekdays** — a company rule for Monday to Friday and one for the weekend can live side by side — and they can act **once a week** instead of once a day, in which case the guard counts the ISO week, so a rule that fires on a Monday fires again on the next Monday; the day names come from `Intl` and are spelled in the language of the display. The badge (RFID/NFC) tab shows the link of a fresh badge as a **QR code** next to the link itself — drawn in the browser, so it works offline and no service sees the link; `docs/i18n.md` also records who settles the translations (German and Russian by the owner, the other nine languages have no native speaker and stay machine translation with the technical pass)",
|
|
61
|
+
"de": "die Automatik-Regeln kennen **Wochentage** — eine Firmenregel für Montag bis Freitag und eine fürs Wochenende stehen nebeneinander — und sie können **einmal pro Woche** statt einmal pro Tag greifen; die Sperre zählt dann die ISO-Woche, sodass eine Montags-Regel am nächsten Montag wieder auslöst; die Tagesnamen kommen aus `Intl` und stehen in der Sprache der Anzeige. Der Ausweis-Tab (RFID/NFC) zeigt den Link eines frischen Ausweises als **QR-Code** neben dem Link selbst — im Browser gezeichnet, funktioniert also offline und kein Dienst sieht den Link; `docs/i18n.md` hält außerdem fest, wer die Übersetzungen entscheidet (Deutsch und Russisch der Eigentümer, für die anderen neun Sprachen gibt es keinen Muttersprachler, sie bleiben maschinenübersetzt mit technischer Prüfung)",
|
|
62
|
+
"ru": "правила автоматики знают **дни недели** — правило компании на понедельник–пятницу и правило на выходные могут соседствовать — и могут срабатывать **раз в неделю** вместо раза в день; тогда защита считает ISO-неделю, поэтому правило на понедельник сработает и в следующий понедельник; названия дней берутся из `Intl` и пишутся на языке интерфейса. Вкладка пропусков (RFID/NFC) показывает ссылку нового пропуска как **QR-код** рядом с самой ссылкой — он рисуется в браузере, поэтому работает офлайн и никакой сервис не видит ссылку; в `docs/i18n.md` также указано, кто решает вопросы переводов (немецкий и русский — владелец, для остальных девяти языков носителя нет, они остаются машинным переводом с технической проверкой)",
|
|
63
|
+
"pt": "as regras automáticas conhecem **dias da semana** — uma regra da empresa para segunda a sexta e outra para o fim de semana podem coexistir — e podem atuar **uma vez por semana** em vez de uma vez por dia; nesse caso a proteção conta a semana ISO, pelo que uma regra de segunda-feira volta a atuar na segunda seguinte; os nomes dos dias vêm do `Intl` e aparecem no idioma do ecrã. O separador dos crachás (RFID/NFC) mostra a ligação de um crachá novo como **código QR** ao lado da própria ligação — desenhado no navegador, por isso funciona offline e nenhum serviço vê a ligação; `docs/i18n.md` regista ainda quem decide as traduções (alemão e russo pelo proprietário, os outros nove idiomas não têm falante nativo e ficam com tradução automática e revisão técnica)",
|
|
64
|
+
"nl": "de automatische regels kennen **dagen van de week** — een bedrijfsregel voor maandag tot vrijdag en een voor het weekend kunnen naast elkaar bestaan — en ze kunnen **één keer per week** in plaats van één keer per dag werken; de bewaking telt dan de ISO-week, zodat een regel voor maandag de volgende maandag weer afgaat; de dagnamen komen uit `Intl` en staan in de taal van de weergave. Het tabblad badges (RFID/NFC) toont de link van een nieuwe badge als **QR-code** naast de link zelf — in de browser getekend, dus werkt offline en geen enkele dienst ziet de link; `docs/i18n.md` legt ook vast wie de vertalingen beslist (Duits en Russisch door de eigenaar, voor de andere negen talen is er geen native speaker, ze blijven machinaal vertaald met technische controle)",
|
|
65
|
+
"fr": "les règles automatiques connaissent les **jours de la semaine** — une règle d'entreprise du lundi au vendredi et une pour le week-end peuvent coexister — et peuvent agir **une fois par semaine** au lieu d'une fois par jour ; la garde compte alors la semaine ISO, si bien qu'une règle du lundi se déclenche à nouveau le lundi suivant ; les noms des jours viennent d'`Intl` et s'affichent dans la langue de l'interface. L'onglet des badges (RFID/NFC) montre le lien d'un nouveau badge sous forme de **code QR** à côté du lien lui-même — dessiné dans le navigateur, il fonctionne donc hors ligne et aucun service ne voit le lien ; `docs/i18n.md` indique aussi qui décide des traductions (l'allemand et le russe par le propriétaire, les neuf autres langues n'ont pas de locuteur natif et restent une traduction automatique avec une relecture technique)",
|
|
66
|
+
"it": "le regole automatiche conoscono i **giorni della settimana** — una regola aziendale dal lunedì al venerdì e una per il fine settimana possono convivere — e possono agire **una volta a settimana** invece di una volta al giorno; in quel caso la protezione conta la settimana ISO, quindi una regola del lunedì scatta di nuovo il lunedì successivo; i nomi dei giorni vengono da `Intl` e sono nella lingua dell'interfaccia. La scheda dei badge (RFID/NFC) mostra il link di un badge nuovo come **codice QR** accanto al link stesso — disegnato nel browser, quindi funziona offline e nessun servizio vede il link; `docs/i18n.md` registra anche chi decide le traduzioni (tedesco e russo dal proprietario, per le altre nove lingue non c'è un madrelingua e restano traduzione automatica con revisione tecnica)",
|
|
67
|
+
"es": "las reglas automáticas conocen los **días de la semana** — una regla de empresa de lunes a viernes y otra para el fin de semana pueden convivir — y pueden actuar **una vez por semana** en lugar de una vez al día; entonces la protección cuenta la semana ISO, así que una regla del lunes vuelve a dispararse el lunes siguiente; los nombres de los días vienen de `Intl` y aparecen en el idioma de la pantalla. La pestaña de credenciales (RFID/NFC) muestra el enlace de una credencial nueva como **código QR** junto al propio enlace — dibujado en el navegador, así que funciona sin conexión y ningún servicio ve el enlace; `docs/i18n.md` también recoge quién decide las traducciones (alemán y ruso el propietario, los otros nueve idiomas no tienen hablante nativo y siguen siendo traducción automática con revisión técnica)",
|
|
68
|
+
"pl": "reguły automatyczne znają **dni tygodnia** — reguła firmowa od poniedziałku do piątku i reguła na weekend mogą współistnieć — i mogą działać **raz w tygodniu** zamiast raz dziennie; wtedy zabezpieczenie liczy tydzień ISO, więc reguła na poniedziałek zadziała ponownie w kolejny poniedziałek; nazwy dni pochodzą z `Intl` i są w języku interfejsu. Zakładka identyfikatorów (RFID/NFC) pokazuje link nowego identyfikatora jako **kod QR** obok samego linku — rysowany w przeglądarce, więc działa offline i żadna usługa nie widzi linku; `docs/i18n.md` opisuje też, kto rozstrzyga tłumaczenia (niemiecki i rosyjski właściciel, dla pozostałych dziewięciu języków nie ma native speakera i pozostają tłumaczeniem maszynowym z kontrolą techniczną)",
|
|
69
|
+
"uk": "правила автоматики знають **дні тижня** — правило компанії з понеділка по п'ятницю й правило на вихідні можуть співіснувати — і можуть спрацьовувати **раз на тиждень** замість разу на день; тоді захист рахує ISO-тиждень, тож правило на понеділок спрацює й наступного понеділка; назви днів беруться з `Intl` і подаються мовою інтерфейсу. Вкладка перепусток (RFID/NFC) показує посилання нової перепустки як **QR-код** поряд із самим посиланням — він малюється у браузері, тож працює офлайн і жодна служба не бачить посилання; `docs/i18n.md` також фіксує, хто вирішує питання перекладів (німецька й російська — власник, для решти дев'яти мов носія немає, вони залишаються машинним перекладом із технічною перевіркою)",
|
|
70
|
+
"zh-cn": "自动规则支持**星期** —— 工作日规则和周末规则可以并存 —— 也可以设为**每周一次**而非每天一次;此时按 ISO 周计数,因此周一的规则在下周一仍会触发;星期名称取自 `Intl`,按界面语言显示。证件(RFID/NFC)标签页会在链接旁显示新证件的**二维码** —— 在浏览器中绘制,因此可离线使用,且没有任何服务能看到该链接;`docs/i18n.md` 还记录了由谁决定翻译(德语和俄语由所有者负责,其余九种语言没有母语者,仍为机器翻译加技术检查)"
|
|
71
|
+
},
|
|
72
|
+
"0.1.4": {
|
|
73
|
+
"en": "badges (RFID/NFC) can be edited — employee, label and validity — and get a new link when they are lost, expired or revoked: a new employee or a new validity signs the link again, so the link handed out before stops working immediately, while a label on its own leaves it alone, and a revoked badge works again with the new link; a second technical pass over the eleven languages corrected the hint about the appearance (a sentence was missing in nine languages), the colour hint (seven languages) and a Russian sentence in the Ukrainian file",
|
|
74
|
+
"de": "Ausweise (RFID/NFC) lassen sich bearbeiten — Mitarbeiter, Bezeichnung und Gültigkeit — und bekommen einen neuen Link, wenn sie verloren, abgelaufen oder widerrufen sind: ein anderer Mitarbeiter oder eine neue Gültigkeit signiert den Link neu, der vorher ausgegebene funktioniert also sofort nicht mehr, eine reine Bezeichnungsänderung lässt ihn in Ruhe, und ein widerrufener Ausweis arbeitet mit dem neuen Link wieder; ein zweiter technischer Durchgang über die elf Sprachen hat den Hinweis zum Aussehen (in neun Sprachen fehlte ein Satz), den Farbhinweis (sieben Sprachen) und einen russischen Satz in der ukrainischen Datei korrigiert",
|
|
75
|
+
"ru": "пропуска (RFID/NFC) можно редактировать — сотрудник, метка и срок действия — и выдавать им новую ссылку, если они потеряны, просрочены или отозваны: смена сотрудника или срока действия подписывает ссылку заново, поэтому выданная ранее сразу перестаёт работать, а изменение только метки её не затрагивает; отозванный пропуск с новой ссылкой снова работает; второй технический проход по одиннадцати языкам исправил подсказку о внешнем виде (в девяти языках отсутствовало предложение), подсказку о цвете (семь языков) и русскую фразу в украинском файле",
|
|
76
|
+
"pt": "os crachás (RFID/NFC) podem ser editados — funcionário, etiqueta e validade — e recebem uma nova ligação quando estão perdidos, expirados ou revogados: mudar o funcionário ou a validade assina de novo a ligação, por isso a anterior deixa de funcionar de imediato, enquanto uma alteração apenas da etiqueta não a afeta, e um crachá revogado volta a funcionar com a nova ligação; uma segunda passagem técnica pelas onze línguas corrigiu a dica do aspeto (faltava uma frase em nove línguas), a dica da cor (sete línguas) e uma frase russa no ficheiro ucraniano",
|
|
77
|
+
"nl": "badges (RFID/NFC) zijn te bewerken — medewerker, label en geldigheid — en krijgen een nieuwe link als ze verloren, verlopen of ingetrokken zijn: een andere medewerker of een nieuwe geldigheid ondertekent de link opnieuw, de eerder uitgegeven link werkt dus onmiddellijk niet meer, een wijziging van alleen het label laat hem ongemoeid, en een ingetrokken badge werkt met de nieuwe link weer; een tweede technische ronde langs de elf talen corrigeerde de hint over het uiterlijk (in negen talen ontbrak een zin), de kleurhint (zeven talen) en een Russische zin in het Oekraïense bestand",
|
|
78
|
+
"fr": "les badges (RFID/NFC) peuvent être modifiés — employé, libellé et validité — et reçoivent un nouveau lien lorsqu'ils sont perdus, expirés ou révoqués : un autre employé ou une nouvelle validité signe à nouveau le lien, le lien précédent cesse donc immédiatement de fonctionner, alors qu'une modification du seul libellé le laisse intact, et un badge révoqué fonctionne de nouveau avec le nouveau lien ; une deuxième passe technique sur les onze langues a corrigé l'indication sur l'apparence (une phrase manquait dans neuf langues), l'indication sur la couleur (sept langues) et une phrase russe dans le fichier ukrainien",
|
|
79
|
+
"it": "i badge (RFID/NFC) si possono modificare — dipendente, etichetta e validità — e ricevono un nuovo link quando sono persi, scaduti o revocati: un altro dipendente o una nuova validità firma di nuovo il link, quindi il link precedente smette subito di funzionare, mentre una modifica della sola etichetta non lo tocca, e un badge revocato torna a funzionare con il nuovo link; una seconda passata tecnica sulle undici lingue ha corretto il suggerimento sull'aspetto (in nove lingue mancava una frase), quello sul colore (sette lingue) e una frase russa nel file ucraino",
|
|
80
|
+
"es": "las credenciales (RFID/NFC) se pueden editar — empleado, etiqueta y validez — y reciben un enlace nuevo cuando están perdidas, caducadas o revocadas: otro empleado o una nueva validez vuelve a firmar el enlace, por lo que el anterior deja de funcionar de inmediato, mientras que cambiar solo la etiqueta no lo afecta, y una credencial revocada vuelve a funcionar con el enlace nuevo; una segunda pasada técnica por los once idiomas corrigió la indicación sobre el aspecto (faltaba una frase en nueve idiomas), la del color (siete idiomas) y una frase rusa en el archivo ucraniano",
|
|
81
|
+
"pl": "identyfikatory (RFID/NFC) można edytować — pracownik, etykieta i ważność — oraz wydać im nowy link, gdy są zgubione, wygasłe lub unieważnione: zmiana pracownika lub ważności podpisuje link od nowa, więc wcześniej wydany natychmiast przestaje działać, a zmiana samej etykiety go nie rusza, i unieważniony identyfikator znów działa z nowym linkiem; druga runda techniczna po jedenastu językach poprawiła wskazówkę o wyglądzie (w dziewięciu językach brakowało zdania), wskazówkę o kolorze (siedem języków) i rosyjskie zdanie w pliku ukraińskim",
|
|
82
|
+
"uk": "перепустки (RFID/NFC) можна редагувати — працівник, мітка та термін дії — і видавати їм нове посилання, якщо їх загублено, прострочено або відкликано: зміна працівника або терміну підписує посилання заново, тому видане раніше одразу перестає працювати, а зміна самої мітки його не чіпає, і відкликана перепустка знову працює з новим посиланням; другий технічний прохід одинадцятьма мовами виправив підказку про вигляд (у дев'яти мовах бракувало речення), підказку про колір (сім мов) і російське речення в українському файлі",
|
|
83
|
+
"zh-cn": "证件(RFID/NFC)可以编辑 —— 员工、标签和有效期 —— 丢失、过期或已撤销时可以重新生成链接:更换员工或有效期会重新签名链接,之前发出的链接立即失效,只改标签则不影响链接,已撤销的证件配上新链接后可再次使用;第二轮技术检查修正了外观提示(九种语言缺少一句)、颜色提示(七种语言)以及乌克兰语文件中的一句俄语"
|
|
84
|
+
},
|
|
85
|
+
"0.1.3": {
|
|
86
|
+
"en": "badges (RFID/NFC) show their state — active, expired or revoked — and when they were last used; the link is built from the address the administration is currently open with (scheme, host and port from the browser), so it works on plain HTTP and behind a reverse proxy alike, and a revoked badge can be removed from the list for good (the audit trail keeps the trace); five wrong overtime labels were corrected (`pt`, `fr`, `it`, `es`, `zh-cn`) and the Polish punch labels now match the badge texts",
|
|
87
|
+
"de": "Ausweise (RFID/NFC) zeigen ihren Zustand — aktiv, abgelaufen oder widerrufen — und wann sie zuletzt benutzt wurden; der Link wird aus der Adresse gebaut, unter der die Verwaltung gerade geöffnet ist (Schema, Host und Port aus dem Browser), funktioniert also auf reinem HTTP wie hinter einem Reverse-Proxy, und ein widerrufener Ausweis lässt sich endgültig aus der Liste entfernen (das Audit-Protokoll behält die Spur); fünf falsche Überstunden-Beschriftungen wurden korrigiert (`pt`, `fr`, `it`, `es`, `zh-cn`) und die polnischen Stempel-Beschriftungen passen jetzt zu den Ausweis-Texten",
|
|
88
|
+
"ru": "пропуска (RFID/NFC) показывают своё состояние — активен, истёк или отозван — и когда они использовались в последний раз; ссылка строится из адреса, по которому администрация открыта сейчас (схема, хост и порт из браузера), поэтому работает и на чистом HTTP, и за обратным прокси, а отозванный пропуск можно окончательно убрать из списка (журнал аудита сохраняет след); исправлены пять неверных подписей сверхурочных (`pt`, `fr`, `it`, `es`, `zh-cn`), а польские подписи отметок теперь совпадают с текстами пропусков",
|
|
89
|
+
"pt": "os crachás (RFID/NFC) mostram o seu estado — ativo, expirado ou revogado — e quando foram usados pela última vez; a ligação é criada a partir do endereço com que a administração está aberta (esquema, host e porta do navegador), por isso funciona em HTTP simples e atrás de um proxy inverso, e um crachá revogado pode ser removido definitivamente da lista (o registo de auditoria mantém o rasto); foram corrigidas cinco legendas erradas de horas extra (`pt`, `fr`, `it`, `es`, `zh-cn`) e as legendas polacas de marcação agora correspondem aos textos dos crachás",
|
|
90
|
+
"nl": "badges (RFID/NFC) tonen hun toestand — actief, verlopen of ingetrokken — en wanneer ze voor het laatst zijn gebruikt; de link wordt opgebouwd uit het adres waarmee de administratie nu openstaat (schema, host en poort uit de browser), werkt dus op gewone HTTP en achter een reverse proxy, en een ingetrokken badge kan definitief uit de lijst worden verwijderd (het auditlog houdt het spoor); vijf foutieve overuren-labels zijn gecorrigeerd (`pt`, `fr`, `it`, `es`, `zh-cn`) en de Poolse stempellabels sluiten nu aan op de badge-teksten",
|
|
91
|
+
"fr": "les badges (RFID/NFC) affichent leur état — actif, expiré ou révoqué — et leur dernier usage ; le lien est construit à partir de l'adresse avec laquelle l'administration est ouverte (schéma, hôte et port depuis le navigateur), il fonctionne donc en HTTP simple comme derrière un proxy inverse, et un badge révoqué peut être supprimé définitivement de la liste (le journal d'audit garde la trace) ; cinq libellés d'heures supplémentaires erronés ont été corrigés (`pt`, `fr`, `it`, `es`, `zh-cn`) et les libellés polonais de pointage correspondent désormais aux textes des badges",
|
|
92
|
+
"it": "i badge (RFID/NFC) mostrano il loro stato — attivo, scaduto o revocato — e quando sono stati usati l'ultima volta; il link viene costruito dall'indirizzo con cui l'amministrazione è aperta (schema, host e porta dal browser), quindi funziona in HTTP semplice e dietro un proxy inverso, e un badge revocato può essere rimosso definitivamente dall'elenco (il registro di audit conserva la traccia); cinque etichette errate degli straordinari sono state corrette (`pt`, `fr`, `it`, `es`, `zh-cn`) e le etichette polacche delle timbrature ora corrispondono ai testi dei badge",
|
|
93
|
+
"es": "las credenciales (RFID/NFC) muestran su estado — activo, caducado o revocado — y cuándo se usaron por última vez; el enlace se construye con la dirección con la que está abierta la administración (esquema, host y puerto desde el navegador), así funciona en HTTP simple y detrás de un proxy inverso, y una credencial revocada puede eliminarse definitivamente de la lista (el registro de auditoría conserva el rastro); se corrigieron cinco etiquetas erróneas de horas extra (`pt`, `fr`, `it`, `es`, `zh-cn`) y las etiquetas polacas de fichaje ahora coinciden con los textos de las credenciales",
|
|
94
|
+
"pl": "identyfikatory (RFID/NFC) pokazują swój stan — aktywny, wygasły lub unieważniony — oraz kiedy były ostatnio użyte; link jest budowany z adresu, pod którym administracja jest teraz otwarta (schemat, host i port z przeglądarki), więc działa zarówno po zwykłym HTTP, jak i za proxy odwrotnym, a unieważniony identyfikator można trwale usunąć z listy (dziennik audytu zachowuje ślad); poprawiono pięć błędnych etykiet nadgodzin (`pt`, `fr`, `it`, `es`, `zh-cn`), a polskie etykiety stemplowania odpowiadają teraz tekstom identyfikatorów",
|
|
95
|
+
"uk": "перепустки (RFID/NFC) показують свій стан — активна, прострочена або відкликана — і коли її востаннє використовували; посилання будується з адреси, за якою адміністрація відкрита зараз (схема, хост і порт із браузера), тож працює і на звичайному HTTP, і за зворотним проксі, а відкликану перепустку можна остаточно прибрати зі списку (журнал аудиту зберігає слід); виправлено п'ять хибних підписів надурочних (`pt`, `fr`, `it`, `es`, `zh-cn`), а польські підписи відміток тепер відповідають текстам перепусток",
|
|
96
|
+
"zh-cn": "证件(RFID/NFC)会显示状态 —— 有效、已过期或已撤销 —— 以及最近使用时间;链接按当前打开管理界面所用的地址生成(协议、主机和端口来自浏览器),因此在纯 HTTP 和反向代理后都能使用,已撤销的证件也可以从列表中永久删除(审计日志保留痕迹);修正了五处错误的加班标签(`pt`、`fr`、`it`、`es`、`zh-cn`),波兰语打点标签也已与证件文本一致"
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"titleLang": {
|
|
100
|
+
"en": "Time tracking",
|
|
101
|
+
"de": "Zeiterfassung",
|
|
102
|
+
"ru": "Учет времени",
|
|
103
|
+
"pt": "Controle de tempo",
|
|
104
|
+
"nl": "Tijdregistratie",
|
|
105
|
+
"fr": "Suivi du temps",
|
|
106
|
+
"it": "Monitoraggio del tempo",
|
|
107
|
+
"es": "Seguimiento del tiempo",
|
|
108
|
+
"pl": "Śledzenie czasu",
|
|
109
|
+
"uk": "Відстеження часу",
|
|
110
|
+
"zh-cn": "时间追踪"
|
|
111
|
+
},
|
|
112
|
+
"desc": {
|
|
113
|
+
"en": "Time tracking (clock-in/out) for ioBroker with web app, badge terminal and reports",
|
|
114
|
+
"de": "Zeiterfassung (Ein-/Ausstempeln) für ioBroker mit Web-App, Ausweisterminal und Berichten",
|
|
115
|
+
"ru": "Учет времени (время прихода/ухода) для ioBroker с помощью веб-приложения, терминала бейджей и отчетов",
|
|
116
|
+
"pt": "Rastreamento de tempo (entrada/saída) para ioBroker com aplicativo web, terminal de crachá e relatórios",
|
|
117
|
+
"nl": "Urenregistratie (in-/uitklokken) voor ioBroker met webapp, badgeterminal en rapporten",
|
|
118
|
+
"fr": "Suivi du temps (pointage d'entrée/sortie) pour ioBroker avec application web, terminal de badge et rapports",
|
|
119
|
+
"it": "Monitoraggio del tempo (timbratura/uscita) per ioBroker con web app, terminale badge e report",
|
|
120
|
+
"es": "Seguimiento del tiempo (entrada/salida) para ioBroker con aplicación web, terminal de credencial e informes",
|
|
121
|
+
"pl": "Śledzenie czasu (wejście/wyłączenie) dla ioBroker z aplikacją internetową, terminalem identyfikacyjnym i raportami",
|
|
122
|
+
"uk": "Відстеження часу (прихід/вихід) для ioBroker із веб-додатком, терміналом бейджів і звітами",
|
|
123
|
+
"zh-cn": "通过网络应用程序、徽章终端和报告对 ioBroker 进行时间跟踪(上下班打卡)"
|
|
124
|
+
},
|
|
125
|
+
"authors": [
|
|
126
|
+
"Alex <sadam6752@gmail.com>"
|
|
127
|
+
],
|
|
128
|
+
"keywords": [
|
|
129
|
+
"time tracking",
|
|
130
|
+
"time-tracker",
|
|
131
|
+
"clock in",
|
|
132
|
+
"attendance",
|
|
133
|
+
"working time",
|
|
134
|
+
"pwa",
|
|
135
|
+
"rfid"
|
|
136
|
+
],
|
|
137
|
+
"licenseInformation": {
|
|
138
|
+
"type": "free",
|
|
139
|
+
"license": "MIT"
|
|
140
|
+
},
|
|
141
|
+
"platform": "Javascript/Node.js",
|
|
142
|
+
"icon": "time-tracker.png",
|
|
143
|
+
"enabled": true,
|
|
144
|
+
"extIcon": "https://raw.githubusercontent.com/sadam6752-tech/ioBroker.time-tracker/main/admin/time-tracker.png",
|
|
145
|
+
"readme": "https://github.com/sadam6752-tech/ioBroker.time-tracker/blob/main/README.md",
|
|
146
|
+
"loglevel": "info",
|
|
147
|
+
"tier": 3,
|
|
148
|
+
"mode": "daemon",
|
|
149
|
+
"type": "storage",
|
|
150
|
+
"connectionType": "local",
|
|
151
|
+
"dataSource": "push",
|
|
152
|
+
"compact": false,
|
|
153
|
+
"adminUI": {
|
|
154
|
+
"config": "json"
|
|
155
|
+
},
|
|
156
|
+
"dependencies": [
|
|
157
|
+
{
|
|
158
|
+
"js-controller": ">=6.0.11"
|
|
159
|
+
}
|
|
160
|
+
],
|
|
161
|
+
"globalDependencies": [
|
|
162
|
+
{
|
|
163
|
+
"admin": ">=7.6.20"
|
|
164
|
+
}
|
|
165
|
+
]
|
|
166
|
+
},
|
|
167
|
+
"protectedNative": [
|
|
168
|
+
"sessionSecret",
|
|
169
|
+
"hmacSecret",
|
|
170
|
+
"adminPassword"
|
|
171
|
+
],
|
|
172
|
+
"encryptedNative": [
|
|
173
|
+
"sessionSecret",
|
|
174
|
+
"hmacSecret",
|
|
175
|
+
"adminPassword"
|
|
176
|
+
],
|
|
177
|
+
"native": {
|
|
178
|
+
"port": 8092,
|
|
179
|
+
"bind": "127.0.0.1",
|
|
180
|
+
"dbPath": "",
|
|
181
|
+
"timezone": "Europe/Berlin",
|
|
182
|
+
"defaultLanguage": "de",
|
|
183
|
+
"holidayCountry": "DE",
|
|
184
|
+
"sessionSecret": "",
|
|
185
|
+
"hmacSecret": "",
|
|
186
|
+
"sessionTtlMinutes": 720,
|
|
187
|
+
"editWindowDays": 7,
|
|
188
|
+
"quickRoundMinutes": 0,
|
|
189
|
+
"absenceCalcUntilToday": true,
|
|
190
|
+
"absenceDeductWorktime": false,
|
|
191
|
+
"backupRetentionDays": 30,
|
|
192
|
+
"adminLogin": "admin",
|
|
193
|
+
"adminPassword": "",
|
|
194
|
+
"kioskEnabled": false,
|
|
195
|
+
"trustProxy": false
|
|
196
|
+
},
|
|
197
|
+
"objects": [],
|
|
198
|
+
"instanceObjects": [
|
|
199
|
+
{
|
|
200
|
+
"_id": "info",
|
|
201
|
+
"type": "channel",
|
|
202
|
+
"common": {
|
|
203
|
+
"name": "Information"
|
|
204
|
+
},
|
|
205
|
+
"native": {}
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
"_id": "info.connection",
|
|
209
|
+
"type": "state",
|
|
210
|
+
"common": {
|
|
211
|
+
"role": "indicator.connected",
|
|
212
|
+
"name": "Device or service connected",
|
|
213
|
+
"type": "boolean",
|
|
214
|
+
"read": true,
|
|
215
|
+
"write": false,
|
|
216
|
+
"def": false
|
|
217
|
+
},
|
|
218
|
+
"native": {}
|
|
219
|
+
}
|
|
220
|
+
]
|
|
221
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "iobroker.time-tracker",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Time tracking (clock-in/out) for ioBroker with web app, badge terminal and reports",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Alex",
|
|
7
|
+
"email": "sadam6752@gmail.com"
|
|
8
|
+
},
|
|
9
|
+
"contributors": [],
|
|
10
|
+
"homepage": "https://github.com/sadam6752-tech/ioBroker.time-tracker",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"ioBroker",
|
|
14
|
+
"time tracking",
|
|
15
|
+
"time-tracker",
|
|
16
|
+
"clock in",
|
|
17
|
+
"attendance",
|
|
18
|
+
"working time",
|
|
19
|
+
"pwa",
|
|
20
|
+
"rfid"
|
|
21
|
+
],
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/sadam6752-tech/ioBroker.time-tracker.git"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">= 22"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@iobroker/adapter-core": "^3.4.3",
|
|
31
|
+
"better-sqlite3": "^13.0.3",
|
|
32
|
+
"exceljs": "^4.4.0",
|
|
33
|
+
"luxon": "^3.7.2",
|
|
34
|
+
"pdfkit": "^0.20.2",
|
|
35
|
+
"ws": "^8.21.3"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@alcalzone/release-script": "^5.2.1",
|
|
39
|
+
"@alcalzone/release-script-plugin-iobroker": "^5.2.0",
|
|
40
|
+
"@alcalzone/release-script-plugin-license": "^5.2.2",
|
|
41
|
+
"@alcalzone/release-script-plugin-manual-review": "^5.2.0",
|
|
42
|
+
"@iobroker/adapter-dev": "^1.5.0",
|
|
43
|
+
"@iobroker/dev-server": "^0.8.0",
|
|
44
|
+
"@iobroker/eslint-config": "^2.3.4",
|
|
45
|
+
"@iobroker/testing": "^6.2.1",
|
|
46
|
+
"@playwright/test": "^1.63.0",
|
|
47
|
+
"@tsconfig/node22": "^22.0.6",
|
|
48
|
+
"@types/better-sqlite3": "^9.6.0",
|
|
49
|
+
"@types/iobroker": "npm:@iobroker/types@^7.2.2",
|
|
50
|
+
"@types/luxon": "^3.7.5",
|
|
51
|
+
"@types/node": "^22.20.2",
|
|
52
|
+
"@types/pdfkit": "^0.17.6",
|
|
53
|
+
"@types/ws": "^8.18.1",
|
|
54
|
+
"nyc": "^18.0.0",
|
|
55
|
+
"rimraf": "^6.1.3",
|
|
56
|
+
"source-map-support": "^0.5.21",
|
|
57
|
+
"ts-node": "^10.9.2",
|
|
58
|
+
"typescript": "~5.9.3"
|
|
59
|
+
},
|
|
60
|
+
"main": "build/main.js",
|
|
61
|
+
"files": [
|
|
62
|
+
"admin{,/!(src)/**}/!(tsconfig|tsconfig.*|.eslintrc).{json,json5}",
|
|
63
|
+
"admin{,/!(src)/**}/*.{html,css,png,svg,jpg,js}",
|
|
64
|
+
"build/",
|
|
65
|
+
"www/",
|
|
66
|
+
"src-pwa/src/i18n/",
|
|
67
|
+
"io-package.json",
|
|
68
|
+
"LICENSE"
|
|
69
|
+
],
|
|
70
|
+
"scripts": {
|
|
71
|
+
"prebuild": "rimraf build",
|
|
72
|
+
"build": "build-adapter ts",
|
|
73
|
+
"build:pwa": "npm --prefix src-pwa run build",
|
|
74
|
+
"dev:pwa": "npm --prefix src-pwa run dev",
|
|
75
|
+
"install:pwa": "npm --prefix src-pwa install",
|
|
76
|
+
"watch": "build-adapter ts --watch",
|
|
77
|
+
"prebuild:ts": "rimraf build",
|
|
78
|
+
"build:ts": "build-adapter ts",
|
|
79
|
+
"watch:ts": "build-adapter ts --watch",
|
|
80
|
+
"test:ts": "mocha --config test/mocharc.custom.json src/**/*.test.ts",
|
|
81
|
+
"test:package": "mocha test/package --exit",
|
|
82
|
+
"test:integration": "mocha test/integration --exit",
|
|
83
|
+
"test": "npm run test:ts && npm run test:package",
|
|
84
|
+
"check": "tsc --noEmit && npm --prefix src-pwa run check",
|
|
85
|
+
"coverage": "nyc mocha --config test/mocharc.coverage.json src/**/*.test.ts",
|
|
86
|
+
"lint": "eslint -c eslint.config.mjs . --max-warnings 0",
|
|
87
|
+
"lint:pwa": "eslint -c eslint.config.mjs src-pwa/src --max-warnings 0",
|
|
88
|
+
"translate": "translate-adapter",
|
|
89
|
+
"release": "release-script",
|
|
90
|
+
"version:check": "node tools/check-version.mjs",
|
|
91
|
+
"check:package": "node tools/check-package.mjs",
|
|
92
|
+
"prepack": "node tools/check-package.mjs",
|
|
93
|
+
"version:bump": "node tools/bump-version.mjs",
|
|
94
|
+
"push:approve": "node tools/approve-push.mjs",
|
|
95
|
+
"hooks:install": "git config core.hooksPath .githooks",
|
|
96
|
+
"dev-server": "dev-server",
|
|
97
|
+
"load-smoke": "node tools/load-smoke.mjs",
|
|
98
|
+
"first-run": "node tools/first-run-check.mjs",
|
|
99
|
+
"e2e": "playwright test",
|
|
100
|
+
"check:i18n": "pwsh -NoProfile -File tools/check-i18n.ps1 -FailOnIssue",
|
|
101
|
+
"check:i18n:report": "pwsh -NoProfile -File tools/check-i18n.ps1",
|
|
102
|
+
"check:i18n:review": "node tools/check-i18n-review.mjs",
|
|
103
|
+
"check:adapter": "pwsh -NoProfile -File tools/check-adapter.ps1 -FailOnIssue"
|
|
104
|
+
},
|
|
105
|
+
"nyc": {
|
|
106
|
+
"include": [
|
|
107
|
+
"src/**/*.ts"
|
|
108
|
+
],
|
|
109
|
+
"exclude": [
|
|
110
|
+
"src/**/*.test.ts"
|
|
111
|
+
],
|
|
112
|
+
"extension": [
|
|
113
|
+
".ts"
|
|
114
|
+
],
|
|
115
|
+
"require": [
|
|
116
|
+
"ts-node/register/transpile-only"
|
|
117
|
+
],
|
|
118
|
+
"reporter": [
|
|
119
|
+
"text-summary",
|
|
120
|
+
"text",
|
|
121
|
+
"html",
|
|
122
|
+
"lcov"
|
|
123
|
+
],
|
|
124
|
+
"sourceMap": true,
|
|
125
|
+
"instrument": true
|
|
126
|
+
},
|
|
127
|
+
"bugs": {
|
|
128
|
+
"url": "https://github.com/sadam6752-tech/ioBroker.time-tracker/issues"
|
|
129
|
+
},
|
|
130
|
+
"readmeFilename": "README.md"
|
|
131
|
+
}
|