iobroker.zeiterfassung 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +365 -0
  3. package/admin/i18n/de.json +26 -0
  4. package/admin/i18n/en.json +26 -0
  5. package/admin/i18n/es.json +26 -0
  6. package/admin/i18n/fr.json +26 -0
  7. package/admin/i18n/it.json +26 -0
  8. package/admin/i18n/nl.json +26 -0
  9. package/admin/i18n/pl.json +26 -0
  10. package/admin/i18n/pt.json +26 -0
  11. package/admin/i18n/ru.json +26 -0
  12. package/admin/i18n/uk.json +26 -0
  13. package/admin/i18n/zh-cn.json +26 -0
  14. package/admin/jsonConfig.json +232 -0
  15. package/admin/zeiterfassung.png +0 -0
  16. package/build/lib/adapter/commands.js +140 -0
  17. package/build/lib/adapter/commands.js.map +7 -0
  18. package/build/lib/adapter/presence.js +129 -0
  19. package/build/lib/adapter/presence.js.map +7 -0
  20. package/build/lib/adapter/states.js +229 -0
  21. package/build/lib/adapter/states.js.map +7 -0
  22. package/build/lib/db/database.js +96 -0
  23. package/build/lib/db/database.js.map +7 -0
  24. package/build/lib/db/migrations.js +428 -0
  25. package/build/lib/db/migrations.js.map +7 -0
  26. package/build/lib/db/repositories/absences.js +432 -0
  27. package/build/lib/db/repositories/absences.js.map +7 -0
  28. package/build/lib/db/repositories/audit.js +90 -0
  29. package/build/lib/db/repositories/audit.js.map +7 -0
  30. package/build/lib/db/repositories/entries.js +335 -0
  31. package/build/lib/db/repositories/entries.js.map +7 -0
  32. package/build/lib/db/repositories/holidays.js +150 -0
  33. package/build/lib/db/repositories/holidays.js.map +7 -0
  34. package/build/lib/db/repositories/payouts.js +221 -0
  35. package/build/lib/db/repositories/payouts.js.map +7 -0
  36. package/build/lib/db/repositories/rfid.js +189 -0
  37. package/build/lib/db/repositories/rfid.js.map +7 -0
  38. package/build/lib/db/repositories/rules.js +356 -0
  39. package/build/lib/db/repositories/rules.js.map +7 -0
  40. package/build/lib/db/repositories/settings.js +91 -0
  41. package/build/lib/db/repositories/settings.js.map +7 -0
  42. package/build/lib/db/repositories/terminals.js +243 -0
  43. package/build/lib/db/repositories/terminals.js.map +7 -0
  44. package/build/lib/db/repositories/users.js +486 -0
  45. package/build/lib/db/repositories/users.js.map +7 -0
  46. package/build/lib/db/seed.js +178 -0
  47. package/build/lib/db/seed.js.map +7 -0
  48. package/build/lib/domain/avatar.js +63 -0
  49. package/build/lib/domain/avatar.js.map +7 -0
  50. package/build/lib/domain/breaks.js +56 -0
  51. package/build/lib/domain/breaks.js.map +7 -0
  52. package/build/lib/domain/calculation.js +139 -0
  53. package/build/lib/domain/calculation.js.map +7 -0
  54. package/build/lib/domain/holidays.js +112 -0
  55. package/build/lib/domain/holidays.js.map +7 -0
  56. package/build/lib/domain/punch.js +93 -0
  57. package/build/lib/domain/punch.js.map +7 -0
  58. package/build/lib/domain/target.js +89 -0
  59. package/build/lib/domain/target.js.map +7 -0
  60. package/build/lib/errors.js +56 -0
  61. package/build/lib/errors.js.map +7 -0
  62. package/build/lib/reports/labels.js +347 -0
  63. package/build/lib/reports/labels.js.map +7 -0
  64. package/build/lib/reports/pdf.js +250 -0
  65. package/build/lib/reports/pdf.js.map +7 -0
  66. package/build/lib/reports/types.js +32 -0
  67. package/build/lib/reports/types.js.map +7 -0
  68. package/build/lib/reports/xls.js +206 -0
  69. package/build/lib/reports/xls.js.map +7 -0
  70. package/build/lib/services/aggregation.js +473 -0
  71. package/build/lib/services/aggregation.js.map +7 -0
  72. package/build/lib/services/auth.js +381 -0
  73. package/build/lib/services/auth.js.map +7 -0
  74. package/build/lib/services/backup.js +226 -0
  75. package/build/lib/services/backup.js.map +7 -0
  76. package/build/lib/services/closing.js +106 -0
  77. package/build/lib/services/closing.js.map +7 -0
  78. package/build/lib/services/sessionSecret.js +100 -0
  79. package/build/lib/services/sessionSecret.js.map +7 -0
  80. package/build/lib/services/sync.js +278 -0
  81. package/build/lib/services/sync.js.map +7 -0
  82. package/build/lib/util/time.js +125 -0
  83. package/build/lib/util/time.js.map +7 -0
  84. package/build/lib/web/api.js +2095 -0
  85. package/build/lib/web/api.js.map +7 -0
  86. package/build/lib/web/cookies.js +80 -0
  87. package/build/lib/web/cookies.js.map +7 -0
  88. package/build/lib/web/events.js +50 -0
  89. package/build/lib/web/events.js.map +7 -0
  90. package/build/lib/web/pin-guard.js +86 -0
  91. package/build/lib/web/pin-guard.js.map +7 -0
  92. package/build/lib/web/problem.js +129 -0
  93. package/build/lib/web/problem.js.map +7 -0
  94. package/build/lib/web/rate-limit.js +65 -0
  95. package/build/lib/web/rate-limit.js.map +7 -0
  96. package/build/lib/web/router.js +267 -0
  97. package/build/lib/web/router.js.map +7 -0
  98. package/build/lib/web/server.js +167 -0
  99. package/build/lib/web/server.js.map +7 -0
  100. package/build/lib/web/static.js +151 -0
  101. package/build/lib/web/static.js.map +7 -0
  102. package/build/lib/web/stream.js +135 -0
  103. package/build/lib/web/stream.js.map +7 -0
  104. package/build/main.js +578 -0
  105. package/build/main.js.map +7 -0
  106. package/io-package.json +163 -0
  107. package/package.json +126 -0
  108. package/src-pwa/src/i18n/de.json +225 -0
  109. package/src-pwa/src/i18n/en.json +225 -0
  110. package/src-pwa/src/i18n/es.json +225 -0
  111. package/src-pwa/src/i18n/fr.json +225 -0
  112. package/src-pwa/src/i18n/index.ts +96 -0
  113. package/src-pwa/src/i18n/it.json +225 -0
  114. package/src-pwa/src/i18n/nl.json +225 -0
  115. package/src-pwa/src/i18n/pl.json +225 -0
  116. package/src-pwa/src/i18n/pt.json +225 -0
  117. package/src-pwa/src/i18n/ru.json +225 -0
  118. package/src-pwa/src/i18n/uk.json +225 -0
  119. package/src-pwa/src/i18n/zh-cn.json +225 -0
  120. package/www/assets/index-DgzmXwoy.js +112 -0
  121. package/www/assets/index-DgzmXwoy.js.map +1 -0
  122. package/www/assets/workbox-window.prod.es5-Bd17z0YL.js +3 -0
  123. package/www/assets/workbox-window.prod.es5-Bd17z0YL.js.map +1 -0
  124. package/www/favicon.svg +16 -0
  125. package/www/icon-192.png +0 -0
  126. package/www/icon-512.png +0 -0
  127. package/www/index.html +38 -0
  128. package/www/manifest.webmanifest +1 -0
  129. package/www/person.png +0 -0
  130. package/www/sw.js +3 -0
  131. package/www/sw.js.map +1 -0
  132. package/www/workbox-9c191d2f.js +3 -0
  133. package/www/workbox-9c191d2f.js.map +1 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alex <sadam6752@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,365 @@
1
+ ![Logo](admin/zeiterfassung.png)
2
+
3
+ # ioBroker.zeiterfassung
4
+
5
+ [![NPM version](https://img.shields.io/npm/v/iobroker.zeiterfassung.svg)](https://www.npmjs.com/package/iobroker.zeiterfassung)
6
+ [![Downloads](https://img.shields.io/npm/dm/iobroker.zeiterfassung.svg)](https://www.npmjs.com/package/iobroker.zeiterfassung)
7
+ ![Number of Installations](https://iobroker.live/badges/zeiterfassung-installed.svg)
8
+ ![Current version in stable repository](https://iobroker.live/badges/zeiterfassung-stable.svg)
9
+
10
+ [![NPM](https://nodei.co/npm/iobroker.zeiterfassung.png?downloads=true)](https://nodei.co/npm/iobroker.zeiterfassung/)
11
+
12
+ **Tests:** ![Test and Release](https://github.com/sadam6752-tech/ioBroker.zeiterfassung/workflows/Test%20and%20Release/badge.svg)
13
+
14
+ ## zeiterfassung adapter for ioBroker
15
+
16
+ Time tracking (**clock-in/clock-out**) for ioBroker – self-hosted, multi-user, with an installable web app
17
+ (PWA), a badge/PIN terminal, absence and vacation management, and monthly reports.
18
+
19
+ > **Status: work in progress.** The adapter is implemented and tested: database with migrations, domain logic
20
+ > (time pairs, breaks, target time, overtime models, vacation, holidays), REST API with roles and permissions,
21
+ > web app (PWA) incl. offline queue, badge/PIN terminal, RFID scan, monthly reports (PDF/XLS), live events and
22
+ > backups with a tested restore. Still missing before the first release: the acceptance tests and the publication itself. The package is therefore **not installable from npm**
23
+ > yet and there are no stable states.
24
+
25
+ ## Features
26
+
27
+ Everything in this table is implemented unless it is marked as open. The remaining work is listed under
28
+ [Development](#development).
29
+
30
+ | Area | Content |
31
+ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
32
+ | Punching | Web app (PWA, installable, offline-capable with queued sync), kiosk terminal with badge/PIN, NFC deep links |
33
+ | Users & rights | Multi-user with roles (admin/manager/employee) and a full permission catalogue – all decisions server-side |
34
+ | Working time | Target time from weekly hours / employment level / working days, break rules (graduated, applied per time pair), overtime models (monthly/yearly/cumulative), carryover, rounding for quick punch |
35
+ | Absences & vacation | Absence types with factors, half days, planned vacation preview, holidays incl. movable feasts |
36
+ | Corrections | Administration fixes punches (change, delete, add a single punch or a whole day); every change carries a reason, and the history of a punch (who changed it and why) is shown in the app |
37
+ | Reports | Monthly PDF timesheet, XLS export, statistics, payouts/compensation |
38
+ | ioBroker | Aggregates and events as states (`info.*`, `users.<id>.*`, `global.*`, `event.*`) and `command.*` for automations |
39
+ | Data | SQLite file (WAL) in the adapter's data directory; only aggregates are published as states |
40
+
41
+ ## Requirements
42
+
43
+ - ioBroker with js-controller >= 6.0.11 and **Node.js >= 22** (required by the bundled SQLite driver `better-sqlite3`)
44
+ - HTTPS for the web app (required for PWA/service worker); a reverse proxy with Let's Encrypt is recommended
45
+
46
+ ## Installation
47
+
48
+ Not published yet: the adapter is neither on npm nor in the official repository list. To run the current
49
+ state, build it locally (see [Development](#development)) and start it with the dev server:
50
+
51
+ ```bash
52
+ npm ci && npm run install:pwa && npm run build:pwa && npm run build
53
+ ```
54
+
55
+ Once released:
56
+
57
+ ```bash
58
+ iobroker add zeiterfassung
59
+ ```
60
+
61
+ ## Configuration
62
+
63
+ The adapter is configured in the instance settings:
64
+
65
+ | Setting | Meaning |
66
+ | ----------------------------------- | ------------------------------------------------------------------------------------------------------ |
67
+ | Port | Port of the built-in HTTP server (web app, API, terminal) |
68
+ | Bind address | Interface to listen on (`0.0.0.0` = all) |
69
+ | Instance time zone | Fallback time zone (IANA name), e.g. `Europe/Berlin` |
70
+ | Default language for new users | One of the 11 supported languages |
71
+ | Holiday country | Country used to generate public holidays |
72
+ | Database file | Optional path; empty = adapter data directory |
73
+ | Enable kiosk terminal | Switches the shared badge/PIN terminal on |
74
+ | Trust the reverse proxy | Use `X-Forwarded-*` of a proxy (client address, HTTPS) |
75
+ | Session secret | Secret for CSRF tokens (**encrypted at rest**; empty = generated once and stored next to the database) |
76
+ | Badge link secret (HMAC) | Secret for signed badge/NFC links (**encrypted at rest**) |
77
+ | Session lifetime in minutes | Session TTL |
78
+ | Days users may edit on their own | Retroactive editing window for employees |
79
+ | Round quick punches to minutes | Quick-time rounding (0 = off) |
80
+ | Calculate absences only until today | Future absences are not deducted from the target time |
81
+ | Subtract working time from absences | May convert vacation into overtime |
82
+ | Keep database backups for days | Retention of `VACUUM INTO` backups |
83
+
84
+ Instance settings (editable through `PUT /api/settings`, permission `settings.edit`) complement the
85
+ configuration; `report_font_path` is one of them: the path of a `.ttf`/`.otf` file used for PDF statements.
86
+ It is only needed for languages the built-in PDF fonts cannot display (`ru`, `uk`, `zh-cn`); everything else
87
+ works without an additional file.
88
+
89
+ ## Web interface and API
90
+
91
+ The adapter runs its own HTTP server on the configured port and serves two things from it:
92
+
93
+ | Path | Content |
94
+ | ----------------------- | ----------------------------------------------------------------------------------------------------------------- |
95
+ | `/` and all other paths | the built web app from `www/` (`index.html`, assets; unknown paths fall back to the page for client side routing) |
96
+ | `/api/...` | the REST API (JSON, errors as `application/problem+json`) |
97
+ | `/api/stream` | live events over a WebSocket (session token as query parameter) |
98
+
99
+ The API is deliberately mounted below `/api`, so the web app owns every other path. If no `www/` folder is
100
+ part of the installation (for example while the web app is still being developed), the adapter keeps running
101
+ and only the API is reachable — a log line states which of both applies.
102
+
103
+ Downloads are real files, not JSON: `GET /api/reports/xls?year=&month=` returns the monthly work time
104
+ statement of the caller as an Excel workbook (`.xlsx`) and `GET /api/reports/pdf?year=&month=` the same
105
+ statement as a PDF — both with `content-disposition: attachment`, generated in the language and time zone of
106
+ the employee. The PDF is one page per month with the day table, the totals, the absences and two signature
107
+ lines; for `ru`, `uk` and `zh-cn` a Unicode font has to be configured (`report_font_path`), because the
108
+ built-in PDF fonts only cover Latin-1 — the export refuses such a language with a clear message
109
+ (`report_font_missing`) instead of drawing empty boxes. The web app offers both files as buttons in the month
110
+ view and in the year report, so nobody has to build a URL by hand.
111
+
112
+ The web app itself covers the punch screen, the month calendar, the year report (including the two downloads),
113
+ absences, the offline queue with its conflict view and the profile. Callers holding `user.view` or `backup.run`
114
+ additionally get an **administration** entry in the menu: employees (create, activate/deactivate, badge PIN) and
115
+ database backups (list, retention, "create now").
116
+
117
+ ### Kiosk terminal
118
+
119
+ With **Enable kiosk terminal** switched on, a tablet in the workshop or at the entrance can punch for everybody
120
+ without logging in. Create the device in the administration of the web app (**Administration → Terminals**; the
121
+ API below it is `POST /api/terminals` with the permission `terminal.manage`). The device token is shown **exactly
122
+ once** — copy it right away, because only its hash is stored — and the screen also offers the ready-made address
123
+ for the tablet. Then open it on the device:
124
+
125
+ ```
126
+ http://<adapter host>:<port>/terminal?token=<device token>
127
+ ```
128
+
129
+ The screen keeps the token in the browser, so the URL is only needed for the first start. Afterwards it shows the
130
+ badge field (scan or type, Enter punches), the list of active employees for the name plus PIN punch, the server
131
+ clock and — after every punch — the name, the direction and the figures of that day for eight seconds. While the
132
+ kiosk is switched off the screen says so and nothing else happens; a revoked token sends it back to the setup
133
+ form. For the name/PIN path an employee needs a badge PIN (`POST /api/users/:id/pin`, 4–8 digits), for scanning an
134
+ RFID card id (`rfidCard`).
135
+
136
+ A device stands for one place, so it can be limited to the people that work there: the administration offers the
137
+ employees of the device when it is created and later through the **Employees** button of its row
138
+ (`POST /api/terminals` with `userIds`, `PUT /api/terminals/:id/users`). A device without an assignment shows
139
+ everybody, so the behaviour of an existing installation does not change.
140
+
141
+ **Employees need their PIN** is a switch per device. With it switched on a badge **and** the personal PIN are
142
+ required; with it switched off the badge alone is enough and a name picked from the list punches right away. A
143
+ wrong PIN is refused on both kinds of device, and the lock after too many wrong attempts stays active.
144
+
145
+ The same device serves two screens: `/terminal` is the classic kiosk described above, `/presence` shows the
146
+ employees as tiles with their picture and the state of the day — the screen for “who is at the workplace right
147
+ now”. Both keep the device token in the browser of the tablet.
148
+
149
+ ### Branding
150
+
151
+ The installation can carry its own look: the administration (**Administration → Settings → Company branding**) takes
152
+ a **logo**, a **background picture** and an accent colour and applies them to the login screen, the header and the
153
+ kiosk screens. The pictures are kept with the settings but delivered through their own cacheable routes
154
+ (`GET /api/branding/logo`, `GET /api/branding/background`), so the payload of the settings API stays small.
155
+
156
+ A picture straight from a phone is scaled down in the browser (longest edge 2560 px, JPEG in several quality steps)
157
+ until it fits the 512 KiB the API accepts; the field reports the resulting size. The twelve preset colours are all
158
+ light tones that keep the dark text readable — with a very dark background picture the text is the one thing that
159
+ can become hard to read, so a light picture or a light background colour next to it is the safe choice.
160
+
161
+ ### Reverse proxy and HTTPS
162
+
163
+ HTTPS is required for the service worker (PWA installation), so put nginx or caddy in front of the adapter and
164
+ switch on **Trust the reverse proxy** in the instance settings. The adapter then uses the client address the proxy
165
+ appends to `x-forwarded-for` for the rate limits and the audit trail, and `x-forwarded-proto: https` makes the
166
+ session cookie `Secure`. Without that switch both headers are ignored — every client may send them, so a single
167
+ client could otherwise move itself into another rate limit bucket. Only the hop directly in front is evaluated:
168
+ with `x-forwarded-for: client, proxy` the rightmost entry counts, because the left part is client controlled.
169
+
170
+ The sessions of the web app travel in an `httpOnly` cookie (`SameSite=Lax`, `Secure` behind HTTPS), so a script
171
+ injected into the page cannot read them. The web app itself only keeps the CSRF token and the user in its
172
+ `localStorage`; the session token never leaves the cookie. Sessions that were opened before that switch stored a
173
+ token as well — they keep working and are moved over with the next login. State changing requests additionally
174
+ need the `x-csrf-token` header, which `GET /api/auth/me` hands out for the own session (that is also how a browser
175
+ that only holds the cookie learns it again after a reload). Integration clients keep using the `x-session-token`
176
+ header from the login response; that path needs no CSRF token, because a foreign page cannot equip a request with
177
+ a header of its own. Point the proxy at the whole adapter: the web app, `/api` and the WebSocket `/api/stream`
178
+ live on the same port — the stream accepts the cookie too, so the browser needs no token in the URL.
179
+
180
+ ## States (overview)
181
+
182
+ | State | Type | Role | Purpose |
183
+ | ------------------------------------------------ | ------- | ------------------- | ----------------------------------------- |
184
+ | `zeiterfassung.0.info.connection` | boolean | indicator.connected | adapter/service ready |
185
+ | `zeiterfassung.0.info.lastBackup` | number | value.time | instant of the newest database backup |
186
+ | `zeiterfassung.0.users.<id>.displayName` | string | info.name | name of the employee |
187
+ | `zeiterfassung.0.users.<id>.hasOpenEntry` | boolean | indicator.working | employee is clocked in |
188
+ | `zeiterfassung.0.users.<id>.lastPunch` | number | value.time | instant of the last punch of today |
189
+ | `zeiterfassung.0.users.<id>.todayWorkedMinutes` | number | value | minutes worked today |
190
+ | `zeiterfassung.0.users.<id>.todayBalanceMinutes` | number | value | balance of today in minutes |
191
+ | `zeiterfassung.0.users.<id>.openConflicts` | number | value | punches waiting for a decision |
192
+ | `zeiterfassung.0.commands.punchUserId` | number | value | employee the punch commands apply to |
193
+ | `zeiterfassung.0.commands.punch` | boolean | button | punch in or out |
194
+ | `zeiterfassung.0.commands.quickPunch` | boolean | button | punch with the configured quick rounding |
195
+ | `zeiterfassung.0.commands.closeMonth` | string | text | close a month, value `YYYY-MM` |
196
+ | `zeiterfassung.0.commands.recalc` | string | text | recalculate a period, `YYYY-MM` or `YYYY` |
197
+ | `zeiterfassung.0.commands.backup` | boolean | button | write a database backup |
198
+
199
+ Punch records themselves are **not** mirrored into states – they live in the SQLite database.
200
+
201
+ ## First start
202
+
203
+ The first start creates the database, the roles, the settings — and, when the instance has no administrator
204
+ yet, **one administrator account**, because otherwise nobody could log in:
205
+
206
+ | Setting | Meaning |
207
+ | ------------------------------------------- | -------------------------------------------------------------------------------------- |
208
+ | `Login of the first administrator` | login of that account, default `admin` |
209
+ | `Start password of the first administrator` | password of that account; empty = a random password is written to the adapter log once |
210
+
211
+ The account is created with `must_change_pw`, so the start password opens the door exactly once and the web
212
+ app asks for a new password right after the login. Accounts created later in the admin area start the same way.
213
+
214
+ The instance settings of the ioBroker admin are applied on every start and **win over the values stored in the
215
+ database** — holiday country, time zone (also the time zone of new accounts), default language for new users,
216
+ edit window, quick rounding, session lifetime, backup retention and the absence switches. `PUT /api/settings`
217
+ stays for the keys the admin UI does not offer; an empty field never wipes a stored value. The adapter also
218
+ publishes `info.version`, `info.schemaVersion`, `info.dbSizeBytes` and `info.lastError`.
219
+
220
+ ## Languages
221
+
222
+ The adapter's admin UI, the web app and the generated reports are shipped in **11 languages**
223
+ (the ioBroker standard set):
224
+
225
+ `en` (base and fallback), `de`, `ru`, `pt`, `nl`, `fr`, `it`, `es`, `pl`, `uk`, `zh-cn`
226
+
227
+ - Admin UI: `admin/i18n/<lang>.json`, kept in sync with the `io-package.json` metadata by `translate-adapter`
228
+ - Web app: `src-pwa/src/i18n/<lang>.json` (`i18next`), using the same keys
229
+ - Reports (PDF/XLS) use the language of the respective user; the instance language is the fallback
230
+ - Dates, numbers, currencies and units are formatted with `Intl` and the user's time zone
231
+ - Additional translations are welcome – see [`docs/i18n.md`](docs/i18n.md)
232
+ - The API returns stable error **codes** instead of translated messages; the client translates them
233
+ - This README is maintained in English with a German summary at the end
234
+
235
+ ## Privacy
236
+
237
+ Everything runs on your own ioBroker host: no cloud service, no telemetry. Punch and personal data stay in
238
+ the local SQLite file; access is role-based and corrections are audited.
239
+
240
+ ## Links
241
+
242
+ - Repository: https://github.com/sadam6752-tech/ioBroker.zeiterfassung
243
+ - Issues: https://github.com/sadam6752-tech/ioBroker.zeiterfassung/issues
244
+ - ioBroker forum: https://forum.iobroker.net/
245
+
246
+ ## Development
247
+
248
+ This repository is the adapter itself; the web app is a sub-project:
249
+
250
+ ```
251
+ src/ adapter sources (TypeScript)
252
+ src-pwa/ Progressive Web App (Vite + React + MUI) – built into www/
253
+ src-shared/ types and validation shared by adapter and web app
254
+ admin/ jsonConfig configuration and translations (11 languages)
255
+ test/ package and integration tests (@iobroker/testing)
256
+ docs/ operator and translator guide
257
+ ```
258
+
259
+ | Script | Description |
260
+ | -------------------------- | --------------------------------------------------------------------------- |
261
+ | `npm run build` | Compile the TypeScript sources |
262
+ | `npm run watch` | Compile and watch for changes |
263
+ | `npm run install:pwa` | Install the dependencies of the web app (`src-pwa`, own `node_modules`) |
264
+ | `npm run build:pwa` | Type check and build the web app into `www/` |
265
+ | `npm run dev:pwa` | Vite dev server with `/api` proxied to the running instance |
266
+ | `npm run lint` | ESLint with `@iobroker/eslint-config` (adapter and web app) |
267
+ | `npm run lint:pwa` | ESLint for the web app only |
268
+ | `npm run check` | TypeScript type check (adapter and web app) |
269
+ | `npm run test:ts` | Unit tests for the adapter sources |
270
+ | `npm run test:package` | Validate `package.json` / `io-package.json` |
271
+ | `npm run test:integration` | Adapter startup against a real js-controller (packs `build/` and `www/`) |
272
+ | `npm run translate` | Keep the 11 translation files in sync |
273
+ | `npm run check:i18n` | Verify that all 11 languages are complete |
274
+ | `npm run check:adapter` | Local pre-check of the ioBroker adapter rules (see `docs/adapter-check.md`) |
275
+ | `npm run release` | Create a release (version, changelog, tag) |
276
+ | `dev-server watch` | Run and debug the adapter locally |
277
+
278
+ The web app is built into `www/`, which the adapter serves on its own port (`/` = app, `/api` = REST). Both
279
+ steps are needed for a release:
280
+
281
+ ```bash
282
+ npm run install:pwa
283
+ npm run build:pwa
284
+ npm run build
285
+ ```
286
+
287
+ `node tools/make-pwa-icons.mjs` regenerates the app icons (checked in, no image library required).
288
+
289
+ ### Still open
290
+
291
+ - **Acceptance on real data:** the golden comparison against a full year of real punches (needs the `Data`
292
+ directory of the predecessor system) and the sign-off of the layouts and of the PDF rendering for `ru`, `uk` and
293
+ `zh-cn` (they need a Unicode font through `report_font_path`).
294
+ - **Publication:** npm package (the CI workflow publishes through npm trusted publishing), entry in
295
+ `ioBroker.repositories` and the run of the official adapter checker.
296
+ - **Smaller gaps:** the NFC comfort in the admin area (reading and writing a badge link with a phone); the language
297
+ files of the web app are machine translated and wait for a review by native speakers; a very dark background
298
+ picture can make the light text of the app hard to read.
299
+
300
+ Working rules (see [`CONTRIBUTING.md`](CONTRIBUTING.md)): specification first, then tests, then
301
+ flags must follow the official role rules; secrets only via `encryptedNative`/`protectedNative`.
302
+
303
+ ## Changelog
304
+
305
+ ### **WORK IN PROGRESS**
306
+
307
+ <!--
308
+ Platzhalter für die nächste Version (am Zeilenanfang):
309
+ ### **WORK IN PROGRESS**
310
+ -->
311
+
312
+ ### 0.0.4 (2026-09-16)
313
+
314
+ - (Alex) fix: the unit tests get a generous timeout (30 s), so cleaning up temporary directories cannot fail them on a slow runner
315
+
316
+ ### 0.0.3 (2026-09-16)
317
+
318
+ - (Alex) fix: the integration tests read the version from `package.json`, so a version bump cannot break them
319
+
320
+ ### 0.0.2 (2026-09-16)
321
+
322
+ - (Alex) project scaffolding: adapter skeleton (TypeScript + jsonConfig), 11-language metadata, admin configuration fields, CI workflow (@iobroker/testing, Node 22/24/26), i18n checks
323
+ - (Alex) kiosk terminals: employees per device, optional PIN duty per device, on-screen keypad, presence screen with pictures
324
+ - (Alex) branding: company logo, background picture and accent colour for the web app and the kiosk (scaled down in the browser, delivered through cacheable routes)
325
+ - (Alex) session secret: generated once and stored next to the database when the instance settings do not define one
326
+ - (Alex) fixes from the first field test: large picture uploads (body limit), terminals without PIN duty, integration tests on a free port
327
+
328
+ ### 0.0.1
329
+
330
+ - initial release (not published yet)
331
+
332
+ ## Provenance
333
+
334
+ This project is an independent implementation of the time tracking described in the internal specification.
335
+ Behaviour, calculation rules and data formats follow that specification; **no source code** was taken from any
336
+ other project. The verification record is kept outside this repository.
337
+
338
+ ## German summary
339
+
340
+ A short summary in German (and why the README itself is English-only) is in
341
+ [`docs/kurzfassung-de.md`](docs/kurzfassung-de.md).
342
+
343
+ ## License
344
+
345
+ MIT License
346
+
347
+ Copyright (c) 2026 Alex <sadam6752@gmail.com>
348
+
349
+ Permission is hereby granted, free of charge, to any person obtaining a copy
350
+ of this software and associated documentation files (the "Software"), to deal
351
+ in the Software without restriction, including without limitation the rights
352
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
353
+ copies of the Software, and to permit persons to whom the Software is
354
+ furnished to do so, subject to the following conditions:
355
+
356
+ The above copyright notice and this permission notice shall be included in all
357
+ copies or substantial portions of the Software.
358
+
359
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
360
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
361
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
362
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
363
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
364
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
365
+ SOFTWARE.
@@ -0,0 +1,26 @@
1
+ {
2
+ "Badge link secret (HMAC)": "Geheimnis für Badge-Links (HMAC)",
3
+ "Bind address": "Bind-Adresse",
4
+ "Calculate absences only until today": "Abwesenheiten nur bis heute berechnen",
5
+ "Database file (empty = adapter data directory)": "Datenbankdatei (leer = Datenverzeichnis des Adapters)",
6
+ "Days users may edit on their own": "Tage, die Benutzer selbst bearbeiten dürfen",
7
+ "Default language for new users": "Standardsprache für neue Benutzer",
8
+ "empty = the adapter generates one once and stores it next to the database, so sessions survive restarts": "leer = der Adapter erzeugt einmalig eines und legt es neben der Datenbank ab, damit Sitzungen Neustarts überleben",
9
+ "Enable kiosk terminal": "Kiosk-Terminal aktivieren",
10
+ "General": "Allgemein",
11
+ "Holiday country": "Land für Feiertage",
12
+ "Instance time zone": "Zeitzone der Instanz",
13
+ "Keep database backups for days": "Datenbank-Backups aufbewahren für Tage",
14
+ "Migration and backup": "Migration und Backup",
15
+ "Port": "Port",
16
+ "Round quick punches to minutes (0 = off)": "Schnellstempel auf Minuten runden (0 = aus)",
17
+ "Security": "Sicherheit",
18
+ "Session lifetime in minutes": "Sitzungsdauer in Minuten",
19
+ "Session secret": "Sitzungsgeheimnis",
20
+ "Subtract working time from absences": "Arbeitszeit von Abwesenheiten abziehen",
21
+ "Time rules": "Zeitregeln",
22
+ "zeiterfassung adapter settings": "Adaptereinstellungen für Zeiterfassung",
23
+ "127.0.0.1 = only this machine": "127.0.0.1 = nur dieser Rechner",
24
+ "Trust the reverse proxy (X-Forwarded-*)": "Reverse-Proxy vertrauen (X-Forwarded-*)",
25
+ "only switch this on when a reverse proxy is in front of the adapter": "nur einschalten, wenn ein Reverse-Proxy vor dem Adapter steht"
26
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "zeiterfassung adapter settings": "Adapter settings for zeiterfassung",
3
+ "General": "General",
4
+ "Security": "Security",
5
+ "Time rules": "Time rules",
6
+ "Migration and backup": "Migration and backup",
7
+ "Port": "Port",
8
+ "Bind address": "Bind address",
9
+ "Instance time zone": "Instance time zone",
10
+ "Default language for new users": "Default language for new users",
11
+ "empty = the adapter generates one once and stores it next to the database, so sessions survive restarts": "empty = the adapter generates one once and stores it next to the database, so sessions survive restarts",
12
+ "Holiday country": "Holiday country",
13
+ "Database file (empty = adapter data directory)": "Database file (empty = adapter data directory)",
14
+ "Enable kiosk terminal": "Enable kiosk terminal",
15
+ "Session secret": "Session secret",
16
+ "Badge link secret (HMAC)": "Badge link secret (HMAC)",
17
+ "Session lifetime in minutes": "Session lifetime in minutes",
18
+ "Days users may edit on their own": "Days users may edit on their own",
19
+ "Round quick punches to minutes (0 = off)": "Round quick punches to minutes (0 = off)",
20
+ "Calculate absences only until today": "Calculate absences only until today",
21
+ "Subtract working time from absences": "Subtract working time from absences",
22
+ "Keep database backups for days": "Keep database backups for days",
23
+ "127.0.0.1 = only this machine": "127.0.0.1 = only this machine",
24
+ "Trust the reverse proxy (X-Forwarded-*)": "Trust the reverse proxy (X-Forwarded-*)",
25
+ "only switch this on when a reverse proxy is in front of the adapter": "only switch this on when a reverse proxy is in front of the adapter"
26
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "Badge link secret (HMAC)": "Secreto de enlace de insignia (HMAC)",
3
+ "Bind address": "dirección de enlace",
4
+ "Calculate absences only until today": "Calcular ausencias solo hasta hoy",
5
+ "Database file (empty = adapter data directory)": "Archivo de base de datos (vacío = directorio de datos del adaptador)",
6
+ "Days users may edit on their own": "Días que los usuarios pueden editar por su cuenta",
7
+ "Default language for new users": "Idioma predeterminado para nuevos usuarios",
8
+ "empty = the adapter generates one once and stores it next to the database, so sessions survive restarts": "vacío = el adaptador genera uno una vez y lo guarda junto a la base de datos, para que las sesiones sobrevivan a los reinicios",
9
+ "Enable kiosk terminal": "Habilitar terminal de quiosco",
10
+ "General": "General",
11
+ "Holiday country": "País de vacaciones",
12
+ "Instance time zone": "Zona horaria de la instancia",
13
+ "Keep database backups for days": "Mantenga copias de seguridad de la base de datos durante días",
14
+ "Migration and backup": "Migración y copia de seguridad",
15
+ "Port": "Puerto",
16
+ "Round quick punches to minutes (0 = off)": "Redondear golpes rápidos a minutos (0 = desactivado)",
17
+ "Security": "Seguridad",
18
+ "Session lifetime in minutes": "Duración de la sesión en minutos",
19
+ "Session secret": "Secreto de sesión",
20
+ "Subtract working time from absences": "Restar el tiempo de trabajo de las ausencias",
21
+ "Time rules": "reglas de tiempo",
22
+ "zeiterfassung adapter settings": "Configuración del adaptador para zeiterfassung",
23
+ "127.0.0.1 = only this machine": "127.0.0.1 = solo este equipo",
24
+ "Trust the reverse proxy (X-Forwarded-*)": "Confiar en el proxy inverso (X-Forwarded-*)",
25
+ "only switch this on when a reverse proxy is in front of the adapter": "activar solo si hay un proxy inverso delante del adaptador"
26
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "Badge link secret (HMAC)": "Secret du lien du badge (HMAC)",
3
+ "Bind address": "Lier l'adresse",
4
+ "Calculate absences only until today": "Calculez les absences uniquement jusqu'à aujourd'hui",
5
+ "Database file (empty = adapter data directory)": "Fichier de base de données (vide = répertoire de données de l'adaptateur)",
6
+ "Days users may edit on their own": "Jours pendant lesquels les utilisateurs peuvent modifier eux-mêmes",
7
+ "Default language for new users": "Langue par défaut pour les nouveaux utilisateurs",
8
+ "empty = the adapter generates one once and stores it next to the database, so sessions survive restarts": "vide = l'adaptateur en génère un une fois et le stocke à côté de la base de données, pour que les sessions survivent aux redémarrages",
9
+ "Enable kiosk terminal": "Activer le terminal kiosque",
10
+ "General": "Général",
11
+ "Holiday country": "Pays de vacances",
12
+ "Instance time zone": "Fuseau horaire de l'instance",
13
+ "Keep database backups for days": "Conservez les sauvegardes de la base de données pendant des jours",
14
+ "Migration and backup": "Migration et sauvegarde",
15
+ "Port": "Port",
16
+ "Round quick punches to minutes (0 = off)": "Arrondir les pointages rapides en minutes (0 = désactivé)",
17
+ "Security": "Sécurité",
18
+ "Session lifetime in minutes": "Durée de vie de la session en minutes",
19
+ "Session secret": "Secret de séance",
20
+ "Subtract working time from absences": "Soustraire le temps de travail des absences",
21
+ "Time rules": "Règles de temps",
22
+ "zeiterfassung adapter settings": "Paramètres de l'adaptateur pour zeiterfassung",
23
+ "127.0.0.1 = only this machine": "127.0.0.1 = uniquement cette machine",
24
+ "Trust the reverse proxy (X-Forwarded-*)": "Faire confiance au proxy inverse (X-Forwarded-*)",
25
+ "only switch this on when a reverse proxy is in front of the adapter": "à activer uniquement si un proxy inverse se trouve devant l'adaptateur"
26
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "Badge link secret (HMAC)": "Segreto collegamento badge (HMAC)",
3
+ "Bind address": "Associa l'indirizzo",
4
+ "Calculate absences only until today": "Calcola le assenze solo fino ad oggi",
5
+ "Database file (empty = adapter data directory)": "File di database (vuoto = directory dei dati dell'adattatore)",
6
+ "Days users may edit on their own": "Giorni in cui gli utenti possono modificare autonomamente",
7
+ "Default language for new users": "Lingua predefinita per i nuovi utenti",
8
+ "empty = the adapter generates one once and stores it next to the database, so sessions survive restarts": "vuoto = l'adattatore ne genera uno una volta e lo salva accanto al database, così le sessioni sopravvivono ai riavvii",
9
+ "Enable kiosk terminal": "Abilita il terminale kiosk",
10
+ "General": "Generale",
11
+ "Holiday country": "Paese di vacanza",
12
+ "Instance time zone": "Fuso orario dell'istanza",
13
+ "Keep database backups for days": "Conserva i backup del database per giorni",
14
+ "Migration and backup": "Migrazione e backup",
15
+ "Port": "Porta",
16
+ "Round quick punches to minutes (0 = off)": "Arrotonda i colpi rapidi ai minuti (0 = disattivato)",
17
+ "Security": "Sicurezza",
18
+ "Session lifetime in minutes": "Durata della sessione in minuti",
19
+ "Session secret": "Segreto della sessione",
20
+ "Subtract working time from absences": "Sottrarre l'orario di lavoro dalle assenze",
21
+ "Time rules": "Regole del tempo",
22
+ "zeiterfassung adapter settings": "Impostazioni dell'adattatore per zeiterfassung",
23
+ "127.0.0.1 = only this machine": "127.0.0.1 = solo questa macchina",
24
+ "Trust the reverse proxy (X-Forwarded-*)": "Fidati del proxy inverso (X-Forwarded-*)",
25
+ "only switch this on when a reverse proxy is in front of the adapter": "attivare solo se davanti all'adattatore c'è un proxy inverso"
26
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "Badge link secret (HMAC)": "Badgelinkgeheim (HMAC)",
3
+ "Bind address": "Bind adres",
4
+ "Calculate absences only until today": "Bereken de afwezigheden tot en met vandaag",
5
+ "Database file (empty = adapter data directory)": "Databasebestand (leeg = adaptergegevensmap)",
6
+ "Days users may edit on their own": "Dagen waarop gebruikers zelf kunnen bewerken",
7
+ "Default language for new users": "Standaardtaal voor nieuwe gebruikers",
8
+ "empty = the adapter generates one once and stores it next to the database, so sessions survive restarts": "leeg = de adapter genereert er eenmalig een en bewaart het naast de database, zodat sessies herstarts overleven",
9
+ "Enable kiosk terminal": "Schakel kioskterminal in",
10
+ "General": "Algemeen",
11
+ "Holiday country": "Vakantie land",
12
+ "Instance time zone": "Tijdzone van instantie",
13
+ "Keep database backups for days": "Bewaar databaseback-ups dagenlang",
14
+ "Migration and backup": "Migratie en back-up",
15
+ "Port": "Haven",
16
+ "Round quick punches to minutes (0 = off)": "Snelle stoten afronden op minuten (0 = uit)",
17
+ "Security": "Beveiliging",
18
+ "Session lifetime in minutes": "Sessielevensduur in minuten",
19
+ "Session secret": "Sessiegeheim",
20
+ "Subtract working time from absences": "Trek de werktijd af van de afwezigheden",
21
+ "Time rules": "Tijd regels",
22
+ "zeiterfassung adapter settings": "Adapterinstellingen voor zeiterfassung",
23
+ "127.0.0.1 = only this machine": "127.0.0.1 = alleen deze computer",
24
+ "Trust the reverse proxy (X-Forwarded-*)": "Reverse proxy vertrouwen (X-Forwarded-*)",
25
+ "only switch this on when a reverse proxy is in front of the adapter": "alleen inschakelen als er een reverse proxy voor de adapter staat"
26
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "Badge link secret (HMAC)": "Tajny link do plakietki (HMAC)",
3
+ "Bind address": "Powiąż adres",
4
+ "Calculate absences only until today": "Oblicz nieobecności tylko do dzisiaj",
5
+ "Database file (empty = adapter data directory)": "Plik bazy danych (pusty = katalog danych adaptera)",
6
+ "Days users may edit on their own": "Dni, które użytkownicy mogą edytować samodzielnie",
7
+ "Default language for new users": "Domyślny język dla nowych użytkowników",
8
+ "empty = the adapter generates one once and stores it next to the database, so sessions survive restarts": "puste = adapter wygeneruje raz wartość i zapisze ją obok bazy danych, aby sesje przetrwały restarty",
9
+ "Enable kiosk terminal": "Włącz terminal kiosku",
10
+ "General": "Ogólny",
11
+ "Holiday country": "Wakacyjny kraj",
12
+ "Instance time zone": "Strefa czasowa instancji",
13
+ "Keep database backups for days": "Przechowuj kopie zapasowe bazy danych przez kilka dni",
14
+ "Migration and backup": "Migracja i kopia zapasowa",
15
+ "Port": "Port",
16
+ "Round quick punches to minutes (0 = off)": "Zaokrąglij szybkie uderzenia do minut (0 = wyłączone)",
17
+ "Security": "Bezpieczeństwo",
18
+ "Session lifetime in minutes": "Czas życia sesji w minutach",
19
+ "Session secret": "Sekret sesji",
20
+ "Subtract working time from absences": "Odejmij czas pracy od nieobecności",
21
+ "Time rules": "Zasady czasu",
22
+ "zeiterfassung adapter settings": "Ustawienia adaptera dla zeiterfassung",
23
+ "127.0.0.1 = only this machine": "127.0.0.1 = tylko ten komputer",
24
+ "Trust the reverse proxy (X-Forwarded-*)": "Ufaj odwrotnemu proxy (X-Forwarded-*)",
25
+ "only switch this on when a reverse proxy is in front of the adapter": "włączać tylko, gdy przed adapterem stoi odwrotne proxy"
26
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "Badge link secret (HMAC)": "Segredo do link do crachá (HMAC)",
3
+ "Bind address": "Endereço de vinculação",
4
+ "Calculate absences only until today": "Calcular faltas apenas até hoje",
5
+ "Database file (empty = adapter data directory)": "Arquivo de banco de dados (vazio = diretório de dados do adaptador)",
6
+ "Days users may edit on their own": "Dias em que os usuários podem editar por conta própria",
7
+ "Default language for new users": "Idioma padrão para novos usuários",
8
+ "empty = the adapter generates one once and stores it next to the database, so sessions survive restarts": "vazio = o adaptador gera um uma vez e guarda-o junto à base de dados, para que as sessões sobrevivam aos reinícios",
9
+ "Enable kiosk terminal": "Habilitar terminal de quiosque",
10
+ "General": "Em geral",
11
+ "Holiday country": "País de férias",
12
+ "Instance time zone": "Fuso horário da instância",
13
+ "Keep database backups for days": "Mantenha backups de banco de dados por dias",
14
+ "Migration and backup": "Migração e backup",
15
+ "Port": "Porta",
16
+ "Round quick punches to minutes (0 = off)": "Arredondar socos rápidos para minutos (0 = desativado)",
17
+ "Security": "Segurança",
18
+ "Session lifetime in minutes": "Duração da sessão em minutos",
19
+ "Session secret": "Segredo da sessão",
20
+ "Subtract working time from absences": "Subtraia o tempo de trabalho das ausências",
21
+ "Time rules": "Regras de tempo",
22
+ "zeiterfassung adapter settings": "Configurações do adaptador para zeiterfassung",
23
+ "127.0.0.1 = only this machine": "127.0.0.1 = apenas este computador",
24
+ "Trust the reverse proxy (X-Forwarded-*)": "Confiar no proxy inverso (X-Forwarded-*)",
25
+ "only switch this on when a reverse proxy is in front of the adapter": "ativar apenas quando um proxy inverso está à frente do adaptador"
26
+ }