iobroker.time-tracker 0.7.5 → 0.7.6

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/lib/db/repositories/automations.ts"],
4
- "sourcesContent": ["/**\n * Automation rules (`automation_rules`, `automation_runs`).\n *\n * The rules of the adapter itself \u2014 not of a foreign state like the trigger rules. Three kinds are supported:\n *\n * - `clockOut`: at a certain local time the adapter punches out an employee who is still clocked in. The punch is\n * written like any other one (`source: \"api\"`, note `auto.clock_out`), so it appears in the day, in the report\n * and in the audit trail \u2014 the employee can correct it afterwards like any other punch.\n * - `missingPunch`: the same moment, but the adapter only reports it. Nothing is written, so a shop that wants to\n * decide case by case gets a warning instead of a silent correction.\n * - `breakReminder`: an employee whose running work block reached the configured length gets a reminder \u2014 after a\n * break, the block starts again.\n *\n * `user_id` is `NULL` for a company rule and set for a rule of one employee. A rule runs at most once per employee\n * and local date: `automation_runs` holds that decision, which also gives the administration a small log (`action`\n * and `fired_at`) to look at.\n */\n\nimport type { Db } from \"../database\";\nimport { NotFoundError, ValidationError } from \"../../errors\";\nimport { diffFields, writeAuditLog } from \"./audit\";\n\n/** Kinds an automation rule can have. */\nexport type AutomationKind = \"clockIn\" | \"clockOut\" | \"missingPunch\" | \"breakReminder\";\n\n/** How often a rule may act for one employee. */\nexport type AutomationRepeat = \"day\" | \"week\";\n\n/** An automation rule as it is stored. */\nexport interface AutomationRuleRecord {\n\t/** Primary key */\n\tid: number;\n\t/** Free-form label, shown in the administration */\n\tlabel: string | null;\n\t/** What the rule does */\n\tkind: AutomationKind;\n\t/** Employee the rule applies to, `null` for every employee */\n\tuserId: number | null;\n\t/** Minute of the local day the rule acts at (kinds `clockOut` and `missingPunch`) */\n\tatMinute: number | null;\n\t/** Length of the running work block in minutes (kind `breakReminder`) */\n\tafterMinutes: number | null;\n\t/** Days of the week the rule runs on, ISO numbers 1 (Monday) to 7 (Sunday) */\n\tweekdays: number[];\n\t/** Whether the rule may act once a day or once an ISO week per employee */\n\trepeat: AutomationRepeat;\n\t/** Inactive rules are ignored but keep their history */\n\tisActive: boolean;\n}\n\n/** Input for creating or changing a rule. */\nexport interface SaveAutomationRuleInput {\n\t/** Id of an existing rule, omitted when a new rule is created */\n\tid?: number;\n\t/** Free-form label */\n\tlabel?: string | null;\n\t/** What the rule does */\n\tkind: AutomationKind;\n\t/** Employee the rule applies to, `null` or omitted for every employee */\n\tuserId?: number | null;\n\t/** Minute of the local day (kinds `clockOut` and `missingPunch`) */\n\tatMinute?: number | null;\n\t/** Length of the running work block in minutes (kind `breakReminder`) */\n\tafterMinutes?: number | null;\n\t/** Days of the week the rule runs on (ISO 1..7), omitted keeps the stored selection */\n\tweekdays?: number[];\n\t/** How often the rule may act, omitted keeps the stored value */\n\trepeat?: AutomationRepeat;\n\t/** `false` disables the rule without deleting it */\n\tisActive?: boolean;\n\t/** Who changes the rule */\n\tactorId: number;\n\t/** Client IP address of the actor */\n\tactorIp?: string | null;\n\t/** Instant of the change, defaults to now */\n\tnow?: number;\n}\n\n/** One run of a rule. */\nexport interface AutomationRunRecord {\n\t/** Rule that ran */\n\truleId: number;\n\t/** Employee it ran for */\n\tuserId: number;\n\t/** Period it ran in: the local date, or the ISO week (`2026-W38`) for a weekly rule */\n\tperiod: string;\n\t/** Instant it ran */\n\tfiredAt: number;\n\t/** Short description of what happened */\n\taction: string;\n}\n\n/** Automation rule storage operations. */\nexport interface AutomationsRepository {\n\t/** All rules, optionally including the disabled ones */\n\tlist(options?: { includeInactive?: boolean }): AutomationRuleRecord[];\n\t/** Reads one rule */\n\tfindById(id: number): AutomationRuleRecord | null;\n\t/** Creates or updates a rule */\n\tsave(input: SaveAutomationRuleInput): AutomationRuleRecord;\n\t/** Deletes a rule (its runs go with it) */\n\tremove(input: { id: number; actorId: number; actorIp?: string | null; now?: number }): boolean;\n\t/** Runs of the rules, newest first */\n\truns(input?: { ruleId?: number; period?: string; limit?: number }): AutomationRunRecord[];\n\t/** True when the rule already ran for that employee in that period */\n\thasRun(input: { ruleId: number; userId: number; period: string }): boolean;\n\t/** Notes a run; a second call for the same rule, employee and period changes nothing and returns `false` */\n\trecordRun(input: { ruleId: number; userId: number; period: string; action: string; now?: number }): boolean;\n\t/** Forgets a run again \u2014 used when an action failed, so the next check can retry it */\n\tforgetRun(input: { ruleId: number; userId: number; period: string }): boolean;\n}\n\n/** Raw database row of a rule. */\ninterface AutomationRuleRow {\n\tid: number;\n\tlabel: string | null;\n\tkind: string;\n\tuser_id: number | null;\n\tat_minute: number | null;\n\tafter_minutes: number | null;\n\tweekdays: number;\n\trepeat: string;\n\tis_active: number;\n}\n\n/** Raw database row of a run. */\ninterface AutomationRunRow {\n\trule_id: number;\n\tuser_id: number;\n\tperiod: string;\n\tfired_at: number;\n\taction: string;\n}\n\nconst RULE_COLUMNS = \"id, label, kind, user_id, at_minute, after_minutes, weekdays, repeat, is_active\";\nconst RUN_COLUMNS = \"rule_id, user_id, period, fired_at, action\";\n\n/**\n * Fields of a rule that are compared for the audit trail.\n *\n * `weekdays` is not in here: it is a list, and the comparison of the audit trail works on single values. The saved\n * selection is written to the audit detail separately.\n */\nconst AUDITED_FIELDS: (keyof AutomationRuleRecord)[] = [\n\t\"label\",\n\t\"kind\",\n\t\"userId\",\n\t\"atMinute\",\n\t\"afterMinutes\",\n\t\"repeat\",\n\t\"isActive\",\n];\n\n/** Kinds a rule may use. */\nconst KINDS: AutomationKind[] = [\"clockIn\", \"clockOut\", \"missingPunch\", \"breakReminder\"];\n\n/** Kinds that act at a fixed local time. */\nconst TIMED_KINDS: AutomationKind[] = [\"clockIn\", \"clockOut\", \"missingPunch\"];\n\n/**\n * Maps a database row to a rule record.\n *\n * @param row - raw database row\n * @returns rule record\n */\nexport function mapAutomationRuleRow(row: AutomationRuleRow): AutomationRuleRecord {\n\treturn {\n\t\tid: row.id,\n\t\tlabel: row.label,\n\t\tkind: KINDS.includes(row.kind as AutomationKind) ? (row.kind as AutomationKind) : \"clockOut\",\n\t\tuserId: row.user_id,\n\t\tatMinute: row.at_minute,\n\t\tafterMinutes: row.after_minutes,\n\t\t// a row written before the migration has no selection: every day is the sensible default\n\t\tweekdays: maskToWeekdays(row.weekdays ?? 127),\n\t\trepeat: row.repeat === \"week\" ? \"week\" : \"day\",\n\t\tisActive: row.is_active !== 0,\n\t};\n}\n\n/**\n * Maps a database row to a run record.\n *\n * @param row - raw database row\n * @returns run record\n */\nexport function mapAutomationRunRow(row: AutomationRunRow): AutomationRunRecord {\n\treturn {\n\t\truleId: row.rule_id,\n\t\tuserId: row.user_id,\n\t\tperiod: row.period,\n\t\tfiredAt: row.fired_at,\n\t\taction: row.action,\n\t};\n}\n\n/**\n * Validates the kind of a rule.\n *\n * @param value - kind from the request\n * @returns the validated kind\n */\nfunction requireKind(value: string): AutomationKind {\n\tif (!KINDS.includes(value as AutomationKind)) {\n\t\tthrow new ValidationError(`kind must be one of ${KINDS.join(\", \")} (got ${value})`);\n\t}\n\treturn value as AutomationKind;\n}\n\n/**\n * Validates the time of a rule: a timed kind needs a minute of the day, the reminder needs a length.\n *\n * @param kind - kind of the rule\n * @param atMinute - minute of the local day\n * @param afterMinutes - length of the running work block\n * @returns the validated values\n */\nfunction requireWindow(\n\tkind: AutomationKind,\n\tatMinute: number | null,\n\tafterMinutes: number | null,\n): { atMinute: number | null; afterMinutes: number | null } {\n\tif (TIMED_KINDS.includes(kind)) {\n\t\tif (atMinute === null || !Number.isInteger(atMinute) || atMinute < 0 || atMinute > 1439) {\n\t\t\tthrow new ValidationError(`atMinute must be a minute of the day between 0 and 1439 (got ${atMinute})`);\n\t\t}\n\t\treturn { atMinute, afterMinutes: null };\n\t}\n\n\tif (afterMinutes === null || !Number.isInteger(afterMinutes) || afterMinutes < 1 || afterMinutes > 1440) {\n\t\tthrow new ValidationError(`afterMinutes must be between 1 and 1440 (got ${afterMinutes})`);\n\t}\n\treturn { atMinute: null, afterMinutes };\n}\n\n/** Days a rule runs on when nothing else is chosen. */\nexport const ALL_WEEKDAYS: number[] = [1, 2, 3, 4, 5, 6, 7];\n\n/**\n * Turns the stored bitmask into ISO weekdays.\n *\n * @param mask - bits 0..6, bit 0 is Monday\n * @returns the days, at least every day when the mask is empty\n */\nexport function maskToWeekdays(mask: number): number[] {\n\tconst days: number[] = [];\n\tfor (let day = 1; day <= 7; day += 1) {\n\t\tif ((mask & (1 << (day - 1))) !== 0) {\n\t\t\tdays.push(day);\n\t\t}\n\t}\n\treturn days.length > 0 ? days : [...ALL_WEEKDAYS];\n}\n\n/**\n * Turns ISO weekdays into the stored bitmask.\n *\n * @param days - the days, ISO numbers 1..7\n * @returns the mask\n */\nexport function weekdaysToMask(days: number[]): number {\n\tlet mask = 0;\n\tfor (const day of days) {\n\t\tmask |= 1 << (day - 1);\n\t}\n\treturn mask;\n}\n\n/**\n * Validates the weekday selection of a rule.\n *\n * @param value - selection from the request, `undefined` keeps the stored one\n * @param fallback - the stored selection\n * @returns the validated, sorted selection\n */\nfunction requireWeekdays(value: number[] | undefined, fallback: number[]): number[] {\n\tconst days = value ?? fallback;\n\tif (!Array.isArray(days) || days.length === 0) {\n\t\tthrow new ValidationError(\"weekdays must name at least one day (1 = Monday \u2026 7 = Sunday)\");\n\t}\n\tfor (const day of days) {\n\t\tif (!Number.isInteger(day) || day < 1 || day > 7) {\n\t\t\tthrow new ValidationError(`weekdays must be ISO days 1..7 (got ${day})`);\n\t\t}\n\t}\n\treturn [...new Set(days)].sort((left, right) => left - right);\n}\n\n/**\n * Validates how often a rule may act.\n *\n * @param value - value from the request, `undefined` keeps the stored one\n * @param fallback - the stored value\n * @returns the validated value\n */\nfunction requireRepeat(value: AutomationRepeat | undefined, fallback: AutomationRepeat): AutomationRepeat {\n\tconst repeat = value ?? fallback;\n\tif (repeat !== \"day\" && repeat !== \"week\") {\n\t\t// the type already rules it out, this is for a caller that is not TypeScript\n\t\tthrow new ValidationError(`repeat must be \"day\" or \"week\" (got ${String(value)})`);\n\t}\n\treturn repeat;\n}\n\n/**\n * Creates the automation rule repository.\n *\n * @param db - open database handle\n * @returns repository instance\n */\nexport function createAutomationsRepository(db: Db): AutomationsRepository {\n\tconst selectById = db.prepare(`SELECT ${RULE_COLUMNS} FROM automation_rules WHERE id = ?`);\n\tconst selectAll = db.prepare(`SELECT ${RULE_COLUMNS} FROM automation_rules ORDER BY id`);\n\tconst selectActive = db.prepare(`SELECT ${RULE_COLUMNS} FROM automation_rules WHERE is_active = 1 ORDER BY id`);\n\tconst insertRule = db.prepare(\n\t\t`INSERT INTO automation_rules (label, kind, user_id, at_minute, after_minutes, weekdays, repeat, is_active, created_at, updated_at)\n\t\t VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n\t);\n\tconst updateRule = db.prepare(\n\t\t`UPDATE automation_rules\n\t\t SET label = ?, kind = ?, user_id = ?, at_minute = ?, after_minutes = ?, weekdays = ?, repeat = ?,\n\t\t is_active = ?, updated_at = ?\n\t\t WHERE id = ?`,\n\t);\n\tconst deleteRule = db.prepare(\"DELETE FROM automation_rules WHERE id = ?\");\n\t// the runs are deleted with their rule: the rebuilt table of migration 18 has no foreign key for that\n\tconst deleteRunsOfRule = db.prepare(\"DELETE FROM automation_runs WHERE rule_id = ?\");\n\tconst selectRuns = db.prepare(\n\t\t`SELECT ${RUN_COLUMNS} FROM automation_runs\n\t\t WHERE rule_id = ? AND (? IS NULL OR period = ?)\n\t\t ORDER BY period DESC, rule_id LIMIT ?`,\n\t);\n\tconst selectAllRuns = db.prepare(\n\t\t`SELECT ${RUN_COLUMNS} FROM automation_runs ORDER BY fired_at DESC, rule_id LIMIT ?`,\n\t);\n\tconst selectRun = db.prepare(\n\t\t\"SELECT 1 AS found FROM automation_runs WHERE rule_id = ? AND user_id = ? AND period = ?\",\n\t);\n\tconst insertRun = db.prepare(\n\t\t`INSERT OR IGNORE INTO automation_runs (rule_id, user_id, period, fired_at, action)\n\t\t VALUES (?, ?, ?, ?, ?)`,\n\t);\n\tconst deleteRun = db.prepare(\"DELETE FROM automation_runs WHERE rule_id = ? AND user_id = ? AND period = ?\");\n\n\t/**\n\t * Reads one rule.\n\t *\n\t * @param id - rule id\n\t * @returns the rule or `null`\n\t */\n\tconst read = (id: number): AutomationRuleRecord | null => {\n\t\tconst row = selectById.get(id) as AutomationRuleRow | undefined;\n\t\treturn row ? mapAutomationRuleRow(row) : null;\n\t};\n\n\treturn {\n\t\tlist(options?: { includeInactive?: boolean }): AutomationRuleRecord[] {\n\t\t\tconst rows = (options?.includeInactive === true ? selectAll : selectActive).all() as AutomationRuleRow[];\n\t\t\treturn rows.map(mapAutomationRuleRow);\n\t\t},\n\n\t\tfindById: read,\n\n\t\tsave(input: SaveAutomationRuleInput): AutomationRuleRecord {\n\t\t\tconst kind = requireKind(input.kind);\n\t\t\tconst window = requireWindow(kind, input.atMinute ?? null, input.afterMinutes ?? null);\n\t\t\tconst label = (input.label ?? \"\").trim() || null;\n\t\t\tconst userId = input.userId ?? null;\n\t\t\tif (userId !== null && (!Number.isInteger(userId) || userId <= 0)) {\n\t\t\t\tthrow new ValidationError(`userId must reference an existing employee (got ${input.userId})`);\n\t\t\t}\n\t\t\tconst isActive = input.isActive !== false;\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\n\t\t\tconst current = input.id === undefined ? null : read(input.id);\n\t\t\tif (input.id !== undefined && !current) {\n\t\t\t\tthrow new NotFoundError(`automation rule ${input.id} not found`);\n\t\t\t}\n\t\t\tconst weekdays = requireWeekdays(input.weekdays, current?.weekdays ?? ALL_WEEKDAYS);\n\t\t\tconst repeat = requireRepeat(input.repeat, current?.repeat ?? \"day\");\n\n\t\t\tconst next: AutomationRuleRecord = {\n\t\t\t\tid: current?.id ?? 0,\n\t\t\t\tlabel,\n\t\t\t\tkind,\n\t\t\t\tuserId,\n\t\t\t\tatMinute: window.atMinute,\n\t\t\t\tafterMinutes: window.afterMinutes,\n\t\t\t\tweekdays,\n\t\t\t\trepeat,\n\t\t\t\tisActive,\n\t\t\t};\n\t\t\tconst changes = current\n\t\t\t\t? diffFields(\n\t\t\t\t\t\tcurrent as unknown as Record<string, unknown>,\n\t\t\t\t\t\tnext as unknown as Record<string, unknown>,\n\t\t\t\t\t\tAUDITED_FIELDS as unknown as (keyof Record<string, unknown>)[],\n\t\t\t\t\t)\n\t\t\t\t: null;\n\t\t\t// the days are a list, so they are compared by their mask \u2014 that is also what the audit trail shows\n\t\t\tif (changes && weekdaysToMask(current?.weekdays ?? ALL_WEEKDAYS) !== weekdaysToMask(weekdays)) {\n\t\t\t\tchanges.weekdays = { old: current?.weekdays ?? [], new: weekdays };\n\t\t\t}\n\t\t\tif (current && Object.keys(changes ?? {}).length === 0) {\n\t\t\t\treturn current;\n\t\t\t}\n\n\t\t\tlet ruleId = current?.id ?? 0;\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tif (current) {\n\t\t\t\t\tupdateRule.run(\n\t\t\t\t\t\tlabel,\n\t\t\t\t\t\tkind,\n\t\t\t\t\t\tuserId,\n\t\t\t\t\t\twindow.atMinute,\n\t\t\t\t\t\twindow.afterMinutes,\n\t\t\t\t\t\tweekdaysToMask(weekdays),\n\t\t\t\t\t\trepeat,\n\t\t\t\t\t\tisActive ? 1 : 0,\n\t\t\t\t\t\tnow,\n\t\t\t\t\t\tcurrent.id,\n\t\t\t\t\t);\n\t\t\t\t} else {\n\t\t\t\t\truleId = Number(\n\t\t\t\t\t\tinsertRule.run(\n\t\t\t\t\t\t\tlabel,\n\t\t\t\t\t\t\tkind,\n\t\t\t\t\t\t\tuserId,\n\t\t\t\t\t\t\twindow.atMinute,\n\t\t\t\t\t\t\twindow.afterMinutes,\n\t\t\t\t\t\t\tweekdaysToMask(weekdays),\n\t\t\t\t\t\t\trepeat,\n\t\t\t\t\t\t\tisActive ? 1 : 0,\n\t\t\t\t\t\t\tnow,\n\t\t\t\t\t\t\tnow,\n\t\t\t\t\t\t).lastInsertRowid,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\tatUtc: now,\n\t\t\t\t\tactorId: input.actorId,\n\t\t\t\t\taction: current ? \"automation_rule.update\" : \"automation_rule.create\",\n\t\t\t\t\tentity: \"automation_rule\",\n\t\t\t\t\tentityId: ruleId,\n\t\t\t\t\tdetail: current\n\t\t\t\t\t\t? { changes }\n\t\t\t\t\t\t: {\n\t\t\t\t\t\t\t\tkind,\n\t\t\t\t\t\t\t\tuserId,\n\t\t\t\t\t\t\t\tatMinute: window.atMinute,\n\t\t\t\t\t\t\t\tafterMinutes: window.afterMinutes,\n\t\t\t\t\t\t\t\tweekdays,\n\t\t\t\t\t\t\t\trepeat,\n\t\t\t\t\t\t\t\tisActive,\n\t\t\t\t\t\t\t},\n\t\t\t\t\tip: input.actorIp ?? null,\n\t\t\t\t});\n\t\t\t});\n\t\t\trun();\n\n\t\t\tconst saved = read(ruleId);\n\t\t\tif (!saved) {\n\t\t\t\tthrow new Error(`automation rule ${ruleId} disappeared right after the write`);\n\t\t\t}\n\t\t\treturn saved;\n\t\t},\n\n\t\tremove(input: { id: number; actorId: number; actorIp?: string | null; now?: number }): boolean {\n\t\t\tconst current = read(input.id);\n\t\t\tif (!current) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tdeleteRunsOfRule.run(input.id);\n\t\t\t\tdeleteRule.run(input.id);\n\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\tatUtc: now,\n\t\t\t\t\tactorId: input.actorId,\n\t\t\t\t\taction: \"automation_rule.delete\",\n\t\t\t\t\tentity: \"automation_rule\",\n\t\t\t\t\tentityId: input.id,\n\t\t\t\t\tdetail: { kind: current.kind, userId: current.userId, label: current.label },\n\t\t\t\t\tip: input.actorIp ?? null,\n\t\t\t\t});\n\t\t\t});\n\t\t\trun();\n\t\t\treturn true;\n\t\t},\n\n\t\truns(input?: { ruleId?: number; period?: string; limit?: number }): AutomationRunRecord[] {\n\t\t\tconst limit = Math.min(Math.max(input?.limit ?? 50, 1), 500);\n\t\t\tif (input?.ruleId === undefined) {\n\t\t\t\treturn (selectAllRuns.all(limit) as AutomationRunRow[]).map(mapAutomationRunRow);\n\t\t\t}\n\t\t\tconst rows = selectRuns.all(\n\t\t\t\tinput.ruleId,\n\t\t\t\tinput.period ?? null,\n\t\t\t\tinput.period ?? null,\n\t\t\t\tlimit,\n\t\t\t) as AutomationRunRow[];\n\t\t\treturn rows.map(mapAutomationRunRow);\n\t\t},\n\n\t\thasRun(input: { ruleId: number; userId: number; period: string }): boolean {\n\t\t\treturn selectRun.get(input.ruleId, input.userId, input.period) !== undefined;\n\t\t},\n\n\t\trecordRun(input: { ruleId: number; userId: number; period: string; action: string; now?: number }): boolean {\n\t\t\tconst result = insertRun.run(\n\t\t\t\tinput.ruleId,\n\t\t\t\tinput.userId,\n\t\t\t\tinput.period,\n\t\t\t\tinput.now ?? Math.floor(Date.now() / 1000),\n\t\t\t\tinput.action,\n\t\t\t);\n\t\t\treturn result.changes > 0;\n\t\t},\n\n\t\tforgetRun(input: { ruleId: number; userId: number; period: string }): boolean {\n\t\t\treturn deleteRun.run(input.ruleId, input.userId, input.period).changes > 0;\n\t\t},\n\t};\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Automation rules (`automation_rules`, `automation_runs`).\n *\n * The rules of the adapter itself \u2014 not of a foreign state like the trigger rules. Three kinds are supported:\n *\n * - `clockOut`: at a certain local time the adapter punches out an employee who is still clocked in. The punch is\n * written like any other one (`source: \"api\"`, note `auto.clock_out`), so it appears in the day, in the report\n * and in the audit trail \u2014 the employee can correct it afterwards like any other punch.\n * - `missingPunch`: the same moment, but the adapter only reports it. Nothing is written, so a shop that wants to\n * decide case by case gets a warning instead of a silent correction.\n * - `breakReminder`: an employee whose running work block reached the configured length gets a reminder \u2014 after a\n * break, the block starts again.\n *\n * `user_id` is `NULL` for a company rule and set for a rule of one employee. A rule runs at most once per employee\n * and local date: `automation_runs` holds that decision, which also gives the administration a small log (`action`\n * and `fired_at`) to look at.\n */\n\nimport type { Db } from \"../database\";\nimport { NotFoundError, ValidationError } from \"../../errors\";\nimport { diffFields, writeAuditLog } from \"./audit\";\n\n/** Kinds an automation rule can have. */\nexport type AutomationKind = \"clockIn\" | \"clockOut\" | \"missingPunch\" | \"breakReminder\";\n\n/** How often a rule may act for one employee. */\nexport type AutomationRepeat = \"day\" | \"week\";\n\n/** An automation rule as it is stored. */\nexport interface AutomationRuleRecord {\n\t/** Primary key */\n\tid: number;\n\t/** Free-form label, shown in the administration */\n\tlabel: string | null;\n\t/** What the rule does */\n\tkind: AutomationKind;\n\t/** Employee the rule applies to, `null` for every employee */\n\tuserId: number | null;\n\t/** Minute of the local day the rule acts at (kinds `clockOut` and `missingPunch`) */\n\tatMinute: number | null;\n\t/** Length of the running work block in minutes (kind `breakReminder`) */\n\tafterMinutes: number | null;\n\t/** Days of the week the rule runs on, ISO numbers 1 (Monday) to 7 (Sunday) */\n\tweekdays: number[];\n\t/** Whether the rule may act once a day or once an ISO week per employee */\n\trepeat: AutomationRepeat;\n\t/** Inactive rules are ignored but keep their history */\n\tisActive: boolean;\n\t/** First local day the rule is valid on (`YYYY-MM-DD`), `null` for \u201Cfrom now on\u201D */\n\tactiveFrom: string | null;\n\t/** Last local day the rule is valid on (`YYYY-MM-DD`, inclusive), `null` for \u201Cwithout an end\u201D */\n\tactiveUntil: string | null;\n}\n\n/** Input for creating or changing a rule. */\nexport interface SaveAutomationRuleInput {\n\t/** Id of an existing rule, omitted when a new rule is created */\n\tid?: number;\n\t/** Free-form label */\n\tlabel?: string | null;\n\t/** What the rule does */\n\tkind: AutomationKind;\n\t/** Employee the rule applies to, `null` or omitted for every employee */\n\tuserId?: number | null;\n\t/** Minute of the local day (kinds `clockOut` and `missingPunch`) */\n\tatMinute?: number | null;\n\t/** Length of the running work block in minutes (kind `breakReminder`) */\n\tafterMinutes?: number | null;\n\t/** Days of the week the rule runs on (ISO 1..7), omitted keeps the stored selection */\n\tweekdays?: number[];\n\t/** How often the rule may act, omitted keeps the stored value */\n\trepeat?: AutomationRepeat;\n\t/** `false` disables the rule without deleting it */\n\tisActive?: boolean;\n\t/** First local day the rule is valid on, omitted keeps the stored value, `null`/empty clears it */\n\tactiveFrom?: string | null;\n\t/** Last local day the rule is valid on (inclusive), omitted keeps the stored value, `null`/empty clears it */\n\tactiveUntil?: string | null;\n\t/** Who changes the rule */\n\tactorId: number;\n\t/** Client IP address of the actor */\n\tactorIp?: string | null;\n\t/** Instant of the change, defaults to now */\n\tnow?: number;\n}\n\n/** One run of a rule. */\nexport interface AutomationRunRecord {\n\t/** Rule that ran */\n\truleId: number;\n\t/** Employee it ran for */\n\tuserId: number;\n\t/** Period it ran in: the local date, or the ISO week (`2026-W38`) for a weekly rule */\n\tperiod: string;\n\t/** Instant it ran */\n\tfiredAt: number;\n\t/** Short description of what happened */\n\taction: string;\n}\n\n/** Automation rule storage operations. */\nexport interface AutomationsRepository {\n\t/** All rules, optionally including the disabled ones */\n\tlist(options?: { includeInactive?: boolean }): AutomationRuleRecord[];\n\t/** Reads one rule */\n\tfindById(id: number): AutomationRuleRecord | null;\n\t/** Creates or updates a rule */\n\tsave(input: SaveAutomationRuleInput): AutomationRuleRecord;\n\t/** Deletes a rule (its runs go with it) */\n\tremove(input: { id: number; actorId: number; actorIp?: string | null; now?: number }): boolean;\n\t/** Runs of the rules, newest first */\n\truns(input?: { ruleId?: number; period?: string; limit?: number }): AutomationRunRecord[];\n\t/** True when the rule already ran for that employee in that period */\n\thasRun(input: { ruleId: number; userId: number; period: string }): boolean;\n\t/** Notes a run; a second call for the same rule, employee and period changes nothing and returns `false` */\n\trecordRun(input: { ruleId: number; userId: number; period: string; action: string; now?: number }): boolean;\n\t/** Forgets a run again \u2014 used when an action failed, so the next check can retry it */\n\tforgetRun(input: { ruleId: number; userId: number; period: string }): boolean;\n}\n\n/** Raw database row of a rule. */\ninterface AutomationRuleRow {\n\tid: number;\n\tlabel: string | null;\n\tkind: string;\n\tuser_id: number | null;\n\tat_minute: number | null;\n\tafter_minutes: number | null;\n\tweekdays: number;\n\trepeat: string;\n\tis_active: number;\n\tactive_from: string | null;\n\tactive_until: string | null;\n}\n\n/** Raw database row of a run. */\ninterface AutomationRunRow {\n\trule_id: number;\n\tuser_id: number;\n\tperiod: string;\n\tfired_at: number;\n\taction: string;\n}\n\nconst RULE_COLUMNS =\n\t\"id, label, kind, user_id, at_minute, after_minutes, weekdays, repeat, is_active, active_from, active_until\";\nconst RUN_COLUMNS = \"rule_id, user_id, period, fired_at, action\";\n\n/**\n * Fields of a rule that are compared for the audit trail.\n *\n * `weekdays` is not in here: it is a list, and the comparison of the audit trail works on single values. The saved\n * selection is written to the audit detail separately.\n */\nconst AUDITED_FIELDS: (keyof AutomationRuleRecord)[] = [\n\t\"label\",\n\t\"kind\",\n\t\"userId\",\n\t\"atMinute\",\n\t\"afterMinutes\",\n\t\"repeat\",\n\t\"isActive\",\n\t\"activeFrom\",\n\t\"activeUntil\",\n];\n\n/** Kinds a rule may use. */\nconst KINDS: AutomationKind[] = [\"clockIn\", \"clockOut\", \"missingPunch\", \"breakReminder\"];\n\n/** Kinds that act at a fixed local time. */\nconst TIMED_KINDS: AutomationKind[] = [\"clockIn\", \"clockOut\", \"missingPunch\"];\n\n/**\n * Maps a database row to a rule record.\n *\n * @param row - raw database row\n * @returns rule record\n */\nexport function mapAutomationRuleRow(row: AutomationRuleRow): AutomationRuleRecord {\n\treturn {\n\t\tid: row.id,\n\t\tlabel: row.label,\n\t\tkind: KINDS.includes(row.kind as AutomationKind) ? (row.kind as AutomationKind) : \"clockOut\",\n\t\tuserId: row.user_id,\n\t\tatMinute: row.at_minute,\n\t\tafterMinutes: row.after_minutes,\n\t\t// a row written before the migration has no selection: every day is the sensible default\n\t\tweekdays: maskToWeekdays(row.weekdays ?? 127),\n\t\trepeat: row.repeat === \"week\" ? \"week\" : \"day\",\n\t\tisActive: row.is_active !== 0,\n\t\tactiveFrom: row.active_from ?? null,\n\t\tactiveUntil: row.active_until ?? null,\n\t};\n}\n\n/**\n * Maps a database row to a run record.\n *\n * @param row - raw database row\n * @returns run record\n */\nexport function mapAutomationRunRow(row: AutomationRunRow): AutomationRunRecord {\n\treturn {\n\t\truleId: row.rule_id,\n\t\tuserId: row.user_id,\n\t\tperiod: row.period,\n\t\tfiredAt: row.fired_at,\n\t\taction: row.action,\n\t};\n}\n\n/**\n * Validates the kind of a rule.\n *\n * @param value - kind from the request\n * @returns the validated kind\n */\nfunction requireKind(value: string): AutomationKind {\n\tif (!KINDS.includes(value as AutomationKind)) {\n\t\tthrow new ValidationError(`kind must be one of ${KINDS.join(\", \")} (got ${value})`);\n\t}\n\treturn value as AutomationKind;\n}\n\n/**\n * Validates the time of a rule: a timed kind needs a minute of the day, the reminder needs a length.\n *\n * @param kind - kind of the rule\n * @param atMinute - minute of the local day\n * @param afterMinutes - length of the running work block\n * @returns the validated values\n */\nfunction requireWindow(\n\tkind: AutomationKind,\n\tatMinute: number | null,\n\tafterMinutes: number | null,\n): { atMinute: number | null; afterMinutes: number | null } {\n\tif (TIMED_KINDS.includes(kind)) {\n\t\tif (atMinute === null || !Number.isInteger(atMinute) || atMinute < 0 || atMinute > 1439) {\n\t\t\tthrow new ValidationError(`atMinute must be a minute of the day between 0 and 1439 (got ${atMinute})`);\n\t\t}\n\t\treturn { atMinute, afterMinutes: null };\n\t}\n\n\tif (afterMinutes === null || !Number.isInteger(afterMinutes) || afterMinutes < 1 || afterMinutes > 1440) {\n\t\tthrow new ValidationError(`afterMinutes must be between 1 and 1440 (got ${afterMinutes})`);\n\t}\n\treturn { atMinute: null, afterMinutes };\n}\n\n/**\n * Validates one end of the validity period of a rule.\n *\n * `undefined` keeps the stored value, an empty string or `null` clears the limit (that is the \u201Cfrom now on\u201D and\n * \u201Cwithout an end\u201D of the dialog), anything else has to be a real calendar date in `YYYY-MM-DD`.\n *\n * @param value - value from the request\n * @param fallback - value to keep when the request says nothing\n * @param field - field name for the error message\n * @returns the validated date or `null`\n */\nfunction requireValidityDate(value: string | null | undefined, fallback: string | null, field: string): string | null {\n\tif (value === undefined) {\n\t\treturn fallback;\n\t}\n\tconst text = (value ?? \"\").trim();\n\tif (text === \"\") {\n\t\treturn null;\n\t}\n\tconst parts = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(text);\n\tconst date = parts ? new Date(Date.UTC(Number(parts[1]), Number(parts[2]) - 1, Number(parts[3]))) : null;\n\tif (\n\t\t!parts ||\n\t\t!date ||\n\t\tdate.getUTCFullYear() !== Number(parts[1]) ||\n\t\tdate.getUTCMonth() !== Number(parts[2]) - 1 ||\n\t\tdate.getUTCDate() !== Number(parts[3])\n\t) {\n\t\tthrow new ValidationError(`${field} must be a date like 2026-10-01 (got ${text})`);\n\t}\n\treturn text;\n}\n\n/** Days a rule runs on when nothing else is chosen. */\nexport const ALL_WEEKDAYS: number[] = [1, 2, 3, 4, 5, 6, 7];\n\n/**\n * Turns the stored bitmask into ISO weekdays.\n *\n * @param mask - bits 0..6, bit 0 is Monday\n * @returns the days, at least every day when the mask is empty\n */\nexport function maskToWeekdays(mask: number): number[] {\n\tconst days: number[] = [];\n\tfor (let day = 1; day <= 7; day += 1) {\n\t\tif ((mask & (1 << (day - 1))) !== 0) {\n\t\t\tdays.push(day);\n\t\t}\n\t}\n\treturn days.length > 0 ? days : [...ALL_WEEKDAYS];\n}\n\n/**\n * Turns ISO weekdays into the stored bitmask.\n *\n * @param days - the days, ISO numbers 1..7\n * @returns the mask\n */\nexport function weekdaysToMask(days: number[]): number {\n\tlet mask = 0;\n\tfor (const day of days) {\n\t\tmask |= 1 << (day - 1);\n\t}\n\treturn mask;\n}\n\n/**\n * Validates the weekday selection of a rule.\n *\n * @param value - selection from the request, `undefined` keeps the stored one\n * @param fallback - the stored selection\n * @returns the validated, sorted selection\n */\nfunction requireWeekdays(value: number[] | undefined, fallback: number[]): number[] {\n\tconst days = value ?? fallback;\n\tif (!Array.isArray(days) || days.length === 0) {\n\t\tthrow new ValidationError(\"weekdays must name at least one day (1 = Monday \u2026 7 = Sunday)\");\n\t}\n\tfor (const day of days) {\n\t\tif (!Number.isInteger(day) || day < 1 || day > 7) {\n\t\t\tthrow new ValidationError(`weekdays must be ISO days 1..7 (got ${day})`);\n\t\t}\n\t}\n\treturn [...new Set(days)].sort((left, right) => left - right);\n}\n\n/**\n * Validates how often a rule may act.\n *\n * @param value - value from the request, `undefined` keeps the stored one\n * @param fallback - the stored value\n * @returns the validated value\n */\nfunction requireRepeat(value: AutomationRepeat | undefined, fallback: AutomationRepeat): AutomationRepeat {\n\tconst repeat = value ?? fallback;\n\tif (repeat !== \"day\" && repeat !== \"week\") {\n\t\t// the type already rules it out, this is for a caller that is not TypeScript\n\t\tthrow new ValidationError(`repeat must be \"day\" or \"week\" (got ${String(value)})`);\n\t}\n\treturn repeat;\n}\n\n/**\n * Creates the automation rule repository.\n *\n * @param db - open database handle\n * @returns repository instance\n */\nexport function createAutomationsRepository(db: Db): AutomationsRepository {\n\tconst selectById = db.prepare(`SELECT ${RULE_COLUMNS} FROM automation_rules WHERE id = ?`);\n\tconst selectAll = db.prepare(`SELECT ${RULE_COLUMNS} FROM automation_rules ORDER BY id`);\n\tconst selectActive = db.prepare(`SELECT ${RULE_COLUMNS} FROM automation_rules WHERE is_active = 1 ORDER BY id`);\n\tconst insertRule = db.prepare(\n\t\t`INSERT INTO automation_rules (label, kind, user_id, at_minute, after_minutes, weekdays, repeat, is_active, active_from, active_until, created_at, updated_at)\n\t\t VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n\t);\n\tconst updateRule = db.prepare(\n\t\t`UPDATE automation_rules\n\t\t SET label = ?, kind = ?, user_id = ?, at_minute = ?, after_minutes = ?, weekdays = ?, repeat = ?,\n\t\t is_active = ?, active_from = ?, active_until = ?, updated_at = ?\n\t\t WHERE id = ?`,\n\t);\n\tconst deleteRule = db.prepare(\"DELETE FROM automation_rules WHERE id = ?\");\n\t// the runs are deleted with their rule: the rebuilt table of migration 18 has no foreign key for that\n\tconst deleteRunsOfRule = db.prepare(\"DELETE FROM automation_runs WHERE rule_id = ?\");\n\tconst selectRuns = db.prepare(\n\t\t`SELECT ${RUN_COLUMNS} FROM automation_runs\n\t\t WHERE rule_id = ? AND (? IS NULL OR period = ?)\n\t\t ORDER BY period DESC, rule_id LIMIT ?`,\n\t);\n\tconst selectAllRuns = db.prepare(\n\t\t`SELECT ${RUN_COLUMNS} FROM automation_runs ORDER BY fired_at DESC, rule_id LIMIT ?`,\n\t);\n\tconst selectRun = db.prepare(\n\t\t\"SELECT 1 AS found FROM automation_runs WHERE rule_id = ? AND user_id = ? AND period = ?\",\n\t);\n\tconst insertRun = db.prepare(\n\t\t`INSERT OR IGNORE INTO automation_runs (rule_id, user_id, period, fired_at, action)\n\t\t VALUES (?, ?, ?, ?, ?)`,\n\t);\n\tconst deleteRun = db.prepare(\"DELETE FROM automation_runs WHERE rule_id = ? AND user_id = ? AND period = ?\");\n\n\t/**\n\t * Reads one rule.\n\t *\n\t * @param id - rule id\n\t * @returns the rule or `null`\n\t */\n\tconst read = (id: number): AutomationRuleRecord | null => {\n\t\tconst row = selectById.get(id) as AutomationRuleRow | undefined;\n\t\treturn row ? mapAutomationRuleRow(row) : null;\n\t};\n\n\treturn {\n\t\tlist(options?: { includeInactive?: boolean }): AutomationRuleRecord[] {\n\t\t\tconst rows = (options?.includeInactive === true ? selectAll : selectActive).all() as AutomationRuleRow[];\n\t\t\treturn rows.map(mapAutomationRuleRow);\n\t\t},\n\n\t\tfindById: read,\n\n\t\tsave(input: SaveAutomationRuleInput): AutomationRuleRecord {\n\t\t\tconst kind = requireKind(input.kind);\n\t\t\tconst window = requireWindow(kind, input.atMinute ?? null, input.afterMinutes ?? null);\n\t\t\tconst label = (input.label ?? \"\").trim() || null;\n\t\t\tconst userId = input.userId ?? null;\n\t\t\tif (userId !== null && (!Number.isInteger(userId) || userId <= 0)) {\n\t\t\t\tthrow new ValidationError(`userId must reference an existing employee (got ${input.userId})`);\n\t\t\t}\n\t\t\tconst isActive = input.isActive !== false;\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\n\t\t\tconst current = input.id === undefined ? null : read(input.id);\n\t\t\tif (input.id !== undefined && !current) {\n\t\t\t\tthrow new NotFoundError(`automation rule ${input.id} not found`);\n\t\t\t}\n\t\t\tconst weekdays = requireWeekdays(input.weekdays, current?.weekdays ?? ALL_WEEKDAYS);\n\t\t\tconst repeat = requireRepeat(input.repeat, current?.repeat ?? \"day\");\n\t\t\tconst activeFrom = requireValidityDate(input.activeFrom, current?.activeFrom ?? null, \"activeFrom\");\n\t\t\tconst activeUntil = requireValidityDate(input.activeUntil, current?.activeUntil ?? null, \"activeUntil\");\n\t\t\tif (activeFrom !== null && activeUntil !== null && activeUntil < activeFrom) {\n\t\t\t\tthrow new ValidationError(`activeUntil must not be before activeFrom (${activeFrom} \u2026 ${activeUntil})`);\n\t\t\t}\n\n\t\t\tconst next: AutomationRuleRecord = {\n\t\t\t\tid: current?.id ?? 0,\n\t\t\t\tlabel,\n\t\t\t\tkind,\n\t\t\t\tuserId,\n\t\t\t\tatMinute: window.atMinute,\n\t\t\t\tafterMinutes: window.afterMinutes,\n\t\t\t\tweekdays,\n\t\t\t\trepeat,\n\t\t\t\tisActive,\n\t\t\t\tactiveFrom,\n\t\t\t\tactiveUntil,\n\t\t\t};\n\t\t\tconst changes = current\n\t\t\t\t? diffFields(\n\t\t\t\t\t\tcurrent as unknown as Record<string, unknown>,\n\t\t\t\t\t\tnext as unknown as Record<string, unknown>,\n\t\t\t\t\t\tAUDITED_FIELDS as unknown as (keyof Record<string, unknown>)[],\n\t\t\t\t\t)\n\t\t\t\t: null;\n\t\t\t// the days are a list, so they are compared by their mask \u2014 that is also what the audit trail shows\n\t\t\tif (changes && weekdaysToMask(current?.weekdays ?? ALL_WEEKDAYS) !== weekdaysToMask(weekdays)) {\n\t\t\t\tchanges.weekdays = { old: current?.weekdays ?? [], new: weekdays };\n\t\t\t}\n\t\t\tif (current && Object.keys(changes ?? {}).length === 0) {\n\t\t\t\treturn current;\n\t\t\t}\n\n\t\t\tlet ruleId = current?.id ?? 0;\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tif (current) {\n\t\t\t\t\tupdateRule.run(\n\t\t\t\t\t\tlabel,\n\t\t\t\t\t\tkind,\n\t\t\t\t\t\tuserId,\n\t\t\t\t\t\twindow.atMinute,\n\t\t\t\t\t\twindow.afterMinutes,\n\t\t\t\t\t\tweekdaysToMask(weekdays),\n\t\t\t\t\t\trepeat,\n\t\t\t\t\t\tisActive ? 1 : 0,\n\t\t\t\t\t\tactiveFrom,\n\t\t\t\t\t\tactiveUntil,\n\t\t\t\t\t\tnow,\n\t\t\t\t\t\tcurrent.id,\n\t\t\t\t\t);\n\t\t\t\t} else {\n\t\t\t\t\truleId = Number(\n\t\t\t\t\t\tinsertRule.run(\n\t\t\t\t\t\t\tlabel,\n\t\t\t\t\t\t\tkind,\n\t\t\t\t\t\t\tuserId,\n\t\t\t\t\t\t\twindow.atMinute,\n\t\t\t\t\t\t\twindow.afterMinutes,\n\t\t\t\t\t\t\tweekdaysToMask(weekdays),\n\t\t\t\t\t\t\trepeat,\n\t\t\t\t\t\t\tisActive ? 1 : 0,\n\t\t\t\t\t\t\tactiveFrom,\n\t\t\t\t\t\t\tactiveUntil,\n\t\t\t\t\t\t\tnow,\n\t\t\t\t\t\t\tnow,\n\t\t\t\t\t\t).lastInsertRowid,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\tatUtc: now,\n\t\t\t\t\tactorId: input.actorId,\n\t\t\t\t\taction: current ? \"automation_rule.update\" : \"automation_rule.create\",\n\t\t\t\t\tentity: \"automation_rule\",\n\t\t\t\t\tentityId: ruleId,\n\t\t\t\t\tdetail: current\n\t\t\t\t\t\t? { changes }\n\t\t\t\t\t\t: {\n\t\t\t\t\t\t\t\tkind,\n\t\t\t\t\t\t\t\tuserId,\n\t\t\t\t\t\t\t\tatMinute: window.atMinute,\n\t\t\t\t\t\t\t\tafterMinutes: window.afterMinutes,\n\t\t\t\t\t\t\t\tweekdays,\n\t\t\t\t\t\t\t\trepeat,\n\t\t\t\t\t\t\t\tisActive,\n\t\t\t\t\t\t\t},\n\t\t\t\t\tip: input.actorIp ?? null,\n\t\t\t\t});\n\t\t\t});\n\t\t\trun();\n\n\t\t\tconst saved = read(ruleId);\n\t\t\tif (!saved) {\n\t\t\t\tthrow new Error(`automation rule ${ruleId} disappeared right after the write`);\n\t\t\t}\n\t\t\treturn saved;\n\t\t},\n\n\t\tremove(input: { id: number; actorId: number; actorIp?: string | null; now?: number }): boolean {\n\t\t\tconst current = read(input.id);\n\t\t\tif (!current) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tdeleteRunsOfRule.run(input.id);\n\t\t\t\tdeleteRule.run(input.id);\n\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\tatUtc: now,\n\t\t\t\t\tactorId: input.actorId,\n\t\t\t\t\taction: \"automation_rule.delete\",\n\t\t\t\t\tentity: \"automation_rule\",\n\t\t\t\t\tentityId: input.id,\n\t\t\t\t\tdetail: { kind: current.kind, userId: current.userId, label: current.label },\n\t\t\t\t\tip: input.actorIp ?? null,\n\t\t\t\t});\n\t\t\t});\n\t\t\trun();\n\t\t\treturn true;\n\t\t},\n\n\t\truns(input?: { ruleId?: number; period?: string; limit?: number }): AutomationRunRecord[] {\n\t\t\tconst limit = Math.min(Math.max(input?.limit ?? 50, 1), 500);\n\t\t\tif (input?.ruleId === undefined) {\n\t\t\t\treturn (selectAllRuns.all(limit) as AutomationRunRow[]).map(mapAutomationRunRow);\n\t\t\t}\n\t\t\tconst rows = selectRuns.all(\n\t\t\t\tinput.ruleId,\n\t\t\t\tinput.period ?? null,\n\t\t\t\tinput.period ?? null,\n\t\t\t\tlimit,\n\t\t\t) as AutomationRunRow[];\n\t\t\treturn rows.map(mapAutomationRunRow);\n\t\t},\n\n\t\thasRun(input: { ruleId: number; userId: number; period: string }): boolean {\n\t\t\treturn selectRun.get(input.ruleId, input.userId, input.period) !== undefined;\n\t\t},\n\n\t\trecordRun(input: { ruleId: number; userId: number; period: string; action: string; now?: number }): boolean {\n\t\t\tconst result = insertRun.run(\n\t\t\t\tinput.ruleId,\n\t\t\t\tinput.userId,\n\t\t\t\tinput.period,\n\t\t\t\tinput.now ?? Math.floor(Date.now() / 1000),\n\t\t\t\tinput.action,\n\t\t\t);\n\t\t\treturn result.changes > 0;\n\t\t},\n\n\t\tforgetRun(input: { ruleId: number; userId: number; period: string }): boolean {\n\t\t\treturn deleteRun.run(input.ruleId, input.userId, input.period).changes > 0;\n\t\t},\n\t};\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["_a"]
7
7
  }
@@ -1367,7 +1367,9 @@ function createApi(deps) {
1367
1367
  // the repository validates the selection and the repeat: a wrong entry comes back as a 400
1368
1368
  weekdays: Array.isArray(rule.weekdays) ? rule.weekdays.map(Number) : void 0,
1369
1369
  repeat: optionalString(rule, "repeat"),
1370
- isActive: (_c2 = optionalBoolean(rule, "isActive")) != null ? _c2 : true
1370
+ isActive: (_c2 = optionalBoolean(rule, "isActive")) != null ? _c2 : true,
1371
+ activeFrom: optionalString(rule, "activeFrom"),
1372
+ activeUntil: optionalString(rule, "activeUntil")
1371
1373
  };
1372
1374
  });
1373
1375
  const keep = new Set(wanted.map((rule) => rule.id).filter((value) => value !== void 0));
@@ -1387,6 +1389,8 @@ function createApi(deps) {
1387
1389
  ...rule.weekdays === void 0 ? {} : { weekdays: rule.weekdays },
1388
1390
  ...rule.repeat === void 0 ? {} : { repeat: rule.repeat },
1389
1391
  isActive: rule.isActive,
1392
+ activeFrom: rule.activeFrom,
1393
+ activeUntil: rule.activeUntil,
1390
1394
  ...actor
1391
1395
  })
1392
1396
  );
@@ -1401,7 +1405,7 @@ function createApi(deps) {
1401
1405
  "GET",
1402
1406
  "/automation-rules/runs",
1403
1407
  { permission: "settings.view" },
1404
- () => (0, import_router.json)(200, { runs: automations.runs({ limit: 20 }) })
1408
+ () => (0, import_router.json)(200, { runs: automations.runs({ limit: 5 }) })
1405
1409
  );
1406
1410
  route(
1407
1411
  "PUT",