iobroker.kanban 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,6 +13,8 @@ Kanban board adapter for ioBroker with its **own web server**, live sync, webhoo
13
13
 
14
14
  **Full documentation:** [English](docs/en/README.md) · [Deutsch](docs/de/README.md)
15
15
 
16
+ **Questions, ideas, bug reports:** [GitHub issues](https://github.com/bmueller77/ioBroker.kanban/issues), or the ioBroker forum threads [Kanban board (English)](https://forum.iobroker.net/topic/85031/new-adapter-kanban-board) and [KanBan-Board (deutsch)](https://forum.iobroker.net/topic/85030/test-kanban-board).
17
+
16
18
  ## Installation
17
19
 
18
20
  Install the adapter from the ioBroker admin: *Adapters* → filter for `kanban` → install. Then create an instance `kanban.0` and open the web UI at `http://<host>:8095/`.
@@ -122,16 +124,22 @@ Details: [Security & access control](docs/en/README.md#security--access-control)
122
124
 
123
125
  <!-- Der Platzhalter bleibt stehen. release-script trägt hier die
124
126
  nächste Version ein und ersetzt die Überschrift. -->
127
+ ### 0.3.3 (2026-09-21)
128
+ * (bmueller77) **Everything the adapter says is in English now.** Thirty-five error messages were German, from `title fehlt` to `Karte '…' existiert nicht`. They reach users in the log, as REST responses and as the answer to a `sendTo` call, so a German message left half the ioBroker world guessing. Cron messages are included
129
+ * (bmueller77) **Five state names were German** and are English now: `Card count`, `Overdue cards`, `Overdue cards (list)` and `Assigned open cards`. Since these objects are created only when they are missing, a new name never reached an existing installation. The adapter now corrects the names of its own objects once at startup, so boards that have been running for months are renamed as well
130
+ * (bmueller77) The HTTP status code of the REST API no longer depends on the wording of an error. It used to test the message for the German `existiert nicht` to decide between 404 and 400, which would have broken silently with the translation. An error that stands for something missing now carries its status itself
131
+ * (bmueller77) `reminderDaysBefore` is held to the range 0 to 30 in the code as well, not only in the settings dialog. The configuration can be edited by hand, and a value far outside that range moved the reminder threshold somewhere nobody would find
132
+
125
133
  ### 0.3.2 (2026-09-10)
126
134
  * (bmueller77) **The trash has the sort toggle now**, like every other column. The default stays deletion time, oldest first, so whatever is closest to being removed for good sits on top; sorting by due date, priority or age is available all the same. The broom moved to the left of the toggle so that the sorting sits in the same place in every column
127
- * (bmueller77) Fix: the **focus ring** took the accent colour unchanged. That works in the light theme; in the dark one the same dark tone stood against dark surfaces, measured down to 1.52:1 on a card in the trash, against the 3:1 that is required. The ring colour is computed now: lightened or darkened from the accent colour in five percent steps, only as far as it takes to clear every surface it lands on, so the instance keeps its colour family
128
- * (bmueller77) Fix: below 600 px the card editor, the board manager and the views dialog were 32 px narrower and 48 px shorter than the screen and sat pinned to the top left. Their rules hang on their IDs and beat the element rule in the mobile block
135
+ * (bmueller77) Fix: the **focus ring** took the accent colour unchanged. That works in the light theme; in the dark one the same dark tone stood against dark surfaces and was barely visible. The ring colour is computed now: lightened or darkened from the accent colour, only as far as it takes to stand out against every surface it lands on, so the instance keeps its colour family
136
+ * (bmueller77) Fix: below 600 px the card editor, the board manager and the views dialog did not fill the screen and sat pinned to the top left. Their rules hang on their IDs and beat the element rule in the mobile block
129
137
  * (bmueller77) Fix: in the same block the tab panel was squashed as a flex child while its content visibly ran out of it. The scroll area did not count the overflowing pixels, and on a short window **Save scrolled out of sight entirely** - anyone who did not think to scroll back lost their input
130
- * (bmueller77) Fix: the text colour on avatars and chips hung on a fixed brightness threshold of 0.31. The point where black and white give the same contrast is 0.179, and in between the choice came out systematically wrong: on turquoise, white scored 3.0:1 where black would have reached 7.0:1. The calculation itself now stands there instead of a number
138
+ * (bmueller77) Fix: the text colour on avatars and chips hung on a fixed brightness threshold, and around that threshold the choice came out wrong - on turquoise it picked white where black is the more legible of the two. It now compares both and takes the better one
131
139
  * (bmueller77) Fix: the **browser's back button** closed the card editor without a word, while Escape and the close cross ask first. The open dialog now adds a history entry; the back button takes it away and raises the same prompt
132
140
  * (bmueller77) Fix: if someone changed the same card in another window, the open editor did not notice and **overwrote it silently on save**. It now compares the state it opened with against the current one and asks when they differ. Only the fields the editor edits are compared, so a column change or a fresh completion timestamp does not raise the prompt
133
- * (bmueller77) **Hit areas below 24 px**: the eye and the broom in the column header measured 22 x 18, the drag handles in the settings 18 x 18, the "+X more" row was 20 px high. All four are on 24 px now, per WCAG 2.5.8. It weighs more on the broom than elsewhere, since an irreversible action sits behind it
134
- * (bmueller77) Fix: the sort menu carried no roles, the tooltip on a completed card was the only single-line one, a badge without a warning colour stood at 1.21:1 against the white card and now carries a hairline, the disabled menu entry kept the pointing hand, the triangle was lost on a coloured badge, the focus landed on `body` after toggling "+X more", and in drag-handle mode the whole card carried the grab cursor while dragging only starts at the handle
141
+ * (bmueller77) The eye and the broom in the column header, the drag handles in the settings and the "+X more" row were too small to hit reliably, on a touchscreen in particular. All four are bigger now. It weighs more on the broom than elsewhere, since an irreversible action sits behind it
142
+ * (bmueller77) Fix: the sort menu carried no roles, the tooltip on a completed card was the only single-line one, a badge without a warning colour was hard to make out against the white card and now carries a hairline, the disabled menu entry kept the pointing hand, the triangle was lost on a coloured badge, the focus landed on `body` after toggling "+X more", and in drag-handle mode the whole card carried the grab cursor while dragging only starts at the handle
135
143
  * (bmueller77) Fix: the explanatory line in the due-date tooltip told the timeline backwards. It runs from yellow to red now, in eleven languages
136
144
  * (bmueller77) Fix: the heading above the label switch always read "hide labels", even when it was set to "show only these labels". Fix: the prompt for removing a board member counted the cards correctly but named every future non-member instead of the people actually on those cards
137
145
  * (bmueller77) Fix: the subject "card deleted" stood for a move to the trash. The checkbox says "to the trash" and so does the confirmation dialog, only the mail did not. Corrected in eleven languages
@@ -139,21 +147,21 @@ Details: [Security & access control](docs/en/README.md#security--access-control)
139
147
  * (bmueller77) **Only IDs that something hangs on are frozen now.** Until now the adapter wrote the marker back for every user on the first start, and every such write restarts the instance - exactly the window in which saving in the admin was lost, reproduced twice, without a message and without a log entry. It hit the setup, the moment when someone sets the port, saves, enters users, saves. A fresh instance with users but no cards no longer writes its configuration back at all
140
148
  * (bmueller77) Fix: creating a card fired "card created" and "card assigned", and **both attached the `.ics`** although the manual says the invite comes once. The notifier remembers the dispatch itself now
141
149
  * (bmueller77) **Deleting a card sends a cancellation**: an `.ics` with `METHOD:CANCEL`, the same `UID` and the next sequence number, so the calendar clears the entry instead of leaving the appointment standing. Only where an invite was ever sent
142
- * (bmueller77) The **add button** at the foot of a column is narrower. At 25 percent column width it covered 111 of 423 px and caught the clicks on two rows of an expanded checklist. A "+" does not need that width
150
+ * (bmueller77) The **add button** at the foot of a column is narrower. It used to span a good quarter of the column and caught the clicks on the rows of an expanded checklist below it. A "+" does not need that width
143
151
  * (bmueller77) **No cards are created in a done column any more.** A card created there carried no `doneAt` and therefore dropped out of three documented features: the "age in column" sort, the display limit for done cards and the automatic cleanup. It stayed there forever without anyone seeing why. The store refuses it and the UI does not offer it: the card editor leaves done columns out when creating and copying, the transfer dialog likewise, and there is no "+" at the foot of such a column. Moving and editing an existing card keeps the column selectable
144
152
  * (bmueller77) Fix: `doneAt` was cleared when a card moved to the trash, so restoring it lost the completion time. Fix: labels created through the interface were all green and now take their colour in turn from the same palette as the UI. Fix: the mirror states of deleted users stayed behind under `users.*`, so a dashboard reading `assignedCount` kept counting people too many. Fix: the "+" at the foot of a column was not blocked when no user existed - only the header button had been secured, the dead end was still reachable by the other route
145
153
  * (bmueller77) **Error messages no longer arrive as a browser box.** All eight `alert()` calls go through the in-page hint now: a red border, `role=alert`, and they stay until someone closes them. A modal box halts the page until it is clicked away and is unreadable to any automated check - two testers could only report that the renderer refused scripts and had to leave the wording open
146
- * (bmueller77) Fix: on a board without columns "+ Card" stayed active, the editor opened, the column field was empty, and only saving ran into the error. Fix: the menus in the column header hang off `body`, so a deleted column left its menu standing and operable. Fix: with fifteen users the chip bar was 1750 px long and pushed the whole page out of a 1584 px window; it wraps now. Fix: markdown tables in the description and the reading window had neither borders nor padding
154
+ * (bmueller77) Fix: on a board without columns "+ Card" stayed active, the editor opened, the column field was empty, and only saving ran into the error. Fix: the menus in the column header hang off `body`, so a deleted column left its menu standing and operable. Fix: with fifteen users the chip bar grew wider than the window and pushed the whole page out of view; it wraps now. Fix: markdown tables in the description and the reading window had neither borders nor padding
147
155
  * (bmueller77) The socket around the sticky add button is one pixel wide instead of seven. It only shows when the button floats over a card, and then as a heavy frame
148
156
  * (bmueller77) Fix: a saved card that the active filter hides straight away **disappeared without a word**. A tester created six cards and thought saving was broken until the network response 201 told him otherwise. After saving, the board is checked for the card and the UI says so when it is not there
149
157
  * (bmueller77) Fix: from the sixth column on, "+ Add column" sat on the edge of the scroll area, half cut off and without effect, and the input that followed was lost. Fix: the recurrence prefilled day and month with today's date even when the card carried a due date. Fix: the remembered expanded state silently overruled a freshly set display limit, so "Max 3" showed seven cards and looked broken. Fix: the board picker still showed the old name after renaming
150
- * (bmueller77) Fix: the "duration" field took the whole width next to the checkbox, measured 850 px for a value like `01:00`. Fix: the summary of a collapsed section showed raw markdown for the description and only the host for the link, so two cards with different files on the same server looked identical. Fix: the label summary carried no colour dots although the manual describes them. Fix: the interface error texts wrote "zustaendige" without the umlaut
158
+ * (bmueller77) Fix: the "duration" field took the whole width next to the checkbox, for a value like `01:00`. Fix: the summary of a collapsed section showed raw markdown for the description and only the host for the link, so two cards with different files on the same server looked identical. Fix: the label summary carried no colour dots although the manual describes them. Fix: the interface error texts wrote "zustaendige" without the umlaut
151
159
  * (bmueller77) Fix: with the WIP limit exceeded and a filter active, the column header read "2/5" in red - the filtered figure before the slash, the colour from the unfiltered one. The column's own figure stands there now, and the tooltip still names how many the filter shows. The rule sat in the middle of the rendering and had been wrong twice for that reason; it lives in `totalLabel()` with five tests beside it
152
160
  * (bmueller77) Fix: the plural form still read "card(s)", "tarjeta(s)" in the prompt for removing a board member. Written out in all eleven languages now. Fix: "+ New" in the card editor always assigned green - the palette had only reached the board manager
153
161
  * (bmueller77) **A fresh instance no longer ships with two example users.** Since the adapter freezes user IDs, the presets were permanently called "user1" and "user2" and could only be deleted, not renamed, so the first thing to do on a new instance was to throw away two users whose names must not be touched. "+ Card" explains by itself that a user is missing first
154
162
  * (bmueller77) Fix: a recurring card left lying for several intervals produced a follow-up card **dated in the past**. Due 28.08., interval 7, completed 07.09. gave 04.09. The calculation now starts from the later of due date and today. Finishing early keeps the calendar as the reference, otherwise the grid would creep forward with every early tick
155
163
  * (bmueller77) Fix: without a single user the card editor opened and saving failed on a condition nobody could meet there. Fix: the reason on the blocked "+ Card" never appeared, because Chrome delivers no mouse events on disabled buttons. It uses `aria-disabled` now, so the click is caught and says what is missing
156
- * (bmueller77) Fix: the column count and the sort button in the column header measured 46x18 and 22x18 px and are on 24 px minimum height now. Fix: "+X more" hung on the column while the add button hung in the card list, so the hint sat below the add button with 40 px to the last card instead of 8, and 4 px further left than the cards
164
+ * (bmueller77) Fix: the column count and the sort button in the column header were too small to hit reliably and are bigger now. Fix: "+X more" hung on the column while the add button hung in the card list, so the hint sat below the add button, set off from the cards and not lined up with them
157
165
  * (bmueller77) Fix: `info.orphanedAssignees` was only written on start, so a repair or a restore from the trash left the state stale until the next start. Fix: `info.frozenUserIds` only ever grew, so a user created later with the same ID would have found the field locked from the outset. Fix: `info.apiSecret` was created on fresh instances after all, although the manual has said the opposite since 0.3.0
158
166
  * (bmueller77) Fix: "setInterval called, but adapter is shutting down" did not come from a normal restart but from saving the settings: the host stops the instance mid-start while the start is still creating timers. Server, scheduler and the delayed write ask first now
159
167
  * (bmueller77) Fix: "2 Karte(n)" in the prompt for removing a board member. Fix: the tooltip on the due date named only the state; the scale stands below it now. Fix: after "+ Add column" and "+ Add label" the focus stayed on the link. Fix: the links in the rendered markdown preview sat in the tab order although it is display only
@@ -177,12 +185,12 @@ Details: [Security & access control](docs/en/README.md#security--access-control)
177
185
  * (bmueller77) Fix: an unusable date format was accepted without checking and then printed on every card verbatim. A format without a day, month or year token is discarded with a warning in the log
178
186
  * (bmueller77) Fix: Enter inside the checklist saved the whole card and closed the dialog. It creates the next item now; Ctrl and Enter saves from there
179
187
  * (bmueller77) Fix: selection and keyboard focus looked identical on the chips, both a two-pixel ring in the accent colour. The focus ring is dashed now
180
- * (bmueller77) Fix: white on the orange due badge reached 3.08:1, below the 4.5:1 of WCAG AA. The orange is darker (4.59:1). Red as a text colour on a dark surface reached 3.14:1 and now uses a lighter tone through `--danger-fg` (6.85:1)
188
+ * (bmueller77) Fix: white on the orange due badge was hard to read, so the orange is darker now. Red as a text colour on a dark surface got a lighter tone through `--danger-fg`
181
189
  * (bmueller77) Fix: the sticky add-card button sat on top of card titles and labels while scrolling. It has an opaque pad in the column colour now
182
- * (bmueller77) Fix: below 600 px the board row in the settings stretched the board picker and the name field to 140 px, because the column direction turned a `flex-basis` meant as width into a height
183
- * (bmueller77) The due-date badge has a tooltip naming its state, since a colour on its own does not say whether it is worse than the one beside it. New labels take their colour from a palette in rotation. The pencil and the other card icons have a 24 px hit area. "+ Card" is disabled while no board exists, and the filter button that has been dead since 0.3.0 is gone
190
+ * (bmueller77) Fix: below 600 px the board row in the settings stretched the board picker and the name field far beyond their content, because the column direction turned a `flex-basis` meant as width into a height
191
+ * (bmueller77) The due-date badge has a tooltip naming its state, since a colour on its own does not say whether it is worse than the one beside it. New labels take their colour from a palette in rotation. The pencil and the other card icons are easier to hit. "+ Card" is disabled while no board exists, and the filter button that has been dead since 0.3.0 is gone
184
192
  * (bmueller77) **With exactly one user, assignment disappears** (#35). No chips in the header, no avatars on the cards, no assignees field in the editor, no user picker in the views dialog: with one person there is nothing to choose and nothing to filter. The API fills the field in instead of answering `400`, so a script does not fail on a question that answers itself. A card carrying an ID that no longer exists keeps it rather than being silently moved. Adding a second user brings all of it back
185
- * (bmueller77) The copy button on a done card is `mdi:content-copy` now, two offset sheets (#33). It used to be a sheet with a circular arrow, which at 14 px looked like the recurrence icon sitting on the same card, so people read the copy button as a recurrence marker that had moved. The recurrence icon itself never moved. The documentation gained a table listing every icon that can appear on a card
193
+ * (bmueller77) The copy button on a done card is `mdi:content-copy` now, two offset sheets (#33). It used to be a sheet with a circular arrow, which at that size looked like the recurrence icon sitting on the same card, so people read the copy button as a recurrence marker that had moved. The recurrence icon itself never moved. The documentation gained a table listing every icon that can appear on a card
186
194
  * (bmueller77) Labels keep one order everywhere (#32). A card used to list them in the order they had been clicked in the editor, so two cards carrying the same labels looked different. Card, picker and section summary now all follow the order of the board, and that order can be dragged by a handle in the board settings, the same way the columns already worked
187
195
  * (bmueller77) Fix: `overdueCount` and `overdueList` ignored the time of day (#31). A card due today at 09:00 turned red in the board at 09:01, while the states kept their old value until midnight, because the counters compared dates only. One function now decides it for the states, the `cardDue` event and the colour alike
188
196
  * (bmueller77) **The column header can show more than the card count.** Clicking the number opens a menu with four checkmarks: total, tomorrow, today, overdue. Each ticked entry gets its own badge next to the others, in the same colours the due-date badges use on the cards and following the same arithmetic, lead time and time of day included. The choice is stored per column and per device, like the sort mode. Done columns and the trash have no menu, since every card there counts as completed
package/io-package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "kanban",
4
- "version": "0.3.2",
4
+ "version": "0.3.3",
5
5
  "titleLang": {
6
6
  "en": "Kanban Board",
7
7
  "de": "Kanban Board",
@@ -74,6 +74,19 @@
74
74
  "license": "MIT"
75
75
  },
76
76
  "news": {
77
+ "0.3.3": {
78
+ "de": "- Alle Meldungen des Adapters sind jetzt auf Englisch\n- Fünf Zustandsnamen waren deutsch und heißen jetzt \"Card count\", \"Overdue cards\", \"Overdue cards (list)\" und \"Assigned open cards\"; bestehende Installationen werden beim Start umbenannt\n- Der HTTP-Statuscode der REST-Schnittstelle hängt nicht mehr am Wortlaut der Fehlermeldung\n- \"reminderDaysBefore\" wird auch im Code auf 0 bis 30 begrenzt",
79
+ "en": "- Every message the adapter produces is in English now\n- Five state names were German and are now \"Card count\", \"Overdue cards\", \"Overdue cards (list)\" and \"Assigned open cards\"; existing installations are renamed at startup\n- The HTTP status code of the REST API no longer depends on the wording of the error message\n- \"reminderDaysBefore\" is held to the range 0 to 30 in the code as well",
80
+ "ru": "- Все сообщения адаптера теперь на английском языке\n- Пять имён состояний были на немецком, теперь это \"Card count\", \"Overdue cards\", \"Overdue cards (list)\" и \"Assigned open cards\"; существующие установки переименовываются при запуске\n- Код состояния HTTP в REST-интерфейсе больше не зависит от формулировки сообщения об ошибке\n- \"reminderDaysBefore\" ограничивается диапазоном от 0 до 30 и в коде",
81
+ "pt": "- Todas as mensagens do adaptador estão agora em inglês\n- Cinco nomes de estados estavam em alemão e são agora \"Card count\", \"Overdue cards\", \"Overdue cards (list)\" e \"Assigned open cards\"; as instalações existentes são renomeadas no arranque\n- O código de estado HTTP da API REST já não depende da redação da mensagem de erro\n- \"reminderDaysBefore\" é limitado ao intervalo de 0 a 30 também no código",
82
+ "nl": "- Alle meldingen van de adapter zijn nu in het Engels\n- Vijf toestandsnamen waren Duits en heten nu \"Card count\", \"Overdue cards\", \"Overdue cards (list)\" en \"Assigned open cards\"; bestaande installaties worden bij het starten hernoemd\n- De HTTP-statuscode van de REST-API hangt niet meer af van de formulering van de foutmelding\n- \"reminderDaysBefore\" wordt ook in de code begrensd op 0 tot 30",
83
+ "fr": "- Tous les messages de l'adaptateur sont désormais en anglais\n- Cinq noms d'états étaient en allemand et s'appellent maintenant \"Card count\", \"Overdue cards\", \"Overdue cards (list)\" et \"Assigned open cards\" ; les installations existantes sont renommées au démarrage\n- Le code de statut HTTP de l'API REST ne dépend plus de la formulation du message d'erreur\n- \"reminderDaysBefore\" est limité à la plage de 0 à 30 également dans le code",
84
+ "it": "- Tutti i messaggi dell'adattatore sono ora in inglese\n- Cinque nomi di stato erano in tedesco e ora sono \"Card count\", \"Overdue cards\", \"Overdue cards (list)\" e \"Assigned open cards\"; le installazioni esistenti vengono rinominate all'avvio\n- Il codice di stato HTTP dell'API REST non dipende più dal testo del messaggio di errore\n- \"reminderDaysBefore\" è limitato all'intervallo da 0 a 30 anche nel codice",
85
+ "es": "- Todos los mensajes del adaptador están ahora en inglés\n- Cinco nombres de estados estaban en alemán y ahora son \"Card count\", \"Overdue cards\", \"Overdue cards (list)\" y \"Assigned open cards\"; las instalaciones existentes se renombran al iniciar\n- El código de estado HTTP de la API REST ya no depende de la redacción del mensaje de error\n- \"reminderDaysBefore\" se limita al rango de 0 a 30 también en el código",
86
+ "pl": "- Wszystkie komunikaty adaptera są teraz w języku angielskim\n- Pięć nazw stanów było po niemiecku, teraz to \"Card count\", \"Overdue cards\", \"Overdue cards (list)\" i \"Assigned open cards\"; istniejące instalacje są zmieniane przy starcie\n- Kod stanu HTTP interfejsu REST nie zależy już od treści komunikatu o błędzie\n- \"reminderDaysBefore\" jest ograniczany do zakresu od 0 do 30 również w kodzie",
87
+ "uk": "- Усі повідомлення адаптера тепер англійською мовою\n- П'ять імен станів були німецькими, тепер це \"Card count\", \"Overdue cards\", \"Overdue cards (list)\" та \"Assigned open cards\"; наявні встановлення перейменовуються під час запуску\n- Код стану HTTP інтерфейсу REST більше не залежить від формулювання повідомлення про помилку\n- \"reminderDaysBefore\" обмежується діапазоном від 0 до 30 і в коді",
88
+ "zh-cn": "- 适配器的所有消息现在均为英文\n- 五个状态名称原为德语,现为 \"Card count\"、\"Overdue cards\"、\"Overdue cards (list)\" 和 \"Assigned open cards\";现有安装在启动时会被重命名\n- REST 接口的 HTTP 状态码不再取决于错误消息的措辞\n- \"reminderDaysBefore\" 在代码中同样被限制在 0 到 30 之间"
89
+ },
77
90
  "0.3.2": {
78
91
  "de": "- Zahlen im Spaltenkopf umschaltbar: Gesamt, Morgen, Heute, Ueberfaellig\n- Aufklappbare Abschnitte im Karteneditor mit Zusammenfassung in der Kopfzeile\n- Benutzer-IDs werden gesperrt, sobald eine Karte oder ein Avatarbild daran haengt; verwaiste Zustaendige lassen sich in der Oberflaeche reparieren\n- Neue Wiederholungsart \"Alle X Tage (nach Erledigung des Vorgaengers)\"\n- Gelb heisst jetzt genau der naechste Kalendertag; die Vorlaufzeit steuert nur noch die Erinnerungsmail\n- In Erledigt-Spalten entstehen keine Karten mehr\n- Beim Loeschen einer Karte mit Einladung geht eine Kalender-Absage raus\n- Der Papierkorb hat denselben Sortier-Umschalter wie jede andere Spalte\n- Frische Instanzen bringen keine Beispielbenutzer mehr mit\n- Dazu rund siebzig Korrekturen aus acht Testpaketen",
79
92
  "en": "- Switchable counts in the column header: total, tomorrow, today, overdue\n- Collapsible sections in the card editor, each summarised in its header\n- User IDs are frozen once a card or an avatar hangs on them; orphaned assignees can be repaired in the UI\n- New recurrence kind \"every X days (after the previous one is done)\"\n- Yellow now means exactly the next calendar day; the lead time only drives the reminder mail\n- No cards are created in done columns any more\n- Deleting a card with an invite sends a calendar cancellation\n- The trash has the same sort toggle as every other column\n- Fresh instances no longer ship with example users\n- Plus around seventy fixes from eight test packages",
package/lib/cron.js CHANGED
@@ -76,7 +76,7 @@ function parseField(raw, field) {
76
76
  if (stepPart !== undefined) {
77
77
  step = Number(stepPart);
78
78
  if (!Number.isInteger(step) || step < 1) {
79
- throw new Error(`${field.name}: ungültige Schrittweite '${stepPart}'`);
79
+ throw new Error(`${field.name}: invalid step '${stepPart}'`);
80
80
  }
81
81
  }
82
82
 
@@ -104,10 +104,10 @@ function parseField(raw, field) {
104
104
  }
105
105
 
106
106
  if (!Number.isInteger(from) || !Number.isInteger(to)) {
107
- throw new Error(`${field.name}: '${piece}' ist keine gültige Angabe`);
107
+ throw new Error(`${field.name}: '${piece}' is not a valid entry`);
108
108
  }
109
109
  if (from < field.min || to > field.max || from > to) {
110
- throw new Error(`${field.name}: '${piece}' liegt außerhalb von ${field.min}-${field.max}`);
110
+ throw new Error(`${field.name}: '${piece}' is outside ${field.min}-${field.max}`);
111
111
  }
112
112
  for (let v = from; v <= to; v += step) {
113
113
  out.add(v);
@@ -137,7 +137,7 @@ function parseCron(expr) {
137
137
  // Ausdruck ueber die token-freie Pruefroute den Event-Loop fuer Sekunden.
138
138
  // Das laengste sinnvolle Muster (alle Monatsnamen) bleibt weit darunter.
139
139
  if (raw.length > MAX_EXPR_LENGTH) {
140
- throw new Error(`Ausdruck zu lang (höchstens ${MAX_EXPR_LENGTH} Zeichen)`);
140
+ throw new Error(`expression too long (at most ${MAX_EXPR_LENGTH} characters)`);
141
141
  }
142
142
  const parts = raw.split(/\s+/);
143
143
  if (parts.length !== 5) {
package/lib/scheduler.js CHANGED
@@ -146,7 +146,10 @@ class Scheduler {
146
146
  return;
147
147
  }
148
148
  }
149
- const daysBefore = Number(cfg.reminderDaysBefore) || 0;
149
+ // Die Grenze 0..30 steht in der jsonConfig, aber die Konfiguration laesst
150
+ // sich von Hand aendern. Ein absurder Wert verschoebe die Schwelle ins
151
+ // Nirgendwo, und niemand saehe den Grund.
152
+ const daysBefore = Math.min(Math.max(Math.floor(Number(cfg.reminderDaysBefore) || 0), 0), 30);
150
153
  const threshold = new Date();
151
154
  threshold.setDate(threshold.getDate() + daysBefore);
152
155
  const thresholdStr = `${threshold.getFullYear()}-${String(threshold.getMonth() + 1).padStart(2, '0')}-${String(threshold.getDate()).padStart(2, '0')}`;
package/lib/server.js CHANGED
@@ -342,7 +342,7 @@ class Server {
342
342
  _apiRoutes(app) {
343
343
  const wrap = fn => (req, res) => {
344
344
  Promise.resolve(fn(req, res)).catch(e => {
345
- const code = /existiert nicht/.test(e.message) ? 404 : 400;
345
+ const code = Number(e.status) || 400;
346
346
  res.status(code).json({ error: e.message });
347
347
  });
348
348
  };
@@ -359,7 +359,7 @@ class Server {
359
359
  timeFormat: cfg.timeFormat || '24h',
360
360
  // Die Oberflaeche faerbt das Vorwarnfenster damit ein, damit
361
361
  // Farbe und Erinnerungsmail dieselbe Aussage treffen
362
- reminderDaysBefore: Number(cfg.reminderDaysBefore) || 0,
362
+ reminderDaysBefore: Math.min(Math.max(Math.floor(Number(cfg.reminderDaysBefore) || 0), 0), 30),
363
363
  language: this.adapter._language || 'en',
364
364
  trashRetentionDays: TRASH_RETENTION_DAYS,
365
365
  });
@@ -770,7 +770,7 @@ class Server {
770
770
 
771
771
  const wrap = fn => (req, res) => {
772
772
  Promise.resolve(fn(req, res)).catch(e => {
773
- const code = /existiert nicht/.test(e.message) ? 404 : 400;
773
+ const code = Number(e.status) || 400;
774
774
  res.status(code).json({ error: e.message });
775
775
  });
776
776
  };
package/lib/store.js CHANGED
@@ -355,7 +355,7 @@ function assertDate(v, field) {
355
355
  }
356
356
  const s = String(v);
357
357
  if (!DATE_RE.test(s)) {
358
- throw new Error(`${field} muss im Format YYYY-MM-DD vorliegen, nicht '${s}'`);
358
+ throw new Error(`${field} must be in YYYY-MM-DD format, not '${s}'`);
359
359
  }
360
360
  return s;
361
361
  }
@@ -365,7 +365,7 @@ function assertTime(v, field) {
365
365
  }
366
366
  const s = String(v);
367
367
  if (!TIME_RE.test(s)) {
368
- throw new Error(`${field} muss im Format HH:MM vorliegen, nicht '${s}'`);
368
+ throw new Error(`${field} must be in HH:MM format, not '${s}'`);
369
369
  }
370
370
  return s;
371
371
  }
@@ -385,17 +385,32 @@ function assertRecurrence(rec) {
385
385
  }
386
386
  const check = validateCron(rec.cron);
387
387
  if (!check.ok) {
388
- throw new Error(`Cron-Ausdruck ungültig: ${check.error}`);
388
+ throw new Error(`Invalid cron expression: ${check.error}`);
389
389
  }
390
390
  return check.time;
391
391
  }
392
+ /**
393
+ * Fehler mit HTTP-Statuscode. Der REST-Aufsatz hat den Code bisher aus dem Text
394
+ * der Meldung geraten (/existiert nicht/). Das haette die Uebersetzung ins
395
+ * Englische stillschweigend gebrochen und war ohnehin die falsche Ebene: Ob
396
+ * etwas nicht existiert, weiss die Datenhaltung, nicht der Aufsatz.
397
+ *
398
+ * @param message Meldung in englischer Sprache
399
+ * @returns Error mit gesetztem status 404
400
+ */
401
+ function notFound(message) {
402
+ const e = new Error(message);
403
+ e.status = 404;
404
+ return e;
405
+ }
406
+
392
407
  function assertPriority(v) {
393
408
  if (v === undefined || v === null || v === '') {
394
409
  return 0;
395
410
  }
396
411
  const n = Number(v);
397
412
  if (!Number.isInteger(n) || n < 0 || n > 2) {
398
- throw new Error(`priority kennt nur 0, 1 oder 2, nicht '${v}'`);
413
+ throw new Error(`priority accepts only 0, 1 or 2, not '${v}'`);
399
414
  }
400
415
  return n;
401
416
  }
@@ -553,7 +568,7 @@ class Store {
553
568
  _requireBoard(id) {
554
569
  const b = this.boards.get(id);
555
570
  if (!b) {
556
- throw new Error(`Board '${id}' existiert nicht`);
571
+ throw notFound(`board '${id}' does not exist`);
557
572
  }
558
573
  return b;
559
574
  }
@@ -588,7 +603,7 @@ class Store {
588
603
 
589
604
  async createBoard({ id, title }) {
590
605
  if (!title) {
591
- throw new Error('title fehlt');
606
+ throw new Error('title is missing');
592
607
  }
593
608
  let boardId = slugify(id || title);
594
609
  let n = 2;
@@ -733,17 +748,17 @@ class Store {
733
748
  addCard(boardId, data, source) {
734
749
  const board = this._requireBoard(boardId);
735
750
  if (!data || !data.title) {
736
- throw new Error('title fehlt');
751
+ throw new Error('title is missing');
737
752
  }
738
753
  const firstNormal = board.columns.find(c => !c.isTrash);
739
754
  // Eine angegebene, aber unbekannte Spalte ist ein Fehler des Aufrufers — die
740
755
  // Karte still in der ersten Spalte abzulegen verschleiert das nur.
741
756
  if (data.columnId && !board.columns.some(c => c.id === data.columnId && !c.isTrash)) {
742
- throw new Error(`Spalte '${data.columnId}' existiert nicht in Board '${boardId}'`);
757
+ throw notFound(`column '${data.columnId}' does not exist in board '${boardId}'`);
743
758
  }
744
759
  const columnId = data.columnId || (firstNormal && firstNormal.id);
745
760
  if (!columnId) {
746
- throw new Error('Board hat keine Spalten');
761
+ throw new Error('board has no columns');
747
762
  }
748
763
  // In einer Erledigt-Spalte wird nichts angelegt. Karten wandern dorthin,
749
764
  // sie entstehen dort nicht: Was dort neu auftauchte, trug kein doneAt,
@@ -752,8 +767,8 @@ class Store {
752
767
  // immer liegen, ohne dass jemand den Grund sah (D2, E12).
753
768
  if (this._isDoneColumn(board, columnId)) {
754
769
  throw new Error(
755
- 'Karten werden nicht in einer Erledigt-Spalte angelegt. ' +
756
- 'Lege sie in einer offenen Spalte an und verschiebe sie danach.',
770
+ 'cards are not created in a done column. ' +
771
+ 'Create the card in an open column and move it there afterwards.',
757
772
  );
758
773
  }
759
774
  const due = assertDate(data.due, 'due');
@@ -812,7 +827,7 @@ class Store {
812
827
  const board = this._requireBoard(boardId);
813
828
  const card = board.cards.find(c => c.id === cardId);
814
829
  if (!card) {
815
- throw new Error(`Karte '${cardId}' existiert nicht`);
830
+ throw notFound(`card '${cardId}' does not exist`);
816
831
  }
817
832
  const oldAssignees = new Set(card.assignees);
818
833
  const oldDueAt = `${card.due || ''} ${card.dueTime || ''}`;
@@ -878,10 +893,10 @@ class Store {
878
893
  const board = this._requireBoard(boardId);
879
894
  const card = board.cards.find(c => c.id === cardId);
880
895
  if (!card) {
881
- throw new Error(`Karte '${cardId}' existiert nicht`);
896
+ throw notFound(`card '${cardId}' does not exist`);
882
897
  }
883
898
  if (!board.columns.some(c => c.id === columnId)) {
884
- throw new Error(`Spalte '${columnId}' existiert nicht`);
899
+ throw notFound(`column '${columnId}' does not exist`);
885
900
  }
886
901
  const fromColumn = card.columnId;
887
902
  const wasDone = this._isDoneColumn(board, fromColumn);
@@ -1108,7 +1123,7 @@ class Store {
1108
1123
  const board = this._requireBoard(boardId);
1109
1124
  const card = board.cards.find(c => c.id === cardId);
1110
1125
  if (!card) {
1111
- throw new Error(`Karte '${cardId}' existiert nicht`);
1126
+ throw notFound(`card '${cardId}' does not exist`);
1112
1127
  }
1113
1128
  const trash = this._ensureTrashColumn(board);
1114
1129
  const fromColumn = card.columnId;
@@ -1151,13 +1166,13 @@ class Store {
1151
1166
  }
1152
1167
  const idx = board.cards.findIndex(c => c.id === cardId);
1153
1168
  if (idx === -1) {
1154
- throw new Error(`Karte '${cardId}' existiert nicht`);
1169
+ throw notFound(`card '${cardId}' does not exist`);
1155
1170
  }
1156
1171
  // Nur aus dem Papierkorb: Sonst entfernt ein einzelner Aufruf eine aktive
1157
1172
  // Karte unwiederbringlich und umgeht die Aufbewahrungsfrist. Der Aufraeumlauf
1158
1173
  // und "Papierkorb leeren" gehen ohnehin ueber eigene Wege.
1159
1174
  if (!opts.auto && !this._isTrashColumn(board, board.cards[idx].columnId)) {
1160
- throw new Error(`Karte '${cardId}' liegt nicht im Papierkorb`);
1175
+ throw new Error(`card '${cardId}' is not in the trash`);
1161
1176
  }
1162
1177
  const [card] = board.cards.splice(idx, 1);
1163
1178
  const col = card.columnId;
@@ -1184,7 +1199,7 @@ class Store {
1184
1199
  const board = this._requireBoard(boardId);
1185
1200
  const card = board.cards.find(c => c.id === cardId);
1186
1201
  if (!card) {
1187
- throw new Error(`Karte '${cardId}' existiert nicht`);
1202
+ throw notFound(`card '${cardId}' does not exist`);
1188
1203
  }
1189
1204
  if (!this._isTrashColumn(board, card.columnId)) {
1190
1205
  return card;
@@ -1194,7 +1209,7 @@ class Store {
1194
1209
  target = board.columns.find(c => !c.isTrash && !c.isDone) || board.columns.find(c => !c.isTrash);
1195
1210
  }
1196
1211
  if (!target) {
1197
- throw new Error('Keine Zielspalte für die Wiederherstellung');
1212
+ throw new Error('no target column for the restore');
1198
1213
  }
1199
1214
  const fromColumn = card.columnId;
1200
1215
  this._mutate(board, b => {
@@ -1361,18 +1376,18 @@ class Store {
1361
1376
  // eines Boards passiert per Drag & Drop bzw. über die Spaltenauswahl.
1362
1377
  const sameBoard = fromBoardId === toBoardId;
1363
1378
  if (sameBoard && mode !== 'copy') {
1364
- throw new Error('Quell- und Ziel-Board sind identisch');
1379
+ throw new Error('source and target board are identical');
1365
1380
  }
1366
1381
  const card = from.cards.find(c => c.id === cardId);
1367
1382
  if (!card) {
1368
- throw new Error(`Karte '${cardId}' existiert nicht`);
1383
+ throw notFound(`card '${cardId}' does not exist`);
1369
1384
  }
1370
1385
  let target = toColumnId && to.columns.find(c => c.id === toColumnId && !c.isTrash);
1371
1386
  if (!target) {
1372
1387
  target = to.columns.find(c => c.allowAdd && !c.isTrash) || to.columns.find(c => !c.isTrash);
1373
1388
  }
1374
1389
  if (!target) {
1375
- throw new Error('Ziel-Board hat keine Spalte');
1390
+ throw new Error('target board has no column');
1376
1391
  }
1377
1392
  // Labels über den Namen abgleichen
1378
1393
  const labelName = l =>
@@ -1399,7 +1414,7 @@ class Store {
1399
1414
  assignees = overrides.assignees.filter(a => members.has(a));
1400
1415
  }
1401
1416
  if (!assignees.length) {
1402
- throw new Error('Ziel-Board: mindestens eine zuständige Person (Mitglied) nötig');
1417
+ throw new Error('target board: at least one assignee who is a member is required');
1403
1418
  }
1404
1419
  const isDone = !!target.isDone;
1405
1420
  const nowIso = new Date().toISOString();
@@ -1576,6 +1591,51 @@ class Store {
1576
1591
  }
1577
1592
  }
1578
1593
 
1594
+ /**
1595
+ * Zustandsnamen aus alten Installationen nachziehen.
1596
+ *
1597
+ * Die Objekte entstehen ueber setObjectNotExistsAsync. Ein geaenderter Name im
1598
+ * Code erreicht ein bestehendes Objekt deshalb nie: Auf einer Instanz, die es
1599
+ * vor 0.3.3 schon gab, stuenden weiter die deutschen Namen, und genau die
1600
+ * landen im Objekt-Dump. Der Durchlauf laeuft einmal je Start ueber die
1601
+ * eigenen Objekte und kostet einen Lesezugriff.
1602
+ *
1603
+ * @returns Anzahl der umbenannten Objekte
1604
+ */
1605
+ async migrateStateNames() {
1606
+ const NEUE_NAMEN = {
1607
+ 'Anzahl Karten': 'Card count',
1608
+ 'Überfällige Karten': 'Overdue cards',
1609
+ 'Überfällige Karten (Liste)': 'Overdue cards (list)',
1610
+ 'Zugewiesene offene Karten': 'Assigned open cards',
1611
+ };
1612
+ let geaendert = 0;
1613
+ let objekte = null;
1614
+ try {
1615
+ objekte = await this.adapter.getAdapterObjectsAsync();
1616
+ } catch (e) {
1617
+ this.adapter.log.warn(`state name migration skipped: ${e.message}`);
1618
+ return 0;
1619
+ }
1620
+ for (const [id, obj] of Object.entries(objekte || {})) {
1621
+ const name = obj && obj.common && obj.common.name;
1622
+ if (typeof name !== 'string' || !NEUE_NAMEN[name]) {
1623
+ continue;
1624
+ }
1625
+ const kurz = id.startsWith(`${this.adapter.namespace}.`) ? id.slice(this.adapter.namespace.length + 1) : id;
1626
+ try {
1627
+ await this.adapter.extendObjectAsync(kurz, { common: { name: NEUE_NAMEN[name] } });
1628
+ geaendert++;
1629
+ } catch (e) {
1630
+ this.adapter.log.warn(`could not rename '${id}': ${e.message}`);
1631
+ }
1632
+ }
1633
+ if (geaendert) {
1634
+ this.adapter.log.info(`${geaendert} state name(s) translated to English`);
1635
+ }
1636
+ return geaendert;
1637
+ }
1638
+
1579
1639
  async _ensureBoardObjects(boardId, title) {
1580
1640
  await this.adapter.setObjectNotExistsAsync(`boards.${boardId}`, {
1581
1641
  type: 'channel',
@@ -1585,8 +1645,8 @@ class Store {
1585
1645
  const stateDefs = {
1586
1646
  data: { name: 'Board JSON', type: 'string', role: 'json', read: true, write: false },
1587
1647
  rev: { name: 'Revision', type: 'number', role: 'value', read: true, write: false },
1588
- cardCount: { name: 'Anzahl Karten', type: 'number', role: 'value', read: true, write: false },
1589
- overdueCount: { name: 'Überfällige Karten', type: 'number', role: 'value', read: true, write: false },
1648
+ cardCount: { name: 'Card count', type: 'number', role: 'value', read: true, write: false },
1649
+ overdueCount: { name: 'Overdue cards', type: 'number', role: 'value', read: true, write: false },
1590
1650
  };
1591
1651
  for (const [suffix, common] of Object.entries(stateDefs)) {
1592
1652
  await this.adapter.setObjectNotExistsAsync(`boards.${boardId}.${suffix}`, {
@@ -1660,7 +1720,7 @@ class Store {
1660
1720
  if (alle.length === 1) {
1661
1721
  return [alle[0]];
1662
1722
  }
1663
- throw new Error('assignees fehlt - mindestens eine zuständige Person ist Pflicht');
1723
+ throw new Error('assignees is missing - at least one assignee is required');
1664
1724
  }
1665
1725
  const erlaubt = new Set(alle);
1666
1726
  const bekannt = [...erlaubt].sort();
@@ -1669,9 +1729,7 @@ class Store {
1669
1729
  }
1670
1730
  const fremd = [...new Set(list.filter(a => !erlaubt.has(a)))];
1671
1731
  if (fremd.length) {
1672
- throw new Error(
1673
- `unbekannte zuständige Person: ${fremd.join(', ')} - vorhanden sind: ${bekannt.join(', ') || '(keine)'}`,
1674
- );
1732
+ throw new Error(`unknown assignee: ${fremd.join(', ')} - available are: ${bekannt.join(', ') || '(none)'}`);
1675
1733
  }
1676
1734
  return [...new Set(list)];
1677
1735
  }
@@ -1808,7 +1866,7 @@ class Store {
1808
1866
  }
1809
1867
  const known = (this.adapter.config.users || []).map(u => u && u.name).filter(Boolean);
1810
1868
  if (!known.includes(neu)) {
1811
- throw new Error(`Benutzer '${neu}' existiert nicht`);
1869
+ throw notFound(`user '${neu}' does not exist`);
1812
1870
  }
1813
1871
  let cards = 0;
1814
1872
  const boards = new Set();
@@ -2019,14 +2077,14 @@ class Store {
2019
2077
  });
2020
2078
  const defs = {
2021
2079
  assignedCount: {
2022
- name: 'Zugewiesene offene Karten',
2080
+ name: 'Assigned open cards',
2023
2081
  type: 'number',
2024
2082
  role: 'value',
2025
2083
  read: true,
2026
2084
  write: false,
2027
2085
  },
2028
- overdueCount: { name: 'Überfällige Karten', type: 'number', role: 'value', read: true, write: false },
2029
- overdueList: { name: 'Überfällige Karten (Liste)', type: 'string', role: 'json', read: true, write: false },
2086
+ overdueCount: { name: 'Overdue cards', type: 'number', role: 'value', read: true, write: false },
2087
+ overdueList: { name: 'Overdue cards (list)', type: 'string', role: 'json', read: true, write: false },
2030
2088
  };
2031
2089
  for (const [suffix, common] of Object.entries(defs)) {
2032
2090
  await this.adapter.setObjectNotExistsAsync(`users.${name}.${suffix}`, {
@@ -2049,5 +2107,6 @@ module.exports = {
2049
2107
  nextDue,
2050
2108
  recMatches,
2051
2109
  nextCronDates,
2110
+ notFound,
2052
2111
  TRASH_RETENTION_DAYS,
2053
2112
  };
package/main.js CHANGED
@@ -11,7 +11,7 @@ const { cardWithDueAt, boardWithDueAt } = require('./lib/dueat');
11
11
  const { Server } = require('./lib/server');
12
12
 
13
13
  const { freezePlan, prunePlan } = require('./lib/freeze');
14
- const { hasDateToken } = require('./lib/store');
14
+ const { hasDateToken, notFound } = require('./lib/store');
15
15
 
16
16
  // Wie oft der Adapter versucht, das Merkmal `fixed` in die Benutzerliste
17
17
  // zurueckzuschreiben, bevor er aufgibt. Mehr als einer, weil ein Speichern der
@@ -55,6 +55,8 @@ class Kanban extends utils.Adapter {
55
55
  // angelegt und braucht dafür den lokalisierten Namen.
56
56
  await this._resolveLanguage();
57
57
  await this.store.load();
58
+ // Namen aus alten Installationen nachziehen (siehe migrateStateNames).
59
+ await this.store.migrateStateNames();
58
60
  // Erst nach dem Laden: Ob eine Kennung festgeschrieben werden muss,
59
61
  // haengt daran, ob Karten auf sie zeigen, und die kennt der Store
60
62
  // vorher nicht. Schreibt der Adapter dabei seine eigene Konfiguration,
@@ -418,7 +420,7 @@ class Kanban extends utils.Adapter {
418
420
  // Nicht still null liefern: ein Skript, das nur den Statuscode prüft,
419
421
  // hielte das sonst für einen Erfolg.
420
422
  if (!b) {
421
- throw new Error(`Board '${boardId}' existiert nicht`);
423
+ throw notFound(`board '${boardId}' does not exist`);
422
424
  }
423
425
  return board(b);
424
426
  }
@@ -439,11 +441,11 @@ class Kanban extends utils.Adapter {
439
441
  case 'doneCard': {
440
442
  const b = this.store.getBoard(boardId);
441
443
  if (!b) {
442
- throw new Error(`Board '${boardId}' existiert nicht`);
444
+ throw notFound(`board '${boardId}' does not exist`);
443
445
  }
444
446
  const doneCol = b.columns.find(c => c.isDone);
445
447
  if (!doneCol) {
446
- throw new Error(`Board '${boardId}' hat keine Erledigt-Spalte`);
448
+ throw new Error(`board '${boardId}' has no done column`);
447
449
  }
448
450
  return card(this.store.moveCard(boardId, cardId, doneCol.id, undefined, source));
449
451
  }
@@ -472,7 +474,7 @@ class Kanban extends utils.Adapter {
472
474
  ),
473
475
  );
474
476
  default:
475
- throw new Error(`Unbekanntes Kommando '${cmd}'`);
477
+ throw new Error(`unknown command '${cmd}'`);
476
478
  }
477
479
  }
478
480
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.kanban",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Kanban board for ioBroker with webhooks, multi-user support and email notifications",
5
5
  "author": {
6
6
  "name": "Björn Müller",
@@ -60,7 +60,7 @@
60
60
  "@alcalzone/release-script-plugin-manual-review": "^5.2.0",
61
61
  "@iobroker/adapter-dev": "^1.5.0",
62
62
  "@iobroker/eslint-config": "^2.2.0",
63
- "@iobroker/testing": "^5.2.2",
63
+ "@iobroker/testing": "^6.2.1",
64
64
  "@tsconfig/node22": "^22.0.6",
65
65
  "@types/node": "^22.0.0"
66
66
  }