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,114 @@
1
+
2
+ import { fmtWork } from './board.js';
3
+ import { $, ALL, LANG, api, esc, ic, state, tr } from './core.js';
4
+ import { openDrawer } from './drawer.js';
5
+ import { HEAT_LEGEND, RANGE_BTN, RANGE_LABEL, calendarHTML, dashPanel, doneBarsHTML, feedHTML, openStatusPicker, plural, statCard, streaks, timeHeatHTML, topProjHTML, waitingHTML } from './settings.js';
6
+ import { selectProject } from './sidebar.js';
7
+ import { refresh } from './sse.js';
8
+
9
+ export async function renderDashboard() {
10
+ const dash = $('dashboard');
11
+ if (!state.projects.length) {
12
+ dash.innerHTML = `<div class="dash-wrap"><div class="panel demo-cta">
13
+ <div class="demo-cta-title">${tr('The board is empty')}</div>
14
+ <div class="muted">${tr('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.')}</div>
15
+ <div><button class="btn-primary" id="demo-create">${tr('Create demo project')}</button></div>
16
+ </div></div>`;
17
+ $('demo-create').onclick = async () => {
18
+ $('demo-create').disabled = true;
19
+ try { await api('POST', '/api/projects/demo', { lang: LANG }, { quiet: true }); } catch { }
20
+ await refresh();
21
+ selectProject('demo');
22
+ };
23
+ return;
24
+ }
25
+ let d;
26
+ try { d = await api('GET', `/api/dashboard?range=${state.dashRange}`); }
27
+ catch { dash.innerHTML = `<div class="dash-wrap"><div class="muted">${tr('Could not load the dashboard.')}</div></div>`; return; }
28
+
29
+ const s = d.summary;
30
+ const rlab = RANGE_LABEL[d.range] || tr('this week');
31
+ const phRange = `<span class="ph-sub">${rlab}</span>`;
32
+ const den = s.done_period + s.active;
33
+ const pct = den ? Math.round((s.done_period / den) * 100) : 0;
34
+ const [cur, mx] = streaks(d.done_per_day);
35
+ const cyc = s.cycle_hours_period != null ? `${s.cycle_hours_period.toFixed(1)} ${tr('h')}` : '—';
36
+ const q = d.queue;
37
+ const low = q.todo_ready <= 2;
38
+
39
+ const cards = '<div class="dcards">'
40
+ + statCard(tr('Done'), s.done_period, rlab)
41
+ + statCard(tr('Active'), s.active, tr('queued and in progress'))
42
+ + statCard(tr('Completed'), `${pct}%`, `${s.done_period} ${tr('of')} ${den} ${rlab}`)
43
+ + statCard(tr('Work time'), fmtWork(s.work_seconds_period), rlab)
44
+ + statCard(tr('Streak'), `${cur} ${tr('d')}`, `${tr('max')} ${mx} ${tr('d')}`)
45
+ + statCard(tr('Task cycle'), cyc, `${tr('from created to done')} (${rlab})`)
46
+ + '</div>';
47
+
48
+ const rangeCtl = `<div class="dash-top"><div class="dash-range">${RANGE_BTN
49
+ .map(([v, lab]) => `<button class="dr-btn${v === d.range ? ' active' : ''}" data-range="${v}">${tr(lab)}</button>`)
50
+ .join('')}</div></div>`;
51
+
52
+ const queue = `<div class="queue-strip${low ? ' low' : ''}">`
53
+ + '<div class="qs-main">'
54
+ + `<span class="qs-item" data-go="1" title="${esc(tr('open the all-projects board'))}"><b>${q.todo_ready}</b> ${plural(q.todo_ready, 'task', 'tasks')} ${tr('ready to work on')}</span>`
55
+ + '<span class="muted">·</span>'
56
+ + `<span class="qs-item" data-go="1" title="${esc(tr('open the all-projects board'))}">${q.backlog} ${tr('in the backlog')}</span>`
57
+ + `${low ? `<span class="queue-warn">${ic('warn', 12)} ${tr('running out of tasks soon — time to plan')}</span>` : ''}`
58
+ + '</div>'
59
+ + `<div class="qs-note">${tr('“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.')}</div>`
60
+ + '</div>';
61
+
62
+ const fun = d.fun.peak_hour != null
63
+ ? `${ic('trend', 13)} ${tr('Tasks closed:')} ${d.fun.done_total}. ${tr('Busiest around')} ${d.fun.peak_hour}:00.`
64
+ : `${ic('trend', 13)} ${tr('Tasks closed:')} ${d.fun.done_total}.`;
65
+
66
+ dash.innerHTML = `<div class="dash-wrap">
67
+ ${rangeCtl}
68
+ ${dashPanel(tr('Waiting for you'), waitingHTML(d.waiting))}
69
+ ${cards}
70
+ ${queue}
71
+ <div class="dash-2col">
72
+ ${dashPanel(tr('Activity by day'), `${calendarHTML(d.heat_days, d.range_since)}<div class="heat-note">${rlab}</div>`, HEAT_LEGEND)}
73
+ ${dashPanel(tr('When work happens'), timeHeatHTML(d.heat_time), phRange)}
74
+ </div>
75
+ <div class="dash-2col">
76
+ ${dashPanel(tr('Top projects'), topProjHTML(d.top_projects), phRange)}
77
+ ${dashPanel(tr('Done by day'), doneBarsHTML(d.done_per_day, d.range_since), phRange)}
78
+ </div>
79
+ ${dashPanel(tr('Recent activity'), feedHTML(d.recent))}
80
+ <div class="fun-line">${fun}</div>
81
+ </div>`;
82
+
83
+ dash.querySelectorAll('[data-key]').forEach((el) => { el.onclick = () => openDrawer(el.dataset.key); });
84
+ dash.querySelectorAll('.dr-btn').forEach((b) => { b.onclick = () => { state.dashRange = b.dataset.range; renderDashboard(); }; });
85
+ dash.querySelectorAll('.wr-dot').forEach((el) => {
86
+ el.onclick = (e) => { e.stopPropagation(); openStatusPicker(el, el.dataset.pick, el.dataset.cur); };
87
+ });
88
+ dash.querySelectorAll('.qs-item').forEach((el) => { el.onclick = () => selectProject(ALL); });
89
+ }
90
+
91
+ const dashTip = document.createElement('div');
92
+ dashTip.className = 'dash-tip hidden';
93
+ document.body.appendChild(dashTip);
94
+ function moveDashTip(x, y) {
95
+ const r = dashTip.getBoundingClientRect();
96
+ let left = x + 13; let top = y + 15;
97
+ if (left + r.width > window.innerWidth - 8) left = x - r.width - 13;
98
+ if (top + r.height > window.innerHeight - 8) top = y - r.height - 13;
99
+ dashTip.style.left = `${left}px`;
100
+ dashTip.style.top = `${top}px`;
101
+ }
102
+ let tipFor = null;
103
+ $('dashboard').addEventListener('mousemove', (e) => {
104
+ const el = e.target.closest('[data-tip]');
105
+ if (!el) { if (tipFor) { tipFor = null; dashTip.classList.add('hidden'); } return; }
106
+ if (el !== tipFor) {
107
+ tipFor = el;
108
+ dashTip.innerHTML = `<span class="dt-n">${esc(el.dataset.tip)}</span>`
109
+ + (el.dataset.tipSub ? `<span class="dt-d">${esc(el.dataset.tipSub)}</span>` : '');
110
+ dashTip.classList.remove('hidden');
111
+ }
112
+ moveDashTip(e.clientX, e.clientY);
113
+ });
114
+ $('dashboard').addEventListener('mouseleave', () => { tipFor = null; dashTip.classList.add('hidden'); });