local-kanban 1.6.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.
Files changed (64) hide show
  1. package/CHANGELOG.md +381 -0
  2. package/LICENSE +21 -0
  3. package/README.md +144 -0
  4. package/README.ru.md +144 -0
  5. package/bin/kb +306 -0
  6. package/bin/local-kanban +217 -0
  7. package/ecosystem.example.cjs +25 -0
  8. package/package.json +52 -0
  9. package/public/app.js +18 -0
  10. package/public/boot-fallback.js +3 -0
  11. package/public/drawer-sync.js +14 -0
  12. package/public/favicon-180.png +0 -0
  13. package/public/favicon-32.png +0 -0
  14. package/public/favicon-64.png +0 -0
  15. package/public/favicon.ico +0 -0
  16. package/public/favicon.svg +6 -0
  17. package/public/icon-192.png +0 -0
  18. package/public/icon-512.png +0 -0
  19. package/public/icon-maskable-192.png +0 -0
  20. package/public/icon-maskable-512.png +0 -0
  21. package/public/index.html +138 -0
  22. package/public/js/board.js +698 -0
  23. package/public/js/chaos.js +247 -0
  24. package/public/js/core.js +821 -0
  25. package/public/js/dash.js +114 -0
  26. package/public/js/drawer.js +919 -0
  27. package/public/js/horizon.js +140 -0
  28. package/public/js/init.js +208 -0
  29. package/public/js/project.js +338 -0
  30. package/public/js/settings.js +905 -0
  31. package/public/js/sidebar.js +640 -0
  32. package/public/js/sse.js +386 -0
  33. package/public/manifest.webmanifest +32 -0
  34. package/public/offline.html +39 -0
  35. package/public/style.css +1247 -0
  36. package/public/sw.js +31 -0
  37. package/scripts/raycast/open-kanban.sh +18 -0
  38. package/scripts/update.js +43 -0
  39. package/skills/deploy/SKILL.md +70 -0
  40. package/skills/kanban/SKILL.md +217 -0
  41. package/src/backup.js +111 -0
  42. package/src/bus.js +10 -0
  43. package/src/config.js +40 -0
  44. package/src/db.js +321 -0
  45. package/src/error-handler.js +11 -0
  46. package/src/file-snapshot.js +22 -0
  47. package/src/origin-guard.js +32 -0
  48. package/src/pm2-services.js +31 -0
  49. package/src/query-params.js +17 -0
  50. package/src/repo-base.js +23 -0
  51. package/src/restore.js +153 -0
  52. package/src/routes/dashboard.js +132 -0
  53. package/src/routes/events.js +30 -0
  54. package/src/routes/horizons.js +66 -0
  55. package/src/routes/projects.js +532 -0
  56. package/src/routes/skills.js +116 -0
  57. package/src/routes/system.js +285 -0
  58. package/src/routes/tasks.js +477 -0
  59. package/src/schema.sql +135 -0
  60. package/src/server.js +100 -0
  61. package/src/skills.js +119 -0
  62. package/src/sync/github.js +265 -0
  63. package/src/sync/worker.js +136 -0
  64. package/src/task-shape.js +29 -0
@@ -0,0 +1,821 @@
1
+
2
+ const ICONS = {
3
+ pin: 'M9.6 2.2 13.8 6.4M11 4 7.6 7.4l-3 .6 4.4 4.4.6-3L13 6M5.2 10.8 2.6 13.4',
4
+ gear: 'M2.5 4.6h11M2.5 11.4h11',
5
+ git: 'M4.6 6.2v6.2M4.6 3.6v.2M11.4 6.2c0 2.4-1.9 3.6-4 4M11.4 3.6v.2',
6
+ chart: 'M2.5 13.5h11M4.6 11.4V7.6M8 11.4V3.4M11.4 11.4V6',
7
+ trend: 'M2.5 11.5 6 8l2.6 2.4L13.5 5M13.5 5H10M13.5 5v3.4',
8
+ copy: 'M5.5 5.5V3.2a1 1 0 0 1 1-1h6.3a1 1 0 0 1 1 1v6.3a1 1 0 0 1-1 1h-2.3M2.2 6.5h6.3a1 1 0 0 1 1 1v6.3a1 1 0 0 1-1 1H2.2a1 1 0 0 1-1-1V7.5a1 1 0 0 1 1-1Z',
9
+ link: 'M6.6 9.4a2.8 2.8 0 0 0 4 0l2-2a2.8 2.8 0 0 0-4-4l-1 1M9.4 6.6a2.8 2.8 0 0 0-4 0l-2 2a2.8 2.8 0 0 0 4 4l1-1',
10
+ attach: 'M11 5.2 6.3 9.9a1.7 1.7 0 0 0 2.4 2.4l4.7-4.7a3.2 3.2 0 0 0-4.6-4.5L4 7.9a4.8 4.8 0 0 0 6.8 6.8l3-3',
11
+ comment: 'M13.8 8.6a5.2 5.2 0 0 1-5.2 5.2H5.4L2.2 15.4l.9-3A5.2 5.2 0 1 1 13.8 8.6Z',
12
+ timer: 'M8 4.8V8l2 1.6M8 14a6 6 0 1 0 0-12 6 6 0 0 0 0 12ZM6.2 1.4h3.6',
13
+ warn: 'M8 2.6 14.4 13.4H1.6L8 2.6ZM8 6.6v3M8 11.4v.1',
14
+ trash: 'M2.8 4.4h10.4M6.4 4.4V2.9h3.2v1.5M4.2 4.4l.6 8.7a1 1 0 0 0 1 .9h4.4a1 1 0 0 0 1-.9l.6-8.7M6.7 6.9v4.6M9.3 6.9v4.6',
15
+ plus: 'M8 3.4v9.2M3.4 8h9.2',
16
+ download: 'M8 2.6v7.8M5 7.4 8 10.4l3-3M2.8 12.6v.8a1 1 0 0 0 1 1h8.4a1 1 0 0 0 1-1v-.8',
17
+ duplicate: 'M3.4 10.6H2.6a1 1 0 0 1-1-1V2.6a1 1 0 0 1 1-1h7a1 1 0 0 1 1 1v.8M6.4 5.4h7a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1Z',
18
+ person: 'M8 8.4a2.6 2.6 0 1 0 0-5.2 2.6 2.6 0 0 0 0 5.2ZM3.2 14a4.8 4.8 0 0 1 9.6 0',
19
+ bot: 'M4.4 6.4h7.2a1 1 0 0 1 1 1v4.6a1 1 0 0 1-1 1H4.4a1 1 0 0 1-1-1V7.4a1 1 0 0 1 1-1ZM8 6.4V4M6.2 9.4v.1M9.8 9.4v.1M1.6 8.6v1.8M14.4 8.6v1.8',
20
+ soundOff: 'M8.4 3.2 4.9 6H2.4v4h2.5l3.5 2.8V3.2ZM11.2 6.4l3.2 3.2M14.4 6.4l-3.2 3.2',
21
+ lock: 'M3.6 7.2h8.8a.9.9 0 0 1 .9.9v4.7a.9.9 0 0 1-.9.9H3.6a.9.9 0 0 1-.9-.9V8.1a.9.9 0 0 1 .9-.9ZM5.4 7.2V5.3a2.6 2.6 0 0 1 5.2 0v1.9',
22
+ status: 'M8 13.4a5.4 5.4 0 1 0 0-10.8 5.4 5.4 0 0 0 0 10.8Z',
23
+ flag: 'M4 14V2.6M4 3.2h7.5l-1.6 2.4 1.6 2.4H4',
24
+ tag: 'M8 2.2H3.2a1 1 0 0 0-1 1V8l5.6 5.6a1.2 1.2 0 0 0 1.7 0l3.9-3.9a1.2 1.2 0 0 0 0-1.7L8 2.2Z',
25
+ chevron: 'M4.2 6.2 8 10l3.8-3.8',
26
+ doc: 'M9.2 1.8H4.6a1 1 0 0 0-1 1v10.4a1 1 0 0 0 1 1h6.8a1 1 0 0 0 1-1V4.6L9.2 1.8ZM9.2 1.8v2.8h3.2M6 8.2h4M6 10.8h4',
27
+ eye: 'M1.8 8S4.2 3.8 8 3.8 14.2 8 14.2 8 11.8 12.2 8 12.2 1.8 8 1.8 8Z',
28
+ };
29
+ const ICON_EXTRA = {
30
+ status: '<path d="M8 2.6v10.8a5.4 5.4 0 0 0 0-10.8Z" fill="currentColor" stroke="none"/>',
31
+ tag: '<circle cx="5.4" cy="5.4" r="1" fill="currentColor" stroke="none"/>',
32
+ eye: '<circle cx="8" cy="8" r="1.7" fill="none" stroke="currentColor" stroke-width="1.3"/>',
33
+ gear: '<circle cx="5.8" cy="4.6" r="1.5" fill="none" stroke="currentColor" stroke-width="1.3"/>'
34
+ + '<circle cx="10.4" cy="11.4" r="1.5" fill="none" stroke="currentColor" stroke-width="1.3"/>',
35
+ git: '<circle cx="4.6" cy="13.2" r="1.5" fill="none" stroke="currentColor" stroke-width="1.3"/>'
36
+ + '<circle cx="4.6" cy="2.8" r="1.5" fill="none" stroke="currentColor" stroke-width="1.3"/>'
37
+ + '<circle cx="11.4" cy="2.8" r="1.5" fill="none" stroke="currentColor" stroke-width="1.3"/>',
38
+ };
39
+ export function ic(name, size = 14) {
40
+ const d = ICONS[name];
41
+ if (!d) return '';
42
+ return `<svg class="ic" viewBox="0 0 16 16" width="${size}" height="${size}" aria-hidden="true">`
43
+ + `<path d="${d}" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>`
44
+ + `${ICON_EXTRA[name] || ''}</svg>`;
45
+ }
46
+
47
+ export function esc(s) {
48
+ return String(s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
49
+ }
50
+
51
+ export const LANG = (() => {
52
+ try {
53
+ const saved = JSON.parse(localStorage.getItem('kb.set.lang'));
54
+ if (saved === 'ru' || saved === 'en') return saved;
55
+ } catch { }
56
+ return (navigator.language || 'ru').toLowerCase().startsWith('ru') ? 'ru' : 'en';
57
+ })();
58
+ const I18N_RU = {
59
+ '(grooming mode):': '(режим «разбор бэклога»):',
60
+ '+ add task': '+ добавить задачу',
61
+ '+ label': '+ лейбл',
62
+ '+ link': '+ связать',
63
+ '/path/to/the/folder — may be empty': '/путь/к/папке — можно пусто',
64
+ 'a folder on this computer — or just a task board without one': 'папка на этом компьютере — или просто доска-список задач без папки',
65
+ 'a server project needs an SSH host and a path': 'для серверного проекта обязательны SSH-хост и путь',
66
+ 'a skill with this name already exists': 'скилл с таким именем уже есть',
67
+ 'A task is ready for review': 'Задача готова к проверке',
68
+ 'About': 'О доске',
69
+ 'action menu (status, priority, labels, duplicate…)': 'меню действий (статус, приоритет, метки, дублировать…)',
70
+ 'Active': 'Активных',
71
+ 'Activity by day': 'Активность по дням',
72
+ 'Add': 'Добавить',
73
+ 'add': 'добавить',
74
+ 'Add a label to all': 'Добавить метку всем',
75
+ 'Add a subtask': 'Добавить подзадачу',
76
+ 'Add checklist item': 'Добавить чек-бокс',
77
+ 'Add project': 'Добавить проект',
78
+ 'Add subtask': 'Добавить подзадачу',
79
+ 'Add to the board': 'Добавить на доску',
80
+ 'Add your own project with the "+ Add project" row in the sidebar — or create a demo project with example tasks to look around. The demo can be deleted later in the project settings.': 'Заведи свой проект строкой «+ Добавить проект» в сайдбаре — или создай демо-проект с примерами задач, чтобы осмотреться. Демо потом удаляется в настройках проекта.',
81
+ 'Add…': 'Добавить…',
82
+ 'All clear — nothing needs your decision': 'Всё разобрано — ничего не ждёт твоего решения',
83
+ 'All projects': 'Все проекты',
84
+ 'all self-healed': 'всё починилось само',
85
+ 'All set!': 'Готово!',
86
+ 'Appearance': 'Оформление',
87
+ 'applies after page reload': 'применится после перезагрузки страницы',
88
+ 'Archive': 'Архивировать',
89
+ 'at': 'от',
90
+ 'Attach a screenshot': 'Приложить скриншот',
91
+ 'attachment': 'вложение',
92
+ 'attachments:': 'вложений:',
93
+ 'audio:': 'аудио:',
94
+ 'awaiting release': 'ждут релиза',
95
+ 'Back to the board': 'К доске',
96
+ 'Back up now': 'Сделать бэкап',
97
+ 'Backlog': 'Бэклог',
98
+ 'Backups': 'Бэкапы',
99
+ 'Basics': 'Основное',
100
+ 'Bell': 'Колокольчик',
101
+ 'Blocked': 'Заблокировано',
102
+ 'blocked by autoplay': 'заблокирован автоплеем',
103
+ 'blocked flag': 'блокировка',
104
+ 'board skill': 'скилл доски',
105
+ 'Board updates': 'Обновление доски',
106
+ 'board(s) will move to “Other”.': 'досок(и) переедут в «Прочее».',
107
+ 'Branch': 'Ветка',
108
+ 'browser': 'браузер',
109
+ 'built-in section — cannot be deleted': 'служебный раздел — удалить нельзя',
110
+ 'built-in section — the name cannot be changed': 'служебный раздел — имя менять нельзя',
111
+ 'Busiest around': 'Жарче всего около',
112
+ 'Calendar': 'Календарь',
113
+ 'Cancel': 'Отмена',
114
+ 'Cancel task': 'Отменить задачу',
115
+ 'Cancel the task': 'Отменить задачу',
116
+ 'Cancel the task? The issue will be closed as not planned.': 'Отменить задачу? Issue закроется как not planned.',
117
+ 'Cancelled': 'Отменено',
118
+ 'cannot check (no network, or the repo is private/not a git checkout)': 'не проверить (нет сети, либо репо приватный/без git)',
119
+ 'Card actions': 'Управление карточкой',
120
+ 'change status': 'сменить статус',
121
+ 'changed — not written to the file yet': 'изменено — в файл ещё не записано',
122
+ 'Chaos': 'Хаос',
123
+ 'Check': 'Проверить',
124
+ 'Check board is up': 'Проверочная доска поднята',
125
+ 'Check for updates': 'Проверить обновления',
126
+ 'Check the backup': 'Проверить бэкап',
127
+ 'checking git on the server…': 'проверяю git на сервере…',
128
+ 'Checking the file and starting the board…': 'Проверяю файл и поднимаю доску…',
129
+ 'checking…': 'проверяю…',
130
+ 'Chime': 'Перезвон',
131
+ 'Chord': 'Аккорд',
132
+ 'Claude can finish the setup for you — copy this and paste it into its chat:': 'Дальше настройку может провести сам Claude — скопируй это и вставь ему в чат:',
133
+ 'Claude Code skills live in skills/ — symlink or copy them into ~/.claude/skills:': 'Скиллы Claude Code лежат в папке skills/ — симлинк или копия в ~/.claude/skills:',
134
+ 'Claude is working on this task — fields are locked, comments are open': 'Задача в работе у Claude — поля заблокированы, комментарии доступны',
135
+ 'Claude will ask clarifying questions first and wait for your answer': 'Claude сперва задаст уточняющие вопросы по задаче и дождётся ответа',
136
+ 'Clear': 'Очистить',
137
+ 'Clear selection': 'Снять выделение',
138
+ 'Clear the error log?': 'Очистить журнал ошибок?',
139
+ 'Clear the log': 'Очистить журнал',
140
+ 'clear the mark': 'снять отметку',
141
+ 'Click': 'Щелчок',
142
+ 'click the rows above to assign your own shortcut (Esc cancels); the ones below are fixed': 'клик по верхним строкам — назначить свою комбинацию (Esc — отмена); нижние фиксированы',
143
+ 'click to assign your own shortcut': 'кликните, чтобы назначить свою комбинацию',
144
+ 'clone from a git URL…': 'клонировать по git-URL…',
145
+ 'cloning…': 'клонирую…',
146
+ 'Close': 'Закрыть',
147
+ 'close issue': 'закрытие issue',
148
+ 'close the card · clear the selection': 'закрыть карточку · снять выделение',
149
+ 'Close the check board': 'Закрыть проверочную доску',
150
+ 'closed': 'закрыт',
151
+ 'closes in': 'закроется через',
152
+ 'Code version': 'Версия кода',
153
+ 'comment': 'комментарий',
154
+ 'Comment…': 'Комментарий…',
155
+ 'commit': 'коммит',
156
+ 'Compact cards': 'Компактные карточки',
157
+ 'Completed': 'Доведено до конца',
158
+ 'Copied': 'Скопировано',
159
+ 'copy and paste into Claude chat — it will run the setup and ask you questions': 'скопируй и вставь Claude в чат — он проведёт настройку и задаст вопросы',
160
+ 'Copy link': 'Копировать ссылку',
161
+ 'Copy the backlog as a grooming job for Claude': 'Скопировать бэклог как задание на разбор для Claude',
162
+ 'Copy the path': 'Скопировать путь',
163
+ 'Copy the prompt': 'Скопировать промпт',
164
+ 'Copy the tasks as a job for Claude': 'Скопировать задачи как задание для Claude',
165
+ 'could not attach': 'не удалось приложить',
166
+ 'could not attach the screenshot': 'не удалось приложить скрин',
167
+ 'could not create it': 'не удалось создать',
168
+ 'Could not duplicate': 'Не удалось дублировать',
169
+ 'could not load': 'не удалось загрузить',
170
+ 'Could not load the dashboard.': 'Не удалось загрузить дашборд.',
171
+ 'could not read the file': 'не удалось прочитать файл',
172
+ 'Create': 'Создать',
173
+ 'Create (⌘/Ctrl+Enter)': 'Создать (⌘/Ctrl+Enter)',
174
+ 'Create a project folder': 'Создать папку проекта',
175
+ 'Create an example project with tasks in every column? It shows links, checklists and labels, and can be deleted with one click in the project settings.': 'Создать проект-пример с задачами по всем колонкам? Он показывает связи, чек-листы и метки, и удаляется одной кнопкой в настройках проекта.',
176
+ 'Create demo': 'Создать демо',
177
+ 'Create demo project': 'Создать демо-проект',
178
+ 'create issue': 'создание issue',
179
+ 'Created': 'Создана',
180
+ 'created': 'создано',
181
+ 'created ✓': 'создан ✓',
182
+ 'creating issue…': 'issue создаётся…',
183
+ 'creating…': 'создаю…',
184
+ 'Custom deploy skill': 'Свой deploy-скилл',
185
+ 'd': 'д',
186
+ 'd ago': 'д назад',
187
+ 'Dark': 'Тёмная',
188
+ 'Dashboard': 'Дашборд',
189
+ 'Database': 'База данных',
190
+ 'Database backups': 'Бэкапы базы',
191
+ 'Database path': 'Путь к базе',
192
+ 'Days': 'Дни',
193
+ 'Default dashboard range': 'Окно дашборда по умолчанию',
194
+ 'Delete': 'Удалить',
195
+ 'delete': 'удалить',
196
+ 'Delete demo project entirely': 'Удалить демо-проект целиком',
197
+ 'delete issue': 'удаление issue',
198
+ 'delete item': 'удалить пункт',
199
+ 'Delete section': 'Удалить раздел',
200
+ 'Delete the comment': 'Удалить комментарий',
201
+ 'Delete the comment?': 'Удалить комментарий?',
202
+ 'Delete the demo project and all its tasks?': 'Удалить демо-проект со всеми его задачами?',
203
+ 'delete the selected cards / the one under the cursor': 'удалить выбранные / карточку под курсором',
204
+ 'delete the selection / the card under the cursor': 'удалить выбранные / карточку под курсором',
205
+ 'Delete the skill': 'Удалить скилл',
206
+ 'Delete the task permanently (comments included)?': 'Удалить задачу навсегда (вместе с комментариями)?',
207
+ 'Demo project': 'Демо-проект',
208
+ 'Deploy skill': 'Deploy-скилл',
209
+ 'Deploy skills': 'Деплой-скиллы',
210
+ 'Deploy skills of the projects': 'Деплой-скиллы проектов',
211
+ 'Deploying': 'Деплою',
212
+ 'Description': 'Описание',
213
+ 'Description…': 'Описание…',
214
+ 'Details are in the README. Enjoy!': 'Подробности — в README. Хорошей работы!',
215
+ 'dev branch': 'Ветка dev',
216
+ 'dev has no commits beyond main': 'в dev нет коммитов сверх main',
217
+ 'Diagnostics': 'Диагностика',
218
+ 'did not work': 'не получилось',
219
+ 'Did not work out': 'Не получилось',
220
+ 'did not work — select and copy manually': 'не вышло — выдели и скопируй вручную',
221
+ 'Ding': 'Динь',
222
+ 'Do the tasks from': 'Сделай задачи из',
223
+ 'Do the tasks with the to-do status:': 'Сделай задачи со статусом сделать:',
224
+ 'Doing': 'Делаю',
225
+ 'Domain': 'Домен',
226
+ 'Domain (to verify after deploy)': 'Домен (для проверки после деплоя)',
227
+ 'Done': 'Готово',
228
+ 'Done by day': 'Сделано по дням',
229
+ 'Done in the last 7 days': 'Готово за последние 7 дней',
230
+ 'Download backup': 'Скачать бэкап',
231
+ 'draw lines between linked cards; when off, links show as a chain badge': 'рисовать линии между связанными карточками; выключены — связь видна значком-звеном',
232
+ 'Drop': 'Капля',
233
+ 'Duplicate': 'Дублировать',
234
+ 'edit issue': 'правка issue',
235
+ 'empty': 'пусто',
236
+ 'empty = no deploy / deploy = the generic one': 'пусто = без деплоя / deploy = универсальный',
237
+ 'empty — sync is off, the board runs locally': 'пусто — синк выключен, доска работает локально',
238
+ 'Enable sync': 'Включить синк',
239
+ 'enter the folder path…': 'путь к папке вручную…',
240
+ 'Errors': 'Ошибки',
241
+ 'errors': 'ошибок',
242
+ 'events': 'событ.',
243
+ 'Every task can be mirrored to an issue and every project to a GitHub Project. Requires the gh CLI authenticated with the project scope (gh auth login && gh auth refresh -s project). You can also enable this later in Settings.': 'Каждая задача может зеркалиться в issue, а проект — в GitHub Project. Нужен установленный gh CLI с правом project (gh auth login && gh auth refresh -s project). Это можно включить и позже — в Настройках.',
244
+ 'Everything lands here: a server failure, a failed sync op, an error in the browser. Empty means the board runs clean.': 'Сюда попадает всё: сбой сервера, упавшая синхронизация, ошибка в браузере. Пусто — значит доска работает без сбоев.',
245
+ 'everything synced ✓': 'всё синхронизировано ✓',
246
+ 'failed in the queue': 'ошибок в очереди',
247
+ 'fetching…': 'загружаю…',
248
+ 'File': 'Файл',
249
+ 'folder': 'папка',
250
+ 'folder is gone — click to archive': 'папка удалена — клик чтобы архивировать',
251
+ 'Fri': 'Пт',
252
+ 'free for everyone, nothing switched off — if it earns you money, $12 a year per person keeps it maintained': 'бесплатна для всех, ничего не отключено — если она зарабатывает тебе деньги, $12 в год за человека помогают её поддерживать',
253
+ 'from': 'от',
254
+ 'from created to done': 'от создания до готово',
255
+ 'General': 'Основные',
256
+ 'gh CLI found and authenticated ✓': 'gh CLI найден и авторизован ✓',
257
+ 'gh CLI missing or not authenticated — you can enable sync later': 'gh CLI не найден или не авторизован — синк можно включить позже',
258
+ 'GitHub is not set up (gh auth needed)': 'GitHub не настроен (нужен gh auth)',
259
+ 'GitHub sync': 'Синхронизация с GitHub',
260
+ 'GitHub sync (optional)': 'Синхронизация с GitHub (опционально)',
261
+ 'GitHub sync queue': 'Очередь синка с GitHub',
262
+ 'give the folder path': 'укажи путь к папке',
263
+ 'give the git URL': 'укажи git-URL',
264
+ 'Go to project': 'Перейти к проекту',
265
+ 'goal': 'цель',
266
+ 'Goal for the day': 'Цель на день',
267
+ 'goals by time horizon': 'цели по периодам времени',
268
+ 'Got it': 'Понятно',
269
+ 'Groom the backlog (grooming mode):': 'Разбери бэклог (режим «разбор бэклога»):',
270
+ 'Groom the backlog of': 'Разбери бэклог из',
271
+ 'h': 'ч',
272
+ 'h ago': 'ч назад',
273
+ 'h without movement': 'ч без движения',
274
+ 'Half a year': 'Полгода',
275
+ 'hide the description preview — more cards fit on screen': 'скрывать превью описания — на экран влезает больше',
276
+ 'Hide the project from the list': 'Скрыть проект из списка',
277
+ 'high': 'высокий',
278
+ 'Horizon': 'Горизонт',
279
+ 'Hotkeys': 'Горячие клавиши',
280
+ 'in 6 months': 'за полгода',
281
+ 'in dev': 'в dev',
282
+ 'in review — waiting for you': 'на проверке — ждёт тебя',
283
+ 'in the backlog': 'в бэклоге',
284
+ 'Install the kb command for Claude like this:': 'Команда kb для Claude ставится так:',
285
+ 'Interface language': 'Язык интерфейса',
286
+ 'is gone from disk. Archive the project? Tasks are kept.': 'удалена с диска. Архивировать проект? Задачи сохранятся.',
287
+ 'it has to go through work first': 'сначала через работу',
288
+ 'It will be checked and opened as a SEPARATE board on a neighbouring port. This board and its tasks will not change.': 'Он будет проверен и открыт ОТДЕЛЬНОЙ доской на соседнем порту. Текущая доска и её задачи не изменятся.',
289
+ 'item': 'пункт',
290
+ 'Jot a task down quickly — it lands in the project you pick.': 'Быстро запиши задачу — она уйдёт в выбранный проект.',
291
+ 'just now': 'только что',
292
+ 'KB': 'КБ',
293
+ 'keep changes local and catch up with GitHub later': 'копить изменения локально и догнать GitHub позже',
294
+ 'keep the right panel visible even with no task selected': 'держать панель справа, даже когда задача не выбрана',
295
+ 'Key (slug + task prefix)': 'Ключ (slug + префикс задач)',
296
+ 'key: lowercase latin letters, digits and hyphens': 'ключ: латиница строчными, цифры и дефис',
297
+ 'Label': 'Метка',
298
+ 'Labels': 'Метки',
299
+ 'labels': 'метки',
300
+ 'last 7 days:': 'за 7 дней:',
301
+ 'last at': 'последний в',
302
+ 'Last opened': 'Последний открытый',
303
+ 'last successful sync:': 'последний успешный синк:',
304
+ 'less': 'меньше',
305
+ 'Let’s jot down some tasks?': 'Накидаем задач?',
306
+ 'Life': 'Жизнь',
307
+ 'Light': 'Светлая',
308
+ 'light, dark or in Claude colors': 'светлая, тёмная или в цветах Claude',
309
+ 'line break in the comment field': 'перенос строки в поле комментария',
310
+ 'Link': 'Связать',
311
+ 'Link a parent task': 'Связать родительскую задачу',
312
+ 'Link a task': 'Связать задачу',
313
+ 'Link lines': 'Линии связей',
314
+ 'Link parent task': 'Связать родительскую задачу',
315
+ 'Link task': 'Связать задачу',
316
+ 'linked to:': 'связано:',
317
+ 'Load the shared version': 'Загрузить общую версию',
318
+ 'loaded from GitHub': 'загружено с GitHub',
319
+ 'loaded from the installed package': 'загружено из установленного пакета',
320
+ 'Local': 'Локальные',
321
+ 'Local + server': 'Локальный + сервер',
322
+ 'local copy of the code + deploy to a server over ssh': 'локальная копия кода + деплой на сервер по ssh',
323
+ 'Local folder': 'Локальная папка',
324
+ 'Local project': 'Локальный',
325
+ 'Lock off: the choice resets after creating': 'Замок выключен: после создания выбор сбрасывается',
326
+ 'Lock on: status/priority/labels are kept after creating': 'Замок включён: статус/приоритет/метки сохраняются после создания',
327
+ 'Lock the selection': 'Замок выбора',
328
+ 'low': 'низкий',
329
+ 'm': 'м',
330
+ 'm ago': 'м назад',
331
+ 'mark as done': 'отметить выполненным',
332
+ 'matches GitHub': 'совпадает с GitHub',
333
+ 'matches main ✓': 'совпадает с main ✓',
334
+ 'max': 'макс',
335
+ 'MB': 'МБ',
336
+ 'me': 'я',
337
+ 'medium': 'средний',
338
+ 'min': 'мин',
339
+ 'Mon': 'Пн',
340
+ 'Month': 'Месяц',
341
+ 'Months': 'Месяцы',
342
+ 'more': 'больше',
343
+ 'move a card (between columns / by position)': 'перенести карточку (между колонками / по позиции)',
344
+ 'move the selection across cards': 'перемещать выделение по карточкам',
345
+ 'Move to status': 'Перевести в статус',
346
+ 'Name': 'Имя',
347
+ 'name (created in ~/claude-projects)': 'имя (создастся в ~/claude-projects)',
348
+ 'name of the new section': 'название нового раздела',
349
+ 'name: latin letters, digits, hyphen': 'имя: латиница, цифры, дефис',
350
+ 'name: latin letters, digits, hyphen, underscore only': 'имя: только латиница, цифры, дефис и подчёркивание',
351
+ 'needs attention': 'требует внимания',
352
+ 'new': 'новая',
353
+ 'new folder': 'новая папка',
354
+ 'New project folder': 'Новая папка проекта',
355
+ 'New section': 'Новый раздел',
356
+ 'New section…': 'Новый раздел…',
357
+ 'New skill': 'Новый скилл',
358
+ 'new task': 'новая задача',
359
+ 'New task +': 'Новая задача +',
360
+ 'New task in': 'Новая задача в',
361
+ 'new tasks go to “Backlog” or “To do” only': 'новую задачу заводим только в «Бэклог» или «Сделать»',
362
+ 'Next': 'Далее',
363
+ 'next project (in the sidebar)': 'следующий проект (в сайдбаре)',
364
+ 'no activity yet': 'пока нет событий',
365
+ 'no backups yet': 'пока нет бэкапов',
366
+ 'Claude moves these columns itself': 'эти колонки двигает сам Claude',
367
+ 'no file': 'файла нет',
368
+ 'not deleted': 'не удалено',
369
+ 'not installed': 'не установлен',
370
+ 'not set': 'не задан',
371
+ 'not set — no deploy': 'не задан — деплоя нет',
372
+ 'not there': 'нет файла',
373
+ 'nothing is written to disk until you save': 'на диск ничего не записано, пока не сохранишь',
374
+ 'nothing to save': 'нечего сохранять',
375
+ 'Other Claude skills': 'Прочие скиллы Claude',
376
+ 'Overwrite': 'Перезаписать',
377
+ 'Overwrite this file?': 'Перезаписать этот файл?',
378
+ 'own skill': 'свой скилл',
379
+ 'Paths & deploy': 'Пути и деплой',
380
+ 'Revert': 'Вернуть как в файле',
381
+ 'Save to the file': 'Сохранить в файл',
382
+ 'saved ✓': 'сохранено ✓',
383
+ 'saved ✓ · the previous version is kept as': 'сохранено ✓ · прежняя версия осталась как',
384
+ 'shared': 'общий',
385
+ 'shared deploy skill': 'общий деплой-скилл',
386
+ 'shared skill': 'общий скилл',
387
+ 'Skill file': 'Файл скилла',
388
+ 'Skills': 'Скиллы',
389
+ 'Skills are the instructions Claude reads. They live outside the board, in ~/.claude/skills, and the board only shows and updates them.': 'Скиллы — инструкции, по которым работает Claude. Они лежат вне доски, в ~/.claude/skills; доска их только показывает и обновляет.',
390
+ 'The board': 'Доска',
391
+ 'the board has no shared version of this skill — it is your own': 'у доски нет общей версии этого скилла — он ваш собственный',
392
+ 'the field above takes a skill name — “deploy” is the shared one': 'в поле выше — имя скилла; «deploy» это общий',
393
+ 'The project has no local folder — set one above and the notes will show up here.': 'У проекта нет локальной папки — укажи её выше, и заметки появятся здесь.',
394
+ 'The project notes and the deploy skill — Claude opens them before touching the code. Paths are absolute: copy one and paste it into the chat.': 'Заметки проекта и деплой-скилл — Claude открывает их до того, как трогать код. Пути полные: скопируй и вставь в чат.',
395
+ 'the skill Claude follows to ship this project; “deploy” is the shared one. A new one is created in Settings → Skills.': 'каким скиллом Claude выкатывает этот проект; «deploy» — общий. Свой заводится в «Настройки → Скиллы».',
396
+ 'the skill is not installed — the file will be created on save': 'скилл не установлен — файл появится при сохранении',
397
+ 'this is a link — only the link is removed, the file it points to stays': 'это ссылка — снимется только она, файл по ту сторону останется',
398
+ 'this is a symlink — saving rewrites the file it points to:': 'это ссылка — сохранение перезапишет файл, на который она ведёт:',
399
+ 'This project is gone': 'Этого проекта больше нет',
400
+ 'this skill was already on disk — added it to the list': 'такой скилл уже лежал на диске — добавил его в список',
401
+ 'update with: npm install -g local-kanban@latest': 'обновить: npm install -g local-kanban@latest',
402
+ Archive: 'Архив',
403
+ 'Archived boards are hidden from the sidebar and left out of every dashboard number. Their tasks are kept.': 'архивные доски спрятаны из сайдбара и не считаются ни в одной цифре дашборда. Задачи при этом сохраняются.',
404
+ 'Bring back': 'вернуть',
405
+ 'nothing archived': 'в архиве пусто',
406
+ 'The last automatic snapshot failed': 'последний автоматический снимок не сделался',
407
+ 'Last successful snapshot': 'последний удачный снимок',
408
+ 'Images and attachments are mirrored next to the snapshots, not inside the downloaded file.': 'картинки и вложения лежат копией рядом со снимками, а не внутри скачиваемого файла.',
409
+ 'The snapshot covers the database only.': 'снимок покрывает только базу.',
410
+ 'no errors ✓': 'ошибок не было ✓',
411
+ 'no folder — just a task list': 'без папки — просто список задач',
412
+ 'no git on the server — the project was not created': 'на сервере нет git — проект не создан',
413
+ 'No git repository on the server at': 'На сервере нет git-репозитория в',
414
+ 'no label': 'без метки',
415
+ 'no local copy — Claude works over ssh; git required on the server': 'без локальной копии — Claude работает по ssh; на сервере нужен git',
416
+ 'no manual tasks in backlog/to do yet': 'пока нет ручных задач в бэклоге/сделать',
417
+ 'no movement — the chat is probably closed': 'нет движения — чат, вероятно, закрыт',
418
+ 'no priority': 'без приоритета',
419
+ 'No priority': 'Без приоритета',
420
+ 'No sound': 'Без звука',
421
+ 'no successful syncs yet': 'успешных синков ещё не было',
422
+ 'noclaude label — done by hand': 'Метка noclaude — сделано вручную',
423
+ 'not created': 'не создан',
424
+ 'not on the board yet': 'ещё не на доске',
425
+ 'not saved': 'не сохранилось',
426
+ 'nothing finished yet': 'пока нет завершённых задач',
427
+ 'nothing found': 'ничего не найдено',
428
+ 'of': 'из',
429
+ 'Open': 'Открыть',
430
+ 'open the all-projects board': 'открыть доску всех проектов',
431
+ 'open the selected card': 'открыть выделенную карточку',
432
+ 'Or do it by hand. Everything is already installed with the package:': 'Или руками. Всё уже приехало вместе с пакетом:',
433
+ 'Or do it by hand. Install the kb command for Claude like this:': 'Или руками. Команда kb для Claude ставится так:',
434
+ 'Other': 'Прочее',
435
+ 'owner (GitHub user)': 'owner (юзер GitHub)',
436
+ 'Owner and issues repository': 'Owner и репозиторий issues',
437
+ 'owner/repo for issues': 'owner/repo для issues',
438
+ 'page loaded at': 'страница загружена в',
439
+ 'parent task': 'родительская задача',
440
+ 'paste a screenshot into the open task or a comment': 'вставить скриншот в открытую задачу или комментарий',
441
+ 'Paths & deploy ▴': 'Пути и деплой ▴',
442
+ 'Paths & deploy ▾': 'Пути и деплой ▾',
443
+ 'Pause sync': 'Пауза синка',
444
+ 'permanently?': 'навсегда?',
445
+ 'pick a project': 'выбери проект',
446
+ 'pick a project first': 'сначала выбери проект',
447
+ 'piling up': 'копится',
448
+ 'Pin': 'Закрепить',
449
+ 'Pin to board': 'Закрепить на доске',
450
+ 'Pin to the top': 'Закрепить наверху',
451
+ 'Pinned': 'Закреплённые',
452
+ 'pinned': 'закреплён',
453
+ 'Planning': 'Планирование',
454
+ 'pm2 processes (comma-separated)': 'pm2-процессы (через запятую)',
455
+ 'Pop': 'Поп',
456
+ 'Prep': 'Подготовка',
457
+ 'press a key…': 'нажмите…',
458
+ 'previous project (in the sidebar)': 'предыдущий проект (в сайдбаре)',
459
+ 'Priority': 'Приоритет',
460
+ 'priority': 'приоритет',
461
+ 'priority:': 'приоритет:',
462
+ 'Priority:': 'Приоритет:',
463
+ 'Priority: none': 'Приоритет: нет',
464
+ 'project': 'проект',
465
+ 'Project code': 'Код проекта',
466
+ 'Project key': 'Код проекта',
467
+ 'Project name': 'Имя проекта',
468
+ 'Project settings': 'Настройки проекта',
469
+ 'Projects': 'Проекты',
470
+ 'projects': 'проектов',
471
+ 'queued': 'в очереди',
472
+ 'queued and in progress': 'в очереди и работе',
473
+ 'quick task capture': 'быстрое создание задач',
474
+ 're-read branch state from GitHub': 'перечитать состояние веток с GitHub',
475
+ 're-read the board code right now': 'перечитать код доски прямо сейчас',
476
+ 'ready to work on': 'готовы к работе',
477
+ 'Recent activity': 'Последние события',
478
+ 'Recent errors': 'Последние ошибки',
479
+ 'recovered on retry': 'починилось повторной попыткой',
480
+ 'related': 'связана',
481
+ 'Read': 'Открыть',
482
+ 'Reload now': 'Обновить сейчас',
483
+ 'Reload the page': 'Обновить страницу',
484
+ 'remove': 'убрать',
485
+ 'remove link': 'убрать связь',
486
+ 'reopen issue': 'переоткрытие issue',
487
+ 'Reset to defaults': 'Сбросить по умолчанию',
488
+ 'Retry failed': 'Повторить ошибки',
489
+ 'Return to work': 'Вернуть в работу',
490
+ 'Review': 'Проверяю',
491
+ 'Review notifications': 'Уведомления о проверке',
492
+ 'right-click on a card': 'правый клик по карточке',
493
+ 'Run git init? Without git a server project cannot be created.': 'Инициализировать git init? Без git серверный проект не заводится.',
494
+ 'Run the kanban skill': 'Запусти скилл kanban',
495
+ 'running': 'работает',
496
+ 'running out of tasks soon — time to plan': 'скоро встанет без задач — пора планировать',
497
+ 'Russian': 'Русский',
498
+ 's ago': 'с назад',
499
+ 'Sat': 'Сб',
500
+ 'Save': 'Сохранить',
501
+ 'save edits in the open card': 'сохранить правки в открытой карточке',
502
+ 'screenshot': 'скрин',
503
+ 'search boards…': 'поиск доски…',
504
+ 'Search tasks…': 'Поиск задач…',
505
+ 'Search, or the title of a new task…': 'Поиск или заголовок новой задачи…',
506
+ 'Section': 'Раздел',
507
+ 'section name': 'название раздела',
508
+ 'Sections': 'Разделы',
509
+ 'Sections group boards in the sidebar. A new name applies to every board in the section.': 'Разделы группируют доски в сайдбаре. Новое имя применится ко всем доскам раздела.',
510
+ 'Select a task — details will show up here': 'Выберите задачу — её детали появятся здесь',
511
+ 'Selected': 'Выбрано',
512
+ 'self-healed': 'починилось само',
513
+ 'Send back': 'Вернуть',
514
+ 'server': 'сервер',
515
+ 'Server only': 'Только сервер',
516
+ 'Server path': 'Путь на сервере',
517
+ 'server signal:': 'сигнал сервера:',
518
+ 'Server uptime': 'Аптайм сервера',
519
+ 'set via env KB_GH_OWNER/KB_GH_REPO — edit your launch config': 'задано через env KB_GH_OWNER/KB_GH_REPO — правится в конфиге запуска',
520
+ 'Settings': 'Настройки',
521
+ 'Setup via Claude': 'Настройка через Claude',
522
+ 'Skip': 'Пропустить',
523
+ 'Skip — work locally': 'Пропустить — работать локально',
524
+ 'Sound diagnostics': 'Диагностика звука',
525
+ 'Sound for Done': 'Звук на «Готово»',
526
+ 'Sound for Review': 'Звук на «Проверяю»',
527
+ 'Sound notifications': 'Звуковые уведомления',
528
+ 'sound off': 'звук выкл',
529
+ 'sound on Review and Done': 'звук при «Проверяю» и «Готово»',
530
+ 'sounds missed:': 'пропущено звуков:',
531
+ 'ssh alias or address': 'алиас/адрес для ssh',
532
+ 'SSH host': 'SSH-хост',
533
+ 'Stalled': 'Висит',
534
+ 'Start screen': 'Стартовый экран',
535
+ 'Start working': 'Начать работу',
536
+ 'Status': 'Статус',
537
+ 'Streak': 'Серия',
538
+ 'status': 'статус',
539
+ 'Status:': 'Статус:',
540
+ 'stuck': 'висит',
541
+ 'subtask': 'подзадача',
542
+ 'Sun': 'Вс',
543
+ 'Sync': 'Синхронизация',
544
+ 'sync': 'синк',
545
+ 'sync is off — the board runs locally': 'синк выключен — доска работает локально',
546
+ 'sync paused': 'синк на паузе',
547
+ 'sync: ok': 'синк: ок',
548
+ 'System': 'Системная',
549
+ 'system notification when a task reaches Review': 'системное уведомление, когда задача уезжает в «Проверяю»',
550
+ 'tag': 'тег',
551
+ 'taken': 'занято',
552
+ 'Task cycle': 'Цикл задачи',
553
+ 'Task sidebar always open': 'Сайдбар задачи всегда открыт',
554
+ 'Task title': 'Заголовок задачи',
555
+ 'tasks': 'задач',
556
+ 'Tasks are kept, the project leaves the list.': 'Задачи сохранятся, проект скроется из списка.',
557
+ 'Tasks closed:': 'Закрыто задач:',
558
+ 'Tasks · projects': 'Задач · проектов',
559
+ 'the arrow keys are taken': 'стрелки заняты',
560
+ 'The board is empty': 'Доска пока пустая',
561
+ 'the board is fresh ✓': 'доска свежая ✓',
562
+ 'the kb command came with it, and the wizard copied the Claude Code skills into ~/.claude/skills. To update later:': 'команда kb приехала вместе с ним, а скиллы Claude Code мастер скопировал в ~/.claude/skills. Обновиться потом:',
563
+ 'The browser blocked sound until the first click — click to enable': 'Браузер заблокировал звук до первого клика — нажмите, чтобы включить',
564
+ 'the fix commit': 'коммит исправления',
565
+ 'The project folder': 'Папка проекта',
566
+ 'the project needs a name': 'нужно имя проекта',
567
+ 'The uploaded file opens as a separate board on a neighbouring port — this board and its tasks stay untouched.': 'Загруженный файл откроется отдельной доской на соседнем порту — текущая доска и её задачи не меняются.',
568
+ 'Theme': 'Тема',
569
+ 'This is a local kanban board built to work in tandem with Claude Code. A couple of steps and you are ready.': 'Это локальная канбан-доска для работы в паре с Claude Code. Пара шагов — и можно работать.',
570
+ 'this month': 'за месяц',
571
+ 'This snapshot has already been rotated out — refreshing the list.': 'Этот снимок уже удалён ротацией — обновляю список.',
572
+ 'This task no longer exists — it was deleted.': 'Этой задачи больше нет — её удалили.',
573
+ 'this takes a few seconds': 'это занимает несколько секунд',
574
+ 'this week': 'за неделю',
575
+ 'this year': 'за год',
576
+ 'Thu': 'Чт',
577
+ 'time spent': 'время в работе',
578
+ 'The board is not responding — try again in a moment.': 'доска не отвечает — попробуй ещё раз через мгновение.',
579
+ 'not saved': 'не сохранено',
580
+ 'The board could not reach the server — the change was not saved': 'доска не достучалась до сервера — правка не сохранена',
581
+ 'capped: the task sat in a working column too long': 'урезано: задача слишком долго лежала в рабочей колонке',
582
+ 'To do': 'Сделать',
583
+ 'Today': 'Сегодня',
584
+ 'Top projects': 'Топ проектов',
585
+ 'Tue': 'Вт',
586
+ 'Unpin': 'Открепить',
587
+ 'unresolved': 'без ответа',
588
+ 'untitled': 'без названия',
589
+ 'up to date ✓': 'последняя версия ✓',
590
+ 'update available': 'есть обновление',
591
+ 'update with: npm run update': 'обнови командой: npm run update',
592
+ 'Upload': 'Загрузить',
593
+ 'used by': 'используют',
594
+ 'Using the board at work': 'Пользуешься доской в работе',
595
+ 'Waiting for you': 'Ждёт тебя',
596
+ 'Wed': 'Ср',
597
+ 'Week': 'Неделя',
598
+ 'Weeks': 'Недели',
599
+ 'Welcome!': 'Добро пожаловать!',
600
+ 'went back to work': 'возвращалась в работу',
601
+ 'Show the rest': 'Развернуть остальные',
602
+ Collapse: 'Свернуть',
603
+ 'What are we working on?': 'Чем займёмся?',
604
+ 'What Claude reads before working': 'Что Claude читает перед работой',
605
+ 'What do we note before it slips?': 'Что запишем, пока не забыл?',
606
+ 'What do we take on?': 'За что возьмёмся?',
607
+ 'What goes into the queue?': 'Что закинем в работу?',
608
+ 'What is wrong? (goes into the task)': 'Что не так? (попадёт в задачу)',
609
+ 'What needs doing?': 'Что нужно сделать?',
610
+ 'What shall we turn into a task?': 'Что превратим в задачу?',
611
+ 'what this project is, notes…': 'что за проект, заметки…',
612
+ 'what to do': 'что сделать',
613
+ 'what to open on launch': 'что открывать при запуске',
614
+ 'What’s on for today?': 'Что на сегодня?',
615
+ 'What’s on your mind?': 'Что не даёт покоя?',
616
+ 'When work happens': 'Когда идёт работа',
617
+ 'Where do we start?': 'С чего начнём?',
618
+ 'Which chaos do we sort out?': 'Какой хаос разгребём?',
619
+ 'with the to-do status:': 'со статусом сделать:',
620
+ 'Work time': 'Время в работе',
621
+ 'working columns are moved by Claude only': 'рабочие колонки двигает только Claude',
622
+ 'Year': 'Год',
623
+ 'Years': 'Годы',
624
+ 'Yesterday': 'Вчера',
625
+ '“Ready to work on” are tasks in the To do column — they can be picked up right now. “In the backlog” are ideas not yet selected for work.': '«Готово к работе» — задачи в колонке «Сделать», их можно брать сейчас. «В бэклоге» — идеи, ещё не отобранные в работу.',
626
+ '‹ Back': '‹ Назад',
627
+ '↑ High': '↑ Высокий',
628
+ '↓ Low': '↓ Низкий',
629
+ '↩ Send back to work': '↩ Вернуть в работу',
630
+ '✓ Accept task': '✓ Принять задачу',
631
+ '⟳ a new version is available': '⟳ доступна новая версия',
632
+ '+ Add project': '+ Добавить проект',
633
+ '+ New section': '+ Новый раздел',
634
+ '+ New skill': '+ Новый скилл',
635
+ '= Medium': '= Средний',
636
+ };
637
+ export function tr(s) { return LANG === 'ru' ? (I18N_RU[s] ?? s) : s; }
638
+
639
+ for (const [was, now] of [['kb.slug', 'kb.ui.project'], ['kb.lastProject', 'kb.ui.lastProject']]) {
640
+ const v = localStorage.getItem(was);
641
+ if (v !== null && localStorage.getItem(now) === null) localStorage.setItem(now, v);
642
+ if (v !== null) localStorage.removeItem(was);
643
+ }
644
+
645
+ // static strings of index.html are English in the markup; this patches them to Russian
646
+ // (the script loads at the end of body, so the DOM is already there)
647
+ function applyLang() {
648
+ if (LANG !== 'ru') return;
649
+ document.documentElement.lang = 'ru';
650
+ const set = (sel, prop, src) => { const el = document.querySelector(sel); if (el) el[prop] = tr(src); };
651
+ set('.sidebar-head', 'textContent', 'Projects');
652
+ set('#search', 'placeholder', 'Search tasks…');
653
+ set('#new-task', 'textContent', 'New task +');
654
+ set('#sound-badge', 'innerHTML', `${ic('soundOff', 13)} ${esc(tr('sound off'))}`);
655
+ set('#drawer-empty', 'textContent', 'Select a task — details will show up here');
656
+ set('#d-lock-note', 'innerHTML', `${ic('lock', 13)} ${esc(tr('Claude is working on this task — fields are locked, comments are open'))}`);
657
+ set('#d-title', 'placeholder', 'Task title');
658
+ set('#d-desc', 'placeholder', 'Description…');
659
+ set('#c-input', 'placeholder', 'Comment…');
660
+ set('#d-accept-btn', 'textContent', '✓ Accept task');
661
+ set('#d-return-icon', 'title', 'Return to work');
662
+ set('#drawer-menu', 'title', 'Card actions');
663
+ set('#drawer-close', 'title', 'Close');
664
+ set('#project-menu', 'title', 'Project settings');
665
+ set('#d-checklist-add', 'title', 'Add checklist item');
666
+ set('#week-stats', 'title', 'Done in the last 7 days');
667
+ set('#sound-badge', 'title', 'The browser blocked sound until the first click — click to enable');
668
+ set('#sync-badge', 'title', 'GitHub sync queue');
669
+ set('#offline-badge', 'textContent', 'not saved');
670
+ set('#offline-badge', 'title', 'The board could not reach the server — the change was not saved');
671
+ }
672
+ applyLang();
673
+
674
+ export const ALL_STATUSES = [
675
+ ['backlog', tr('Backlog')],
676
+ ['todo', tr('To do')],
677
+ ['prep', tr('Prep')],
678
+ ['doing', tr('Doing')],
679
+ ['deploy', tr('Deploying')],
680
+ ['review', tr('Review')],
681
+ ['done', tr('Done')],
682
+ ['cancelled', tr('Cancelled')],
683
+ ];
684
+ export const PRI_ICON = { 1: ['↓', 'p1', tr('low')], 2: ['=', 'p2', tr('medium')], 3: ['↑', 'p3', tr('high')] };
685
+ export const PRI_LEVELS = [[3, tr('↑ High')], [2, tr('= Medium')], [1, tr('↓ Low')], [0, tr('No priority')]];
686
+ export let LABEL_COLORS = {};
687
+ export let LABEL_SELECTABLE = [];
688
+ export async function loadLabelPalette() {
689
+ const d = await api('GET', '/api/labels');
690
+ LABEL_COLORS = d.palette || {};
691
+ LABEL_SELECTABLE = d.selectable || [];
692
+ }
693
+ export const TITLE_LABEL_WORDS = {
694
+ баг: 'bug', bug: 'bug',
695
+ фича: 'feature', feature: 'feature', фикча: 'feature',
696
+ ui: 'ui', юай: 'ui', юй: 'ui',
697
+ доки: 'documentation', docs: 'documentation', документация: 'documentation', дока: 'documentation',
698
+ вопрос: 'question', question: 'question',
699
+ noclaude: 'noclaude', руками: 'noclaude', вручную: 'noclaude',
700
+ };
701
+ export const DONE_LIMIT = 30;
702
+ export const ALL = '*';
703
+ export const DASH = '#dash';
704
+ export const SETTINGS = '#settings';
705
+ export const HORIZON = '#horizon';
706
+ export const CALENDAR = '#calendar';
707
+ export const CHAOS = '#chaos';
708
+ export const HIDDEN_SECTIONS = [HORIZON, CALENDAR];
709
+ export const SIDEBAR_SECTIONS = [[HORIZON, 'Horizon'], [CALENDAR, 'Calendar'], [CHAOS, 'Chaos']]
710
+ .filter(([slug]) => !HIDDEN_SECTIONS.includes(slug))
711
+ .map(([slug, name]) => [slug, tr(name)]);
712
+
713
+ export const state = {
714
+ projects: [], slug: DASH,
715
+ tasks: [], drawerKey: null, search: '',
716
+ searchApplied: '',
717
+ projSettings: null,
718
+ folders: { unregistered: [], missing: [] },
719
+ categories: [],
720
+ dashRange: 'week',
721
+ kbCursor: null,
722
+ };
723
+
724
+ export function $(id) { return document.getElementById(id); }
725
+
726
+ export const SETUP_PROMPT = {
727
+ ru: `Запусти скилл kanban.
728
+
729
+ Ты помогаешь мне настроить только что установленную доску local-kanban. Проведи настройку сам, задавая мне вопросы ПО ОДНОМУ и дожидаясь ответа — не додумывай за меня.
730
+
731
+ 1. Проверь, что команда kb работает (kb p). Если нет — скажи, что выполнить, и дождись меня.
732
+ 2. Спроси, зеркалить ли задачи в GitHub. Если да — проверь gh auth status и право project, спроси owner и репозиторий, включи синк и убедись, что очередь не встала (kb sync).
733
+ 3. Спроси, какие проекты завести. По каждому уточни: имя, где лежит код, есть ли сервер и деплой. Заведи их и покажи kb info по каждому.
734
+ 4. В конце покажи, что получилось, и коротко объясни рабочий цикл: я планирую и принимаю задачи, ты берёшь их из очереди и доводишь до «Проверяю».
735
+
736
+ Мои файлы и серверы без вопроса не трогай.`,
737
+ en: `Run the kanban skill.
738
+
739
+ You are helping me set up a freshly installed local-kanban board. Run the setup yourself, asking me questions ONE AT A TIME and waiting for the answer — do not assume anything on my behalf.
740
+
741
+ 1. Check that the kb command works (kb p). If it does not, tell me what to run and wait for me.
742
+ 2. Ask whether to mirror tasks to GitHub. If yes, check gh auth status and the project scope, ask for the owner and repository, enable sync and confirm the queue is moving (kb sync).
743
+ 3. Ask which projects to register. For each one, ask: name, where the code lives, whether there is a server and a deploy. Register them and show me kb info for each.
744
+ 4. At the end, show what we got and briefly explain the loop: I plan and accept tasks, you take them from the queue and bring them to Review.
745
+
746
+ Do not touch my files or servers without asking.`,
747
+ };
748
+ export const setupPrompt = () => SETUP_PROMPT[LANG] || SETUP_PROMPT.ru;
749
+
750
+ export const seg = (s) => encodeURIComponent(String(s));
751
+ export let ghSyncOn = true;
752
+ export function setGhSyncOn(v) { ghSyncOn = v; }
753
+ const reportedErrors = new Map();
754
+ const REPORT_WINDOW_MS = 10000;
755
+ function reportError(scope, message, detail) {
756
+ const msg = String(message || '').slice(0, 500);
757
+ if (!msg) return;
758
+ const now = Date.now();
759
+ if (now - (reportedErrors.get(msg) || 0) < REPORT_WINDOW_MS) return;
760
+ for (const [k, at] of reportedErrors) { if (now - at >= REPORT_WINDOW_MS) reportedErrors.delete(k); }
761
+ reportedErrors.set(msg, now);
762
+ fetch('/api/errors', {
763
+ method: 'POST',
764
+ headers: { 'Content-Type': 'application/json' },
765
+ body: JSON.stringify({ scope, message: msg, detail: detail ? String(detail).slice(0, 2000) : null }),
766
+ }).catch(() => { });
767
+ }
768
+ window.addEventListener('error', (e) => reportError(e.filename ? `${e.filename}:${e.lineno}` : 'window.onerror', e.message, e.error?.stack));
769
+ window.addEventListener('unhandledrejection', (e) => reportError('unhandledrejection', e.reason?.message || String(e.reason), e.reason?.stack));
770
+
771
+ export const apiBlob = async (path, blob, opts = {}) => {
772
+ const res = await fetch(path, { method: 'POST', headers: { 'Content-Type': blob.type }, body: blob })
773
+ .catch((e) => { if (!opts.quiet) reportError(`POST ${path}`, `network: ${e.message}`); throw e; });
774
+ const data = await res.json().catch(() => ({}));
775
+ if (!res.ok) {
776
+ if (!opts.quiet) reportError(`POST ${path}`, `HTTP ${res.status}: ${data.error || ''}`.trim());
777
+ throw new Error(data.error || `HTTP ${res.status}`);
778
+ }
779
+ return data;
780
+ };
781
+
782
+ export function apiError(message, extra) {
783
+ return Object.assign(new Error(String(message)), extra);
784
+ }
785
+
786
+ const offlineRetries = [];
787
+ export function showOfflineBadge(on) {
788
+ const el = $('offline-badge');
789
+ if (el) el.classList.toggle('hidden', !on);
790
+ }
791
+ export function noteUnsaved(retry) {
792
+ if (retry && !offlineRetries.includes(retry)) offlineRetries.push(retry);
793
+ showOfflineBadge(true);
794
+ const el = $('offline-badge');
795
+ if (el && !el.onclick) el.onclick = () => retryUnsaved();
796
+ }
797
+ export async function retryUnsaved() {
798
+ const queue = offlineRetries.splice(0);
799
+ for (const fn of queue) {
800
+ try { await fn(); } catch { if (!offlineRetries.includes(fn)) offlineRetries.push(fn); }
801
+ }
802
+ showOfflineBadge(offlineRetries.length > 0);
803
+ }
804
+
805
+ export const api = async (method, path, body, opts = {}) => {
806
+ const res = await fetch(path, {
807
+ method,
808
+ headers: body ? { 'Content-Type': 'application/json' } : undefined,
809
+ body: body ? JSON.stringify(body) : undefined,
810
+ }).catch((e) => {
811
+ if (!opts.quiet) reportError(`${method} ${path}`, `network: ${e.message}`);
812
+ throw apiError(e.message, { offline: true });
813
+ });
814
+ if (res.status === 204) return null;
815
+ const data = await res.json().catch(() => ({}));
816
+ if (!res.ok) {
817
+ if (!opts.quiet && path !== '/api/errors') reportError(`${method} ${path}`, `HTTP ${res.status}: ${data.error || ''}`.trim());
818
+ throw apiError(data.error || res.status, { status: res.status });
819
+ }
820
+ return data;
821
+ };