iobroker.kanban 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +224 -0
  3. package/admin/i18n/de.json +100 -0
  4. package/admin/i18n/en.json +100 -0
  5. package/admin/i18n/es.json +100 -0
  6. package/admin/i18n/fr.json +100 -0
  7. package/admin/i18n/it.json +100 -0
  8. package/admin/i18n/nl.json +100 -0
  9. package/admin/i18n/pl.json +100 -0
  10. package/admin/i18n/pt.json +100 -0
  11. package/admin/i18n/ru.json +100 -0
  12. package/admin/i18n/uk.json +100 -0
  13. package/admin/i18n/zh-cn.json +100 -0
  14. package/admin/jsonConfig.json +668 -0
  15. package/admin/kanban.png +0 -0
  16. package/io-package.json +299 -0
  17. package/lib/cron.js +203 -0
  18. package/lib/dueat.js +77 -0
  19. package/lib/events.js +24 -0
  20. package/lib/holidays.js +167 -0
  21. package/lib/i18n-server.js +301 -0
  22. package/lib/notify.js +627 -0
  23. package/lib/scheduler.js +198 -0
  24. package/lib/server.js +799 -0
  25. package/lib/store.js +1500 -0
  26. package/main.js +382 -0
  27. package/package.json +58 -0
  28. package/www/css/app.css +1022 -0
  29. package/www/i18n/de.json +238 -0
  30. package/www/i18n/en.json +238 -0
  31. package/www/i18n/es.json +238 -0
  32. package/www/i18n/fr.json +238 -0
  33. package/www/i18n/it.json +238 -0
  34. package/www/i18n/nl.json +238 -0
  35. package/www/i18n/pl.json +238 -0
  36. package/www/i18n/pt.json +238 -0
  37. package/www/i18n/ru.json +238 -0
  38. package/www/i18n/uk.json +238 -0
  39. package/www/i18n/zh-cn.json +238 -0
  40. package/www/index.html +171 -0
  41. package/www/js/api.js +98 -0
  42. package/www/js/app.js +411 -0
  43. package/www/js/board.js +983 -0
  44. package/www/js/colorpicker.js +165 -0
  45. package/www/js/dialogs.js +1419 -0
  46. package/www/js/i18n.js +43 -0
  47. package/www/vendor/marked.min.js +69 -0
  48. package/www/vendor/purify.min.js +3 -0
  49. package/www/vendor/sortable.min.js +2 -0
@@ -0,0 +1,299 @@
1
+ {
2
+ "common": {
3
+ "name": "kanban",
4
+ "version": "0.3.0",
5
+ "titleLang": {
6
+ "en": "Kanban Board",
7
+ "de": "Kanban Board",
8
+ "ru": "Kanban Board",
9
+ "pt": "Kanban Board",
10
+ "nl": "Kanban Board",
11
+ "fr": "Kanban Board",
12
+ "it": "Kanban Board",
13
+ "es": "Kanban Board",
14
+ "pl": "Kanban Board",
15
+ "uk": "Kanban Board",
16
+ "zh-cn": "Kanban Board"
17
+ },
18
+ "keywords": [
19
+ "kanban",
20
+ "board",
21
+ "tasks",
22
+ "todo",
23
+ "checklist",
24
+ "project",
25
+ "family",
26
+ "organizer"
27
+ ],
28
+ "type": "misc-data",
29
+ "connectionType": "local",
30
+ "dataSource": "push",
31
+ "tier": 3,
32
+ "desc": {
33
+ "en": "Kanban board with own web server, webhooks, multi-user support and email notifications",
34
+ "de": "Kanban-Board mit eigenem Webserver, Webhooks, Multiuser-Support und E-Mail-Benachrichtigungen",
35
+ "ru": "Канбан-доска с собственным веб-сервером, вебхуками, поддержкой нескольких пользователей и уведомлениями по электронной почте",
36
+ "pt": "Quadro Kanban com servidor web próprio, webhooks, suporte multiusuário e notificações por e-mail",
37
+ "nl": "Kanban-bord met eigen webserver, webhooks, ondersteuning voor meerdere gebruikers en e-mailmeldingen",
38
+ "fr": "Tableau Kanban avec serveur web intégré, webhooks, support multi-utilisateurs et notifications par e-mail",
39
+ "it": "Bacheca Kanban con web server integrato, webhook, supporto multiutente e notifiche via e-mail",
40
+ "es": "Tablero Kanban con servidor web propio, webhooks, soporte multiusuario y notificaciones por correo electrónico",
41
+ "pl": "Tablica Kanban z własnym serwerem WWW, webhookami, obsługą wielu użytkowników i powiadomieniami e-mail",
42
+ "uk": "Канбан-дошка з власним вебсервером, вебхуками, підтримкою кількох користувачів і сповіщеннями електронною поштою",
43
+ "zh-cn": "带有独立 Web 服务器、Webhook、多用户支持和电子邮件通知的看板"
44
+ },
45
+ "authors": [
46
+ "Björn Müller"
47
+ ],
48
+ "platform": "Javascript/Node.js",
49
+ "mode": "daemon",
50
+ "compact": true,
51
+ "messagebox": true,
52
+ "readme": "https://github.com/bmueller77/ioBroker.kanban/blob/main/README.md",
53
+ "icon": "kanban.png",
54
+ "extIcon": "https://raw.githubusercontent.com/bmueller77/ioBroker.kanban/main/admin/kanban.png",
55
+ "enabled": true,
56
+ "adminUI": {
57
+ "config": "json"
58
+ },
59
+ "localLinks": {
60
+ "_default": {
61
+ "link": "http://%ip%:%native_port%/",
62
+ "name": "Kanban",
63
+ "color": "#7E57C2",
64
+ "intro": true
65
+ }
66
+ },
67
+ "dependencies": [
68
+ {
69
+ "js-controller": ">=6.0.11"
70
+ }
71
+ ],
72
+ "licenseInformation": {
73
+ "type": "free",
74
+ "license": "MIT"
75
+ },
76
+ "news": {
77
+ "0.3.0": {
78
+ "en": "Per-board trash with a 30-day retention and automatic cleanup, per-column sorting, moving or copying cards between boards, cron-expression recurrence and recurring calendar invites. Security: CORS limited to configured origins, the board restriction of tokens enforced on every route, blank tokens rejected. Plus numerous fixes from the acceptance test.",
79
+ "de": "Papierkorb je Board mit 30-Tage-Frist und automatischem Aufräumen, Sortierung je Spalte, Karten zwischen Boards verschieben oder kopieren, Wiederholung per Cron-Ausdruck und Serientermin-Einladungen. Sicherheit: CORS nur für eingetragene Herkünfte, Board-Begrenzung der Tokens auf allen Wegen durchgesetzt, leere Tokens abgewiesen. Dazu zahlreiche Korrekturen aus dem Abnahmetest.",
80
+ "fr": "Corbeille par tableau avec conservation de 30 jours et nettoyage automatique, tri par colonne, déplacement ou copie de cartes entre tableaux, récurrence par expression cron et invitations d'agenda récurrentes. Sécurité : CORS limité aux origines configurées, restriction des jetons aux tableaux autorisés appliquée sur toutes les routes, jetons vides refusés. Ainsi que de nombreuses corrections issues du test de recette.",
81
+ "it": "Cestino per ogni bacheca con conservazione di 30 giorni e pulizia automatica, ordinamento per colonna, spostamento o copia di schede tra bacheche, ricorrenza tramite espressione cron e inviti di calendario ricorrenti. Sicurezza: CORS limitato alle origini configurate, restrizione dei token alle bacheche applicata su tutte le rotte, token vuoti rifiutati. Inoltre numerose correzioni dal collaudo.",
82
+ "nl": "Prullenbak per bord met een bewaartermijn van 30 dagen en automatisch opruimen, sortering per kolom, kaarten verplaatsen of kopiëren tussen borden, herhaling via cron-expressie en terugkerende agenda-uitnodigingen. Beveiliging: CORS beperkt tot ingestelde origins, bordbeperking van tokens op alle routes afgedwongen, lege tokens geweigerd. Plus talrijke correcties uit de acceptatietest.",
83
+ "es": "Papelera por tablero con retención de 30 días y limpieza automática, ordenación por columna, mover o copiar tarjetas entre tableros, recurrencia mediante expresión cron e invitaciones de calendario periódicas. Seguridad: CORS limitado a los orígenes configurados, restricción de tableros de los tokens aplicada en todas las rutas, tokens vacíos rechazados. Además, numerosas correcciones de la prueba de aceptación.",
84
+ "pt": "Lixeira por quadro com retenção de 30 dias e limpeza automática, ordenação por coluna, mover ou copiar cartões entre quadros, recorrência por expressão cron e convites de calendário recorrentes. Segurança: CORS limitado às origens configuradas, restrição de quadros dos tokens aplicada em todas as rotas, tokens vazios recusados. Além de inúmeras correções do teste de aceitação.",
85
+ "pl": "Kosz dla każdej tablicy z 30-dniowym okresem przechowywania i automatycznym porządkowaniem, sortowanie w kolumnach, przenoszenie lub kopiowanie kart między tablicami, powtarzanie według wyrażenia cron oraz cykliczne zaproszenia kalendarza. Bezpieczeństwo: CORS ograniczony do skonfigurowanych źródeł, ograniczenie tokenów do tablic egzekwowane na wszystkich trasach, puste tokeny odrzucane. Do tego liczne poprawki z testów odbiorczych.",
86
+ "ru": "Корзина для каждой доски с хранением 30 дней и автоматической очисткой, сортировка по столбцам, перемещение и копирование карточек между досками, повторение по cron-выражению и повторяющиеся приглашения в календарь. Безопасность: CORS ограничен настроенными источниками, ограничение токенов по доскам действует на всех маршрутах, пустые токены отклоняются. А также многочисленные исправления по итогам приёмочного теста.",
87
+ "uk": "Кошик для кожної дошки зі зберіганням 30 днів та автоматичним очищенням, сортування за стовпцями, перенесення та копіювання карток між дошками, повторення за cron-виразом і повторювані запрошення в календар. Безпека: CORS обмежено налаштованими джерелами, обмеження токенів за дошками діє на всіх маршрутах, порожні токени відхиляються. А також численні виправлення за результатами приймального тесту.",
88
+ "zh-cn": "每个看板独立的回收站,保留 30 天并自动清理;按列排序;在看板之间移动或复制卡片;支持 cron 表达式重复以及重复日历邀请。安全性:CORS 仅对已配置的来源开放,令牌的看板限制在所有路由上生效,空令牌被拒绝。此外还有大量验收测试中发现的问题修复。"
89
+ },
90
+ "0.2.1": {
91
+ "en": "Express 5; fixed avatar upload (Content-Security-Policy blocked blob: URLs); requires Node.js 20+ and admin 7.8.23+; updated dependencies; adapter metadata for the ioBroker repository; docs with Telegram/Pushover notification examples.",
92
+ "de": "Express 5; Avatar-Upload repariert (Content-Security-Policy blockierte blob:-URLs); benötigt Node.js 20+ und Admin 7.8.23+; aktualisierte Abhängigkeiten; Adapter-Metadaten für das ioBroker-Repository; Doku mit Telegram-/Pushover-Beispielen.",
93
+ "fr": "Express 5 ; correction du téléversement d'avatar (la Content-Security-Policy bloquait les URL blob:) ; nécessite Node.js 20+ et admin 7.8.23+ ; dépendances mises à jour ; métadonnées d'adaptateur pour le dépôt ioBroker ; documentation avec exemples Telegram/Pushover.",
94
+ "it": "Express 5; corretto il caricamento degli avatar (la Content-Security-Policy bloccava gli URL blob:); richiede Node.js 20+ e admin 7.8.23+; dipendenze aggiornate; metadati dell'adapter per il repository ioBroker; documentazione con esempi Telegram/Pushover.",
95
+ "nl": "Express 5; avatar-upload hersteld (Content-Security-Policy blokkeerde blob:-URL's); vereist Node.js 20+ en admin 7.8.23+; bijgewerkte afhankelijkheden; adaptermetadata voor de ioBroker-repository; documentatie met Telegram-/Pushover-voorbeelden.",
96
+ "es": "Express 5; corregida la subida de avatares (la Content-Security-Policy bloqueaba las URL blob:); requiere Node.js 20+ y admin 7.8.23+; dependencias actualizadas; metadatos del adaptador para el repositorio de ioBroker; documentación con ejemplos de Telegram/Pushover.",
97
+ "pt": "Express 5; carregamento de avatares corrigido (a Content-Security-Policy bloqueava URL blob:); requer Node.js 20+ e admin 7.8.23+; dependências atualizadas; metadados do adaptador para o repositório ioBroker; documentação com exemplos de Telegram/Pushover.",
98
+ "pl": "Express 5; naprawiono przesyłanie awatarów (Content-Security-Policy blokowała adresy blob:); wymaga Node.js 20+ i admin 7.8.23+; zaktualizowane zależności; metadane adaptera dla repozytorium ioBroker; dokumentacja z przykładami Telegram/Pushover.",
99
+ "ru": "Express 5; исправлена загрузка аватаров (Content-Security-Policy блокировала blob:-адреса); требуется Node.js 20+ и admin 7.8.23+; обновлённые зависимости; метаданные адаптера для репозитория ioBroker; документация с примерами Telegram/Pushover.",
100
+ "uk": "Express 5; виправлено завантаження аватарів (Content-Security-Policy блокувала blob:-адреси); потрібні Node.js 20+ та admin 7.8.23+; оновлені залежності; метадані адаптера для репозиторію ioBroker; документація з прикладами Telegram/Pushover.",
101
+ "zh-cn": "升级到 Express 5;修复头像上传(内容安全策略此前会阻止 blob: 链接);需要 Node.js 20+ 与 admin 7.8.23+;更新依赖;补充 ioBroker 仓库所需的适配器元数据;文档新增 Telegram/Pushover 示例。"
102
+ },
103
+ "0.2.0": {
104
+ "en": "Mobile: columns stack & collapse (accordion), full-screen dialogs with a fixed action bar. Assignable users per board (Settings > Boards). Header chips are now a saved per-board filter. User colours set in the web UI (Settings > Users), applied without a restart. Colour rings on avatars and automatic black/white text on labels and avatars for readability. Per-board notification link target (board view, card editor or custom URL). New icons for the description badge and the done-column toggle. Per-instance date format is now freely configurable: moment/Day.js tokens incl. localised month/weekday names (e.g. DD. MMMM gives 20 July).",
105
+ "de": "Mobil: Spalten untereinander & einklappbar (Akkordeon), vollflächige Dialoge mit fester Aktionsleiste. Benutzer je Board zuweisbar (Einstellungen > Boards). Kopf-Chips sind jetzt ein je Board gespeicherter Filter. Benutzerfarben in der Weboberfläche (Einstellungen > Users), ohne Neustart. Farbringe an Avataren und automatische Schwarz/Weiß-Schrift auf Labels und Avataren für Lesbarkeit. Benachrichtigungs-Link je Board (Board-Ansicht, Karten-Editor oder eigene URL). Neue Icons für Beschreibungs-Badge und Erledigt-Umschalter. Datumsformat pro Instanz frei konfigurierbar: moment/Day.js-Tokens inkl. lokalisierter Monats-/Wochentagsnamen (z. B. DD. MMMM ergibt 20. Juli).",
106
+ "fr": "Mobile : colonnes empilées et repliables (accordéon), boîtes de dialogue plein écran avec barre d'actions fixe. Utilisateurs assignables par tableau. Les pastilles de l'en-tête sont désormais un filtre mémorisé par tableau. Couleurs d'utilisateur réglables dans l'interface web, sans redémarrage. Anneaux de couleur sur les avatars et texte noir/blanc automatique pour la lisibilité. Cible du lien de notification par tableau. Nouvelles icônes. Format de date librement configurable par instance, avec noms de mois et de jours localisés.",
107
+ "it": "Mobile: colonne impilate e comprimibili (fisarmonica), finestre a tutto schermo con barra azioni fissa. Utenti assegnabili per bacheca. I chip dell'intestazione sono ora un filtro salvato per bacheca. Colori utente impostabili nell'interfaccia web, senza riavvio. Anelli colorati sugli avatar e testo bianco/nero automatico per la leggibilità. Destinazione del link di notifica per bacheca. Nuove icone. Formato data configurabile per istanza, con nomi di mesi e giorni localizzati.",
108
+ "nl": "Mobiel: gestapelde en inklapbare kolommen (accordeon), schermvullende dialogen met vaste actiebalk. Gebruikers per bord toewijsbaar. De chips in de kopregel zijn nu een per bord opgeslagen filter. Gebruikerskleuren instelbaar in de webinterface, zonder herstart. Kleurringen op avatars en automatische zwart/witte tekst voor leesbaarheid. Doel van de meldingslink per bord. Nieuwe pictogrammen. Datumnotatie per instantie vrij instelbaar, met gelokaliseerde maand- en dagnamen.",
109
+ "es": "Móvil: columnas apiladas y plegables (acordeón), diálogos a pantalla completa con barra de acciones fija. Usuarios asignables por tablero. Las fichas de la cabecera son ahora un filtro guardado por tablero. Colores de usuario configurables en la interfaz web, sin reinicio. Anillos de color en los avatares y texto blanco/negro automático para la legibilidad. Destino del enlace de notificación por tablero. Nuevos iconos. Formato de fecha configurable por instancia, con nombres de meses y días localizados.",
110
+ "pt": "Móvel: colunas empilhadas e recolhíveis (acordeão), diálogos em ecrã inteiro com barra de ações fixa. Utilizadores atribuíveis por quadro. As fichas do cabeçalho são agora um filtro guardado por quadro. Cores de utilizador definidas na interface web, sem reiniciar. Anéis de cor nos avatares e texto branco/preto automático para legibilidade. Destino da ligação de notificação por quadro. Novos ícones. Formato de data configurável por instância, com nomes de meses e dias localizados.",
111
+ "pl": "Mobilnie: kolumny jedna pod drugą i zwijane (akordeon), pełnoekranowe okna dialogowe ze stałym paskiem akcji. Użytkownicy przypisywani do tablicy. Znaczniki w nagłówku są teraz filtrem zapisywanym dla każdej tablicy. Kolory użytkowników ustawiane w interfejsie webowym, bez restartu. Kolorowe obwódki awatarów i automatyczny czarny/biały tekst dla czytelności. Cel odnośnika powiadomień dla każdej tablicy. Nowe ikony. Format daty dowolnie konfigurowalny dla instancji, z lokalizowanymi nazwami miesięcy i dni.",
112
+ "ru": "Мобильный вид: столбцы друг под другом и сворачиваемые (аккордеон), полноэкранные диалоги с закреплённой панелью действий. Пользователи назначаются для каждой доски. Чипы в шапке теперь фильтр, сохраняемый для каждой доски. Цвета пользователей задаются в веб-интерфейсе, без перезапуска. Цветные кольца на аватарах и автоматический чёрный/белый текст для читаемости. Цель ссылки уведомления для каждой доски. Новые значки. Формат даты настраивается для экземпляра, с локализованными названиями месяцев и дней.",
113
+ "uk": "Мобільний вигляд: стовпці один під одним і згортання (акордеон), повноекранні діалоги із закріпленою панеллю дій. Користувачі призначаються для кожної дошки. Чіпи в шапці тепер фільтр, збережений для кожної дошки. Кольори користувачів задаються у вебінтерфейсі, без перезапуску. Кольорові кільця на аватарах та автоматичний чорний/білий текст для читабельності. Ціль посилання сповіщення для кожної дошки. Нові піктограми. Формат дати налаштовується для екземпляра, з локалізованими назвами місяців і днів.",
114
+ "zh-cn": "移动端:列改为纵向堆叠并可折叠(手风琴式),对话框全屏显示并带固定操作栏。可按看板指定成员。顶部头像标签现在是按看板保存的筛选器。用户颜色可在网页界面设置,无需重启。头像添加彩色环,标签与头像自动使用黑白文字以保证可读性。通知链接目标可按看板设置。新增图标。日期格式可按实例自由配置,月份与星期名称本地化。"
115
+ },
116
+ "0.1.3": {
117
+ "en": "Fix: the column task count now reflects the active person/label filter (previously showed the column total)",
118
+ "de": "Fix: Der Aufgaben-Zähler in der Spaltenkopfzeile berücksichtigt jetzt den aktiven Personen-/Label-Filter (zeigte vorher die Gesamtzahl)",
119
+ "fr": "Correction : le compteur de tâches dans l'en-tête de colonne tient compte du filtre personnes/étiquettes actif (il affichait auparavant le total de la colonne)",
120
+ "it": "Correzione: il contatore nell'intestazione della colonna tiene conto del filtro per persone/etichette attivo (prima mostrava il totale della colonna)",
121
+ "nl": "Fix: de takenteller in de kolomkop houdt nu rekening met het actieve personen-/labelfilter (toonde eerder het kolomtotaal)",
122
+ "es": "Corrección: el contador de tareas en la cabecera de columna refleja el filtro activo de personas/etiquetas (antes mostraba el total de la columna)",
123
+ "pt": "Correção: o contador de tarefas no cabeçalho da coluna passa a refletir o filtro ativo de pessoas/etiquetas (antes mostrava o total da coluna)",
124
+ "pl": "Poprawka: licznik zadań w nagłówku kolumny uwzględnia aktywny filtr osób/etykiet (wcześniej pokazywał sumę kolumny)",
125
+ "ru": "Исправление: счётчик задач в заголовке столбца учитывает активный фильтр по людям и меткам (раньше показывал общее число в столбце)",
126
+ "uk": "Виправлення: лічильник завдань у заголовку стовпця враховує активний фільтр за людьми/мітками (раніше показував загальну кількість у стовпці)",
127
+ "zh-cn": "修复:列标题中的任务计数现在会考虑当前的人员/标签筛选(此前显示的是该列总数)"
128
+ },
129
+ "0.1.2": {
130
+ "en": "Share dialog: doneLimit distinguishes empty=all and 0=none; label filter is now a blacklist (new labels stay visible)",
131
+ "de": "Teilen-Dialog: doneLimit unterscheidet leer=alle und 0=keine; Label-Filter jetzt Blacklist (neue Labels bleiben sichtbar)",
132
+ "fr": "Boîte de dialogue de partage : doneLimit distingue vide = tous et 0 = aucun ; le filtre d'étiquettes est désormais une liste noire (les nouvelles étiquettes restent visibles)",
133
+ "it": "Finestra di condivisione: doneLimit distingue vuoto = tutte e 0 = nessuna; il filtro etichette è ora una lista di esclusione (le nuove etichette restano visibili)",
134
+ "nl": "Deeldialoog: doneLimit onderscheidt leeg = alle en 0 = geen; het labelfilter is nu een blacklist (nieuwe labels blijven zichtbaar)",
135
+ "es": "Diálogo de compartir: doneLimit distingue vacío = todas y 0 = ninguna; el filtro de etiquetas es ahora una lista de exclusión (las etiquetas nuevas siguen visibles)",
136
+ "pt": "Diálogo de partilha: doneLimit distingue vazio = todos e 0 = nenhum; o filtro de etiquetas é agora uma lista de exclusão (as novas etiquetas permanecem visíveis)",
137
+ "pl": "Okno udostępniania: doneLimit rozróżnia puste = wszystkie i 0 = żadne; filtr etykiet działa teraz jako czarna lista (nowe etykiety pozostają widoczne)",
138
+ "ru": "Диалог «Поделиться»: doneLimit различает пусто = все и 0 = ничего; фильтр меток теперь чёрный список (новые метки остаются видимыми)",
139
+ "uk": "Вікно «Поділитися»: doneLimit розрізняє порожньо = усі та 0 = жодної; фільтр міток тепер чорний список (нові мітки лишаються видимими)",
140
+ "zh-cn": "分享对话框:doneLimit 区分留空=全部与 0=不显示;标签筛选现为黑名单(新标签仍然可见)"
141
+ },
142
+ "0.1.1": {
143
+ "en": "Security: token-protected write API, sanitized markdown preview, CSP, safe link schemes",
144
+ "de": "Sicherheit: tokengeschützte Schreib-API, bereinigte Markdown-Vorschau, CSP, sichere Link-Schemata",
145
+ "fr": "Sécurité : API d'écriture protégée par jeton, aperçu Markdown assaini, CSP, schémas de liens sûrs uniquement",
146
+ "it": "Sicurezza: API di scrittura protetta da token, anteprima Markdown sanificata, CSP, solo schemi di link sicuri",
147
+ "nl": "Beveiliging: schrijf-API beveiligd met token, opgeschoonde Markdown-voorbeeldweergave, CSP, alleen veilige linkschema's",
148
+ "es": "Seguridad: API de escritura protegida con token, vista previa de Markdown saneada, CSP, solo esquemas de enlace seguros",
149
+ "pt": "Segurança: API de escrita protegida por token, pré-visualização Markdown sanitizada, CSP, apenas esquemas de ligação seguros",
150
+ "pl": "Bezpieczeństwo: zapis przez API chroniony tokenem, oczyszczony podgląd Markdown, CSP, tylko bezpieczne schematy linków",
151
+ "ru": "Безопасность: запись через API защищена токеном, очистка предпросмотра Markdown, CSP, только безопасные схемы ссылок",
152
+ "uk": "Безпека: запис через API захищено токеном, очищений попередній перегляд Markdown, CSP, лише безпечні схеми посилань",
153
+ "zh-cn": "安全性:写入 API 需要令牌,Markdown 预览经过净化,启用 CSP,仅允许安全的链接协议"
154
+ },
155
+ "0.1.0": {
156
+ "en": "Initial release",
157
+ "de": "Erstveröffentlichung",
158
+ "fr": "Version initiale",
159
+ "it": "Prima versione",
160
+ "nl": "Eerste release",
161
+ "es": "Versión inicial",
162
+ "pt": "Versão inicial",
163
+ "pl": "Pierwsze wydanie",
164
+ "ru": "Первый выпуск",
165
+ "uk": "Перший випуск",
166
+ "zh-cn": "首次发布"
167
+ }
168
+ },
169
+ "globalDependencies": [
170
+ {
171
+ "admin": ">=7.8.23"
172
+ }
173
+ ]
174
+ },
175
+ "native": {
176
+ "port": 8095,
177
+ "bind": "0.0.0.0",
178
+ "dateFormat": "",
179
+ "timeFormat": "24h",
180
+ "users": [
181
+ {
182
+ "name": "user1",
183
+ "displayName": "User 1",
184
+ "email": "",
185
+ "color": "#7E57C2",
186
+ "icon": "mdi:account"
187
+ },
188
+ {
189
+ "name": "user2",
190
+ "displayName": "User 2",
191
+ "email": "",
192
+ "color": "#26A69A",
193
+ "icon": "mdi:account"
194
+ }
195
+ ],
196
+ "inboundTokens": [],
197
+ "outboundWebhooks": [],
198
+ "emailInstance": "email.0",
199
+ "emailFrom": "",
200
+ "notifyAssigned": true,
201
+ "notifyDue": true,
202
+ "notifyCreated": false,
203
+ "notifyMoved": false,
204
+ "notifyDone": false,
205
+ "notifyDeleted": false,
206
+ "notifyRestored": false,
207
+ "notifyPurged": false,
208
+ "reminderTime": "08:00",
209
+ "reminderDaysBefore": 1,
210
+ "dueExact": false,
211
+ "publicUrl": "",
212
+ "themeDefault": "auto",
213
+ "accentColor": "#7E57C2",
214
+ "customCss": "",
215
+ "language": "",
216
+ "notifyUpdated": false,
217
+ "apiWriteProtection": true,
218
+ "actionStateEnabled": true,
219
+ "corsOrigins": ""
220
+ },
221
+ "objects": [],
222
+ "instanceObjects": [
223
+ {
224
+ "_id": "",
225
+ "type": "meta",
226
+ "common": {
227
+ "name": {
228
+ "en": "Kanban files (avatars)",
229
+ "de": "Kanban-Dateien (Avatare)"
230
+ },
231
+ "type": "meta.user"
232
+ },
233
+ "native": {}
234
+ },
235
+ {
236
+ "_id": "info",
237
+ "type": "channel",
238
+ "common": {
239
+ "name": "Info"
240
+ },
241
+ "native": {}
242
+ },
243
+ {
244
+ "_id": "info.connection",
245
+ "type": "state",
246
+ "common": {
247
+ "name": "Web server running",
248
+ "type": "boolean",
249
+ "role": "indicator.connected",
250
+ "read": true,
251
+ "write": false,
252
+ "def": false
253
+ },
254
+ "native": {}
255
+ },
256
+ {
257
+ "_id": "action",
258
+ "type": "state",
259
+ "common": {
260
+ "name": "Command input (JSON, same commands as sendTo/webhook)",
261
+ "type": "string",
262
+ "role": "json",
263
+ "read": true,
264
+ "write": true,
265
+ "def": ""
266
+ },
267
+ "native": {}
268
+ },
269
+ {
270
+ "_id": "lastEvent",
271
+ "type": "state",
272
+ "common": {
273
+ "name": "Last board event (JSON)",
274
+ "type": "string",
275
+ "role": "json",
276
+ "read": true,
277
+ "write": false,
278
+ "def": ""
279
+ },
280
+ "native": {}
281
+ },
282
+ {
283
+ "_id": "boards",
284
+ "type": "channel",
285
+ "common": {
286
+ "name": "Boards"
287
+ },
288
+ "native": {}
289
+ },
290
+ {
291
+ "_id": "users",
292
+ "type": "channel",
293
+ "common": {
294
+ "name": "Users"
295
+ },
296
+ "native": {}
297
+ }
298
+ ]
299
+ }
package/lib/cron.js ADDED
@@ -0,0 +1,203 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Fünffeldriger Cron-Ausdruck als Wiederholungsmuster: `Minute Stunde Tag Monat Wochentag`.
5
+ *
6
+ * Der Adapter plant damit nichts — er sucht damit das nächste Fälligkeitsdatum einer
7
+ * wiederkehrenden Karte. Minute und Stunde legen die Uhrzeit der Folgekarte fest, die
8
+ * drei Datumsfelder entscheiden, welcher Tag passt.
9
+ *
10
+ * Unterstützt je Feld: `*`, Zahl, Liste (`1,15`), Bereich (`1-5`), Schrittweite
11
+ * (Stern plus `/3`, oder `1-7/2`) sowie die englischen Kurznamen für Monat und Wochentag.
12
+ * Sonntag ist 0 und 7. Nicht unterstützt: `L`, `W`, `#`, `?` — diese Erweiterungen
13
+ * kennt nicht einmal jeder Cron, und ihre Fälle deckt der Adapter mit eigenen
14
+ * Wiederholungstypen ab (n-ter Wochentag, Arbeitstag im Monat).
15
+ */
16
+
17
+ const FIELDS = [
18
+ { name: 'minute', min: 0, max: 59 },
19
+ { name: 'hour', min: 0, max: 23 },
20
+ { name: 'dayOfMonth', min: 1, max: 31 },
21
+ { name: 'month', min: 1, max: 12 },
22
+ { name: 'dayOfWeek', min: 0, max: 7 },
23
+ ];
24
+
25
+ /** Obergrenze fuer die Laenge eines Ausdrucks (siehe parseCron). */
26
+ const MAX_EXPR_LENGTH = 120;
27
+
28
+ const MONTH_NAMES = {
29
+ jan: 1,
30
+ feb: 2,
31
+ mar: 3,
32
+ apr: 4,
33
+ may: 5,
34
+ jun: 6,
35
+ jul: 7,
36
+ aug: 8,
37
+ sep: 9,
38
+ oct: 10,
39
+ nov: 11,
40
+ dec: 12,
41
+ };
42
+ const DAY_NAMES = { sun: 0, mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6 };
43
+
44
+ function namedValue(token, fieldName) {
45
+ const key = String(token).toLowerCase();
46
+ if (fieldName === 'month' && MONTH_NAMES[key] !== undefined) {
47
+ return MONTH_NAMES[key];
48
+ }
49
+ if (fieldName === 'dayOfWeek' && DAY_NAMES[key] !== undefined) {
50
+ return DAY_NAMES[key];
51
+ }
52
+ return null;
53
+ }
54
+
55
+ /**
56
+ * Ein Feld in die Menge der erlaubten Zahlen auflösen. Wirft bei Unsinn.
57
+ *
58
+ * @param raw
59
+ * @param field
60
+ */
61
+ function parseField(raw, field) {
62
+ const spec = String(raw || '').trim();
63
+ if (!spec) {
64
+ throw new Error(`${field.name}: leer`);
65
+ }
66
+ const out = new Set();
67
+
68
+ for (const part of spec.split(',')) {
69
+ const piece = part.trim();
70
+ if (!piece) {
71
+ throw new Error(`${field.name}: leerer Listeneintrag in '${spec}'`);
72
+ }
73
+
74
+ const [rangePart, stepPart] = piece.split('/');
75
+ let step = 1;
76
+ if (stepPart !== undefined) {
77
+ step = Number(stepPart);
78
+ if (!Number.isInteger(step) || step < 1) {
79
+ throw new Error(`${field.name}: ungültige Schrittweite '${stepPart}'`);
80
+ }
81
+ }
82
+
83
+ let from;
84
+ let to;
85
+ if (rangePart === '*') {
86
+ from = field.min;
87
+ to = field.max;
88
+ } else if (rangePart.includes('-')) {
89
+ const [a, b] = rangePart.split('-');
90
+ from = namedValue(a, field.name);
91
+ if (from === null) {
92
+ from = Number(a);
93
+ }
94
+ to = namedValue(b, field.name);
95
+ if (to === null) {
96
+ to = Number(b);
97
+ }
98
+ } else {
99
+ from = namedValue(rangePart, field.name);
100
+ if (from === null) {
101
+ from = Number(rangePart);
102
+ }
103
+ to = stepPart !== undefined ? field.max : from;
104
+ }
105
+
106
+ if (!Number.isInteger(from) || !Number.isInteger(to)) {
107
+ throw new Error(`${field.name}: '${piece}' ist keine gültige Angabe`);
108
+ }
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}`);
111
+ }
112
+ for (let v = from; v <= to; v += step) {
113
+ out.add(v);
114
+ }
115
+ }
116
+
117
+ // Sonntag darf 0 oder 7 heißen — intern immer 0
118
+ if (field.name === 'dayOfWeek' && out.has(7)) {
119
+ out.delete(7);
120
+ out.add(0);
121
+ }
122
+ return out;
123
+ }
124
+
125
+ /**
126
+ * Cron-Ausdruck zerlegen.
127
+ *
128
+ * @param expr
129
+ * @returns {{minute:Set, hour:Set, dayOfMonth:Set, month:Set, dayOfWeek:Set,
130
+ * domRestricted:boolean, dowRestricted:boolean, time:string, expr:string}}
131
+ * @throws {Error} mit einer Meldung, die sich direkt anzeigen lässt
132
+ */
133
+ function parseCron(expr) {
134
+ const raw = String(expr || '').trim();
135
+ // Laengendeckel: firstDue/nextDue rufen den Parser fuer jedes Kandidatendatum
136
+ // erneut auf (bis zu 801 Tage). Ohne Deckel blockiert ein kilobytegrosser
137
+ // Ausdruck ueber die token-freie Pruefroute den Event-Loop fuer Sekunden.
138
+ // Das laengste sinnvolle Muster (alle Monatsnamen) bleibt weit darunter.
139
+ if (raw.length > MAX_EXPR_LENGTH) {
140
+ throw new Error(`Ausdruck zu lang (höchstens ${MAX_EXPR_LENGTH} Zeichen)`);
141
+ }
142
+ const parts = raw.split(/\s+/);
143
+ if (parts.length !== 5) {
144
+ throw new Error(
145
+ `Cron braucht fünf Felder (Minute Stunde Tag Monat Wochentag), gefunden: ${parts.filter(Boolean).length}`,
146
+ );
147
+ }
148
+ const parsed = {};
149
+ FIELDS.forEach((field, i) => {
150
+ parsed[field.name] = parseField(parts[i], field);
151
+ });
152
+
153
+ // Cron-Eigenheit: Sind Tag UND Wochentag eingeschränkt, gilt ODER, nicht UND.
154
+ parsed.domRestricted = parts[2].trim() !== '*';
155
+ parsed.dowRestricted = parts[4].trim() !== '*';
156
+
157
+ // Uhrzeit der Folgekarte: die früheste im Muster genannte
158
+ const hour = Math.min(...parsed.hour);
159
+ const minute = Math.min(...parsed.minute);
160
+ parsed.time = `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
161
+ parsed.expr = parts.join(' ');
162
+ return parsed;
163
+ }
164
+
165
+ /**
166
+ * Passt ein Datum (Date) auf das Muster? Nur die drei Datumsfelder zählen.
167
+ *
168
+ * @param parsed
169
+ * @param d
170
+ */
171
+ function cronMatchesDate(parsed, d) {
172
+ if (!parsed.month.has(d.getMonth() + 1)) {
173
+ return false;
174
+ }
175
+ const dom = parsed.dayOfMonth.has(d.getDate());
176
+ const dow = parsed.dayOfWeek.has(d.getDay());
177
+ if (parsed.domRestricted && parsed.dowRestricted) {
178
+ return dom || dow;
179
+ }
180
+ if (parsed.domRestricted) {
181
+ return dom;
182
+ }
183
+ if (parsed.dowRestricted) {
184
+ return dow;
185
+ }
186
+ return true;
187
+ }
188
+
189
+ /**
190
+ * Gültig? Liefert { ok:true, time } oder { ok:false, error }.
191
+ *
192
+ * @param expr
193
+ */
194
+ function validateCron(expr) {
195
+ try {
196
+ const p = parseCron(expr);
197
+ return { ok: true, time: p.time };
198
+ } catch (e) {
199
+ return { ok: false, error: e.message };
200
+ }
201
+ }
202
+
203
+ module.exports = { parseCron, cronMatchesDate, validateCron };
package/lib/dueat.js ADDED
@@ -0,0 +1,77 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Fälligkeit als ISO-Zeitstempel mit lokalem Offset (`dueAt`).
5
+ *
6
+ * Karten speichern die Fälligkeit getrennt als `due` (YYYY-MM-DD) und optional
7
+ * `dueTime` (HH:MM). Für Automatisierungen ist ein einzelner, eindeutiger
8
+ * Zeitstempel praktischer – den liefern die Ereignisse und (seit 0.3.0) auch
9
+ * die REST-API in jedem Kartenobjekt. Berechnet, nicht gespeichert.
10
+ */
11
+
12
+ // Offset (Minuten) der Zeitzone tz zum Zeitpunkt date.
13
+ function tzOffsetMinutes(date, tz) {
14
+ const dtf = new Intl.DateTimeFormat('en-US', {
15
+ timeZone: tz,
16
+ hourCycle: 'h23',
17
+ year: 'numeric',
18
+ month: '2-digit',
19
+ day: '2-digit',
20
+ hour: '2-digit',
21
+ minute: '2-digit',
22
+ second: '2-digit',
23
+ });
24
+ const map = {};
25
+ for (const p of dtf.formatToParts(date)) {
26
+ if (p.type !== 'literal') {
27
+ map[p.type] = Number(p.value);
28
+ }
29
+ }
30
+ const asUtc = Date.UTC(map.year, map.month - 1, map.day, map.hour, map.minute, map.second);
31
+ return Math.round((asUtc - date.getTime()) / 60000);
32
+ }
33
+
34
+ // Kombiniert Fälligkeitsdatum + Uhrzeit zu einem ISO-String mit lokalem Offset.
35
+ // Ohne Uhrzeit wird 00:00 angenommen. Gibt null zurück, wenn kein Datum vorliegt.
36
+ function buildDueAt(card, tz) {
37
+ if (!card || !card.due) {
38
+ return null;
39
+ }
40
+ const zone = tz || 'Europe/Berlin';
41
+ const [y, mo, d] = card.due.split('-').map(Number);
42
+ const [hh, mm] = (card.dueTime || '00:00').split(':').map(Number);
43
+ const guessUtc = Date.UTC(y, mo - 1, d, hh, mm, 0);
44
+ const off = tzOffsetMinutes(new Date(guessUtc), zone);
45
+ const p2 = n => String(Math.abs(n)).padStart(2, '0');
46
+ const sign = off >= 0 ? '+' : '-';
47
+ const offStr = `${sign}${p2(Math.trunc(off / 60))}:${p2(off % 60)}`;
48
+ return `${card.due}T${p2(hh)}:${p2(mm)}:00${offStr}`;
49
+ }
50
+
51
+ /**
52
+ * Kartenobjekt für die Ausgabe: flache Kopie mit berechnetem dueAt.
53
+ *
54
+ * @param card
55
+ * @param tz
56
+ */
57
+ function cardWithDueAt(card, tz) {
58
+ if (!card || typeof card !== 'object') {
59
+ return card;
60
+ }
61
+ return { ...card, dueAt: buildDueAt(card, tz) };
62
+ }
63
+
64
+ /**
65
+ * Board-Objekt für die Ausgabe: Karten mit dueAt, Original bleibt unberührt.
66
+ *
67
+ * @param board
68
+ * @param tz
69
+ */
70
+ function boardWithDueAt(board, tz) {
71
+ if (!board || !Array.isArray(board.cards)) {
72
+ return board;
73
+ }
74
+ return { ...board, cards: board.cards.map(c => cardWithDueAt(c, tz)) };
75
+ }
76
+
77
+ module.exports = { tzOffsetMinutes, buildDueAt, cardWithDueAt, boardWithDueAt };
package/lib/events.js ADDED
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ const { EventEmitter } = require('node:events');
4
+
5
+ /**
6
+ * Zentraler Event-Bus des Adapters.
7
+ * Ereignistypen: cardCreated | cardUpdated | cardMoved | cardAssigned | cardDone | cardDeleted | cardRestored | cardPurged | cardDue
8
+ * cardDeleted = Karte in den Papierkorb verschoben (wiederherstellbar; beim Aufräumen detail.auto:true, detail.reason:'cleanup')
9
+ * cardRestored = Karte aus dem Papierkorb zurückgeholt
10
+ * cardPurged = Karte endgültig entfernt (nach 30 Tagen im Papierkorb oder manuell)
11
+ * Jedes Event hat die Form:
12
+ * { event, ts, board: {id, title}, card: {...}, detail: {...} }
13
+ * Zusätzlich zum typspezifischen Event wird immer 'event' emittiert (für den Dispatcher).
14
+ */
15
+ class EventBus extends EventEmitter {
16
+ emitEvent(type, data) {
17
+ const event = Object.assign({ event: type, ts: new Date().toISOString() }, data);
18
+ this.emit(type, event);
19
+ this.emit('event', event);
20
+ return event;
21
+ }
22
+ }
23
+
24
+ module.exports = { EventBus };