@mgeri1993/claude-task-manager 1.0.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.
package/js/Utils.js ADDED
@@ -0,0 +1,202 @@
1
+ import { I18n } from "./i18n.js";
2
+
3
+ // Column definitions and their CSS custom-property colors.
4
+ // The JS uses these inline as style="background:var(--todo)" on cards/pills (see
5
+ // BoardView.js, TaskModal.js) — the --todo/--in_progress/... tokens are defined in
6
+ // style.css's :root.
7
+ export const COLUMNS = [
8
+ { key: "todo", label: "To do", color: "var(--todo)" },
9
+ { key: "in_progress", label: "In progress", color: "var(--in_progress)" },
10
+ { key: "blocked", label: "Blocked", color: "var(--blocked)" },
11
+ { key: "review", label: "Review", color: "var(--review)" },
12
+ { key: "done", label: "Done", color: "var(--done)" },
13
+ ];
14
+ export const COLOR = Object.fromEntries(COLUMNS.map(c => [c.key, c.color]));
15
+
16
+ // Inline SVG icons (from the "Redesign a dark mode" mockup). Inherit currentColor, so they
17
+ // take on the parent's text color. The static header icons live in index.html; these are
18
+ // for JS-generated markup (stats bar, cards, feed, lanes).
19
+ export const ICONS = {
20
+ clock: '<svg class="ico" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"></circle><path d="M12 7.5V12l3 2"></path></svg>',
21
+ activity: '<svg class="ico" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12h4l2.5-6 4 12 2.5-6H21"></path></svg>',
22
+ user: '<svg class="ico" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="8" r="3.4"></circle><path d="M5.5 20a6.5 6.5 0 0 1 13 0"></path></svg>',
23
+ arrow: '<svg class="ico" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h13M13 6l6 6-6 6"></path></svg>',
24
+ block: '<svg class="ico" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"></circle><path d="M6 6l12 12"></path></svg>',
25
+ hourglass: '<svg class="ico" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 4h10M7 20h10M8 4v3l4 5 4-5V4M8 20v-3l4-5 4 5v3"></path></svg>',
26
+ pause: '<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="5" width="4" height="14" rx="1"></rect><rect x="14" y="5" width="4" height="14" rx="1"></rect></svg>',
27
+ play: '<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"></path></svg>',
28
+ bell: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9"><path d="M18 8a6 6 0 1 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.7 21a2 2 0 0 1-3.4 0"></path></svg>',
29
+ check: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.5l4.4 4.4L19 7.2"></path></svg>',
30
+ chevron: '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"></path></svg>',
31
+ };
32
+
33
+ // Which status pill needs dark (not white) text for contrast.
34
+ const DARK_PILL_STATUSES = new Set(["review", "done"]);
35
+
36
+ // Agent-avatar palette (from the "Redesign a dark mode" mockup's agent colors) — stable,
37
+ // hash-based assignment, so every agent id always gets the same color. Reused for module
38
+ // badges too (any string hashes to a stable color, not just agent ids).
39
+ const AGENT_PALETTE = ["#5b8def", "#46c07f", "#d99a3f", "#b57cf6", "#45b5c4", "#e2739b", "#e0894e", "#2dd4bf", "#8b8ff0"];
40
+
41
+ /** General helper functions (as static class methods). */
42
+ export class Utils {
43
+ static esc(s) {
44
+ return String(s == null ? "" : s).replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
45
+ }
46
+
47
+ static norm(notes) {
48
+ return !notes ? [] : (Array.isArray(notes) ? notes.map(n => typeof n === "string" ? n : (n && (n.text || n.note)) || "").filter(Boolean) : [String(notes)]);
49
+ }
50
+
51
+ /** Like norm(), but keeps the timestamp: [{ at, text }, ...] (empty text dropped). */
52
+ static normDetailed(notes) {
53
+ if (!Array.isArray(notes)) return Utils.norm(notes).map(text => ({ at: null, text }));
54
+ return notes
55
+ .map(n => typeof n === "string" ? { at: null, text: n } : { at: (n && n.at) || null, text: (n && (n.text || n.note)) || "" })
56
+ .filter(x => x.text);
57
+ }
58
+
59
+ /**
60
+ * Note "kind" from the prefix convention (RESEARCH/PLAN/DECISION/IMPLEMENTATION…): reads
61
+ * the ALL-CAPS word(s) at the start of the text, before a `:` or `.` (optionally followed
62
+ * by a `(...)` aside). Returns { label, cls } or null if there's no such prefix.
63
+ */
64
+ static noteKind(text) {
65
+ const m = /^\s*([A-ZÁÉÍÓÖŐÚÜŰ][A-ZÁÉÍÓÖŐÚÜŰ ]{1,24}?)(?:\s*\([^)]*\))?\s*[:.]/.exec(text || "");
66
+ if (!m) return null;
67
+ const label = m[1].trim();
68
+ const w = label.split(/\s+/)[0].toLowerCase();
69
+ let cls = "k-other";
70
+ if (w.startsWith("kutat") || w.startsWith("research")) cls = "k-research";
71
+ else if (w.startsWith("terv") || w.startsWith("plan")) cls = "k-plan";
72
+ else if (w.startsWith("dönt") || w.startsWith("dont") || w.startsWith("decision")) cls = "k-decision";
73
+ else if (w.startsWith("impl") || w.startsWith("javít") || w.startsWith("javit") || w.startsWith("fix")) cls = "k-impl";
74
+ return { label, cls };
75
+ }
76
+
77
+ static agentKey(t) {
78
+ return t.assignedAgentId || "—";
79
+ }
80
+
81
+ /** Stable (hash-based) avatar color for an agent id (or any string, e.g. a module name);
82
+ * "—" (no owner) is always gray. */
83
+ static agentColor(id) {
84
+ if (!id || id === "—") return "#7f8794";
85
+ let h = 0; for (let i = 0; i < id.length; i++) h = (h * 31 + id.charCodeAt(i)) >>> 0;
86
+ return AGENT_PALETTE[h % AGENT_PALETTE.length];
87
+ }
88
+
89
+ /** 2-character abbreviation for the agent avatar (e.g. "dev-organizer-fe" → "of"). */
90
+ static agentShort(id) {
91
+ if (!id || id === "—") return "—";
92
+ const parts = id.split(/[-_ ]+/).filter(Boolean);
93
+ if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toLowerCase();
94
+ return id.slice(0, 2).toLowerCase();
95
+ }
96
+
97
+ /** #rrggbb → rgba(...) with the given alpha (the mockup's hexA helper). */
98
+ static hexA(hex, a) {
99
+ const n = parseInt(String(hex).replace("#", ""), 16);
100
+ return `rgba(${(n >> 16) & 255},${(n >> 8) & 255},${n & 255},${a})`;
101
+ }
102
+
103
+ static absTime(iso) {
104
+ const d = new Date(iso);
105
+ return isNaN(d) ? (iso || "–") : d.toLocaleString(I18n.locale(), { year: "2-digit", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" });
106
+ }
107
+
108
+ static relTime(iso) {
109
+ const d = new Date(iso); if (isNaN(d)) return "";
110
+ let s = Math.round((Date.now() - d.getTime()) / 1000); const fut = s < 0; s = Math.abs(s);
111
+ if (s < 60) return I18n.t(fut ? "time.soon" : "time.now");
112
+ for (const [sec, key] of [[31557600, "time.years"], [2629800, "time.months"], [604800, "time.weeks"], [86400, "time.days"], [3600, "time.hours"], [60, "time.minutes"]])
113
+ if (s >= sec) return (fut ? "~" : "") + Math.floor(s / sec) + " " + I18n.t(key);
114
+ return I18n.t("time.now");
115
+ }
116
+
117
+ static dur(ms) {
118
+ if (ms == null) return I18n.t("dur.none");
119
+ const s = Math.round(ms / 1000), d = Math.floor(s / 86400), h = Math.floor(s % 86400 / 3600), m = Math.floor(s % 3600 / 60);
120
+ if (d) return I18n.t("dur.days", { d, h });
121
+ if (h) return I18n.t("dur.hours", { h, m });
122
+ if (m) return I18n.t("dur.minutes", { m });
123
+ return I18n.t("dur.seconds", { s });
124
+ }
125
+
126
+ static parseTeam(t) {
127
+ const m = /Team Task #(\d+)/i.exec(t.title || "");
128
+ return m ? +m[1] : null;
129
+ }
130
+
131
+ // Dependency parsing from notes: "Blocked ... #N ... until closed", "resolved"/"feloldva".
132
+ static parseDeps(t) {
133
+ const notes = Utils.norm(t.notes); const blockedBy = new Set(); let resolved = false;
134
+ for (const n of notes) {
135
+ if (/feloldva|resolved/i.test(n)) resolved = true;
136
+ const i = n.search(/blokkolva|blocked/i);
137
+ if (i >= 0) { const seg = n.slice(i); let m; const re = /#(\d+)/g; while ((m = re.exec(seg))) blockedBy.add(+m[1]); }
138
+ }
139
+ const active = blockedBy.size > 0 && !resolved && (t.status === "todo" || t.status === "blocked");
140
+ return { blockedBy: [...blockedBy], resolved, active };
141
+ }
142
+
143
+ // Lead time from the history.
144
+ static cycle(t) {
145
+ const h = (t.history || []).slice().sort((a, b) => new Date(a.at) - new Date(b.at));
146
+ let inProg = 0, start = null, done = null;
147
+ const created = t.createdAt ? new Date(t.createdAt) : (h[0] ? new Date(h[0].at) : null);
148
+ for (const e of h) {
149
+ const at = new Date(e.at);
150
+ if (e.toStatus === "in_progress") start = at;
151
+ else if (e.fromStatus === "in_progress" && start) { inProg += at - start; start = null; }
152
+ if (e.toStatus === "done") done = at;
153
+ }
154
+ if (start && !done) inProg += Date.now() - start; // still in progress
155
+ return { inProgressMs: inProg, leadMs: (created && done) ? (done - created) : null, done: !!done };
156
+ }
157
+
158
+ /** CSS class suffix for .pill elements, for WCAG contrast (empty or " pill-dark"). */
159
+ static pillClass(status) {
160
+ return DARK_PILL_STATUSES.has(status) ? " pill-dark" : "";
161
+ }
162
+
163
+ /** Elapsed time in ms since an ISO timestamp (null if invalid). */
164
+ static ageMs(iso) {
165
+ const d = new Date(iso);
166
+ return isNaN(d) ? null : (Date.now() - d.getTime());
167
+ }
168
+
169
+ /** When the task entered its CURRENT status (from the last matching history entry, else createdAt). */
170
+ static statusSince(t) {
171
+ const h = Array.isArray(t.history) ? t.history.slice().sort((a, b) => new Date(a.at) - new Date(b.at)) : [];
172
+ let since = t.createdAt || (h[0] && h[0].at) || null;
173
+ for (const e of h) if (e.toStatus === t.status) since = e.at;
174
+ return since;
175
+ }
176
+
177
+ /** Wait-level (for the review "awaiting you" age badge escalation): fresh <1h, warn 1–4h, stale >4h. */
178
+ static waitLevel(ms) {
179
+ if (ms == null) return "fresh";
180
+ const h = ms / 3600000;
181
+ return h < 1 ? "fresh" : (h < 4 ? "warn" : "stale");
182
+ }
183
+
184
+ /**
185
+ * Merged activity feed from every task's notes + history, in reverse chronological order.
186
+ * Item: { at, taskId, taskTitle, kind, text, from, to, type }.
187
+ */
188
+ static activityFeed(tasks) {
189
+ const items = [];
190
+ for (const t of tasks) {
191
+ for (const n of Utils.normDetailed(t.notes)) {
192
+ if (!n.at) continue;
193
+ items.push({ at: n.at, taskId: t.id, taskTitle: t.title || t.id, kind: Utils.noteKind(n.text), text: n.text, kindType: "note" });
194
+ }
195
+ for (const h of (Array.isArray(t.history) ? t.history : [])) {
196
+ if (!h.at) continue;
197
+ items.push({ at: h.at, taskId: t.id, taskTitle: t.title || t.id, from: h.fromStatus, to: h.toStatus, text: h.note || "", type: h.type, kindType: "history" });
198
+ }
199
+ }
200
+ return items.sort((a, b) => new Date(b.at) - new Date(a.at));
201
+ }
202
+ }
package/js/i18n.js ADDED
@@ -0,0 +1,338 @@
1
+ /**
2
+ * i18n.js — minimal translation layer for the board. Default language: English. The user
3
+ * can switch to Hungarian at runtime (header toggle); the choice persists in localStorage
4
+ * (tm.lang). Static markup uses data-i18n / data-i18n-placeholder / data-i18n-title
5
+ * attributes (applied by App.applyStaticI18n()); dynamically-rendered JS strings call
6
+ * I18n.t(key) directly.
7
+ */
8
+ const STRINGS = {
9
+ en: {
10
+ "app.title": "Task Manager",
11
+ "app.title.review": "({n} review)",
12
+ "app.starting": "starting…",
13
+ "app.noProject": "no project",
14
+ "app.noProjectShort": "no project registered",
15
+ "app.noProjectRegistered": 'No project registered. From any of your projects, run <code>ctm init</code> on the host, then reload the page.',
16
+ "app.fetchFailed": "Could not read the file. Check that the server is running: <code>docker compose up</code>, then <code>http://localhost:3333/</code>.",
17
+ "app.noChange": "no change · {t}",
18
+ "app.live": "live · {n} task{s} · {t}",
19
+ "app.liveChanged": " · {n} changed",
20
+ "app.paused": "paused",
21
+ "app.background": "in background – paused",
22
+ "app.sending": "sending…",
23
+ "app.done": "done · {t}",
24
+ "app.writeError": "write error",
25
+ "app.writeFailed": "Write failed: <code>{msg}</code>",
26
+ "app.notEnabled": "Writing requires the PHP server (docker compose / php -S). Not available from file://.",
27
+ "app.setActorFirst": 'Set a name in the header ("As …") so the board knows who to write as.',
28
+ "app.noProjectSelected": "No project selected — pick one in the Source field.",
29
+ "app.needFeedback": "Enter feedback in the field to request changes.",
30
+ "app.needNoteText": "Enter some text for the note.",
31
+ "app.approved": "approved → done",
32
+ "app.toDone": "→ done",
33
+ "app.toInProgress": "→ in_progress",
34
+ "app.toReview": "→ review",
35
+ "app.reopened": "reopened",
36
+ "app.changesRequested": "changes requested (sent to the agent's inbox)",
37
+ "app.toBlocked": "→ blocked",
38
+ "app.noteAdded": "note added",
39
+ "app.priority": "priority: {v}",
40
+ "app.module": "module: {v}",
41
+ "app.assigned": "assigned: {v}",
42
+ "app.wrapperLoadFailed": "Could not load the wrapper: <code>{msg}</code>",
43
+ "app.copied": "✓ Copied",
44
+ "app.copyFailed": "✗ Failed",
45
+ "hd.pause": "Pause",
46
+ "hd.resume": "Resume",
47
+ "hd.source": "Source",
48
+ "hd.project": "Project",
49
+ "hd.searchPlaceholder": "search…",
50
+ "hd.poll": "Poll",
51
+ "hd.as": "As",
52
+ "hd.asTitle": "Who the board writes as (task.sh --as). Pick an agent/reviewer, not a fixed \"human\".",
53
+ "hd.asPlaceholder": "e.g. reviewer",
54
+ "hd.notify": "Notify",
55
+ "hd.notifyTitle": "Browser notification on status change (→ review / done)",
56
+ "hd.projects": "Projects",
57
+ "hd.projectsTitle": "Registered projects + copy wrapper task.sh",
58
+ "project.modalTitle": "📁 Projects",
59
+ "hd.context": "Context",
60
+ "hd.lang": "Language",
61
+ "ctrl.sort": "Sort",
62
+ "ctrl.sort.activity": "last activity",
63
+ "ctrl.sort.created": "created",
64
+ "ctrl.sort.title": "title (A→Z)",
65
+ "ctrl.sort.team": "Team #",
66
+ "ctrl.view": "View",
67
+ "ctrl.view.board": "Kanban",
68
+ "ctrl.view.swim": "Swimlane",
69
+ "ctrl.view.feed": "Feed",
70
+ "ctrl.view.compact": "Compact",
71
+ "ctrl.quickFilter": "Quick filter",
72
+ "ctrl.quick.review": "Awaiting you",
73
+ "ctrl.quick.active": "Active",
74
+ "ctrl.quick.blocked": "Blocked",
75
+ "ctrl.module": "Module",
76
+ "ctrl.module.all": "— all modules —",
77
+ "ctrl.readonly": "read-only view – the file is written by the agent",
78
+ "agents.title": "Agent load & filter",
79
+ "agents.active": "{n} active",
80
+ "agents.all": "all",
81
+ "project.wrapperCopy": "Copy wrapper",
82
+ "project.none": 'No project registered. From any of your projects, run <code>ctm init</code> on the host, then reload the page.',
83
+ "modal.close": "✕ Close",
84
+ "modal.approve": "✓ Approved",
85
+ "modal.changes": "↺ Changes needed",
86
+ "modal.block": "⛔ Block",
87
+ "modal.reopen": "↩ Reopen",
88
+ "modal.start": "▶ Starting",
89
+ "modal.toReview": "🔍 To review",
90
+ "modal.done": "✓ Done",
91
+ "modal.notePlaceholder": "Feedback / note — used by Changes needed, Block, and + Note (goes into the affected agent's inbox)",
92
+ "modal.addNote": "+ Note",
93
+ "modal.priority": "Priority",
94
+ "modal.module": "Module",
95
+ "modal.modulePlaceholder": "module",
96
+ "modal.agent": "Agent",
97
+ "modal.agentPlaceholder": "assign",
98
+ "modal.metric.lead": "lead time",
99
+ "modal.metric.inProgress": "in_progress",
100
+ "modal.metric.status": "status",
101
+ "modal.dependencies": "Dependencies",
102
+ "modal.blockedBy": "⛔ Blocked by:",
103
+ "modal.wasBlockedBy": "✅ Was blocked by:",
104
+ "modal.blocks": "🔒 Blocks:",
105
+ "modal.data": "Data",
106
+ "modal.copyId": "⧉ Copy",
107
+ "modal.copyIdTitle": "Copy id to clipboard",
108
+ "modal.notes": "Notes ({n})",
109
+ "modal.history": "History ({n})",
110
+ "modal.kv.id": "ID",
111
+ "modal.kv.team": "Team",
112
+ "modal.kv.agent": "Agent",
113
+ "modal.kv.module": "Module",
114
+ "modal.kv.channel": "Channel",
115
+ "modal.kv.source": "Source",
116
+ "modal.kv.thread": "Thread",
117
+ "modal.kv.created": "Created",
118
+ "modal.kv.updated": "Updated",
119
+ "modal.kv.lastActivity": "Last activity",
120
+ "board.unknown": "Unknown",
121
+ "board.empty": "— empty —",
122
+ "board.noTasks": "— no tasks to show —",
123
+ "board.noActivity": "— no activity —",
124
+ "board.noOwner": "no owner",
125
+ "board.tasks": "{n} tasks",
126
+ "board.leadAvg": "lead time avg",
127
+ "board.inProgressAvg": "in_progress avg",
128
+ "board.percentDone": "{p}% done",
129
+ "board.waitingSince": "waiting since review — for you",
130
+ "badge.new": "new",
131
+ "badge.notes": "+note",
132
+ "badge.history": "+history",
133
+ "badge.updated": "updated",
134
+ "ctx.title": "📌 Context",
135
+ "ctx.updated": "· updated {t}",
136
+ "ctx.none": "No <code>context.json</code> next to the source. The agent creates it during the session (init prompt + decisions) so work can continue in a fresh session.",
137
+ "ctx.goal": "Goal",
138
+ "ctx.currentFocus": "Current focus",
139
+ "ctx.initPrompt": "Init prompt",
140
+ "ctx.constraints": "Constraints",
141
+ "ctx.noConstraints": "No constraints recorded.",
142
+ "ctx.decisions": "Decisions ({n})",
143
+ "ctx.noDecisions": "No decisions recorded yet.",
144
+ "ctx.openQuestions": "Open questions",
145
+ "ctx.noOpenQuestions": "No open questions.",
146
+ "ctx.notes": "Notes",
147
+ "time.now": "now",
148
+ "time.soon": "soon",
149
+ "time.years": "y ago",
150
+ "time.months": "mo ago",
151
+ "time.weeks": "w ago",
152
+ "time.days": "d ago",
153
+ "time.hours": "h ago",
154
+ "time.minutes": "m ago",
155
+ "dur.days": "{d}d {h}h",
156
+ "dur.hours": "{h}h {m}m",
157
+ "dur.minutes": "{m}m",
158
+ "dur.seconds": "{s}s",
159
+ "dur.none": "–",
160
+ },
161
+ hu: {
162
+ "app.title": "Task Manager",
163
+ "app.title.review": "({n} review)",
164
+ "app.starting": "indul…",
165
+ "app.noProject": "nincs projekt",
166
+ "app.noProjectShort": "nincs regisztrált projekt",
167
+ "app.noProjectRegistered": 'Nincs regisztrált projekt. Bármelyik projektedből futtasd a host gépen: <code>ctm init</code>, majd frissítsd az oldalt.',
168
+ "app.fetchFailed": "Nem sikerült beolvasni a fájlt. Ellenőrizd, hogy fut-e a szerver: <code>docker compose up</code>, majd <code>http://localhost:3333/</code>.",
169
+ "app.noChange": "nincs változás · {t}",
170
+ "app.live": "él · {n} taszk{s} · {t}",
171
+ "app.liveChanged": " · {n} változott",
172
+ "app.paused": "szüneteltetve",
173
+ "app.background": "háttérben – szünet",
174
+ "app.sending": "küldés…",
175
+ "app.done": "kész · {t}",
176
+ "app.writeError": "írás hiba",
177
+ "app.writeFailed": "Írás sikertelen: <code>{msg}</code>",
178
+ "app.notEnabled": "Az írás a PHP szervert igényli (docker compose / php -S). file://-ról nem elérhető.",
179
+ "app.setActorFirst": 'Állíts be egy nevet a fejlécben („Mint …"), hogy kinek a nevében írjon a board.',
180
+ "app.noProjectSelected": "Nincs kiválasztva projekt — válassz egyet a Forrás mezőben.",
181
+ "app.needFeedback": "A változtatás kéréséhez írj visszajelzést a mezőbe.",
182
+ "app.needNoteText": "Írj szöveget a jegyzethez.",
183
+ "app.approved": "jóváhagyva → done",
184
+ "app.toDone": "→ done",
185
+ "app.toInProgress": "→ in_progress",
186
+ "app.toReview": "→ review",
187
+ "app.reopened": "újranyitva",
188
+ "app.changesRequested": "változtatás kérve (az agent inboxába küldve)",
189
+ "app.toBlocked": "→ blocked",
190
+ "app.noteAdded": "jegyzet hozzáfűzve",
191
+ "app.priority": "prioritás: {v}",
192
+ "app.module": "modul: {v}",
193
+ "app.assigned": "hozzárendelve: {v}",
194
+ "app.wrapperLoadFailed": "Wrapper betöltése sikertelen: <code>{msg}</code>",
195
+ "app.copied": "✓ Másolva",
196
+ "app.copyFailed": "✗ Hiba",
197
+ "hd.pause": "Szünet",
198
+ "hd.resume": "Folytatás",
199
+ "hd.source": "Forrás",
200
+ "hd.project": "Projekt",
201
+ "hd.searchPlaceholder": "keresés…",
202
+ "hd.poll": "Poll",
203
+ "hd.as": "Mint",
204
+ "hd.asTitle": "Ki nevében írjon a board (task.sh --as). Kiválasztható agent/reviewer, nem fix human.",
205
+ "hd.asPlaceholder": "pl. reviewer",
206
+ "hd.notify": "Értesítés",
207
+ "hd.notifyTitle": "Böngésző-értesítés státuszváltáskor (→ review / done)",
208
+ "hd.projects": "Projektek",
209
+ "hd.projectsTitle": "Regisztrált projektek + wrapper task.sh másolása",
210
+ "project.modalTitle": "📁 Projektek",
211
+ "hd.context": "Kontextus",
212
+ "hd.lang": "Nyelv",
213
+ "ctrl.sort": "Rendezés",
214
+ "ctrl.sort.activity": "utolsó aktivitás",
215
+ "ctrl.sort.created": "létrehozás",
216
+ "ctrl.sort.title": "cím (A→Z)",
217
+ "ctrl.sort.team": "Team #",
218
+ "ctrl.view": "Nézet",
219
+ "ctrl.view.board": "Kanban",
220
+ "ctrl.view.swim": "Swimlane",
221
+ "ctrl.view.feed": "Folyam",
222
+ "ctrl.view.compact": "Kompakt",
223
+ "ctrl.quickFilter": "Gyorsszűrő",
224
+ "ctrl.quick.review": "Rád vár",
225
+ "ctrl.quick.active": "Aktív",
226
+ "ctrl.quick.blocked": "Blocked",
227
+ "ctrl.module": "Modul",
228
+ "ctrl.module.all": "— összes modul —",
229
+ "ctrl.readonly": "read-only nézet – a fájlt az agent írja",
230
+ "agents.title": "Ágens-terhelés & szűrő",
231
+ "agents.active": "{n} aktív",
232
+ "agents.all": "összes",
233
+ "project.wrapperCopy": "Wrapper másolása",
234
+ "project.none": 'Nincs regisztrált projekt. Bármelyik projektedből futtasd a host gépen: <code>ctm init</code>, majd frissítsd az oldalt.',
235
+ "modal.close": "✕ Bezár",
236
+ "modal.approve": "✓ Jóváhagyva",
237
+ "modal.changes": "↺ Változtatás kell",
238
+ "modal.block": "⛔ Blokk",
239
+ "modal.reopen": "↩ Újranyit",
240
+ "modal.start": "▶ Indítom",
241
+ "modal.toReview": "🔍 Review-ra",
242
+ "modal.done": "✓ Kész",
243
+ "modal.notePlaceholder": "Visszajelzés / jegyzet — a Változtatás kell, a Blokk és a + Jegyzet ezt használja (az érintett agent inboxába kerül)",
244
+ "modal.addNote": "+ Jegyzet",
245
+ "modal.priority": "Prio",
246
+ "modal.module": "Modul",
247
+ "modal.modulePlaceholder": "modul",
248
+ "modal.agent": "Agent",
249
+ "modal.agentPlaceholder": "hozzárendelés",
250
+ "modal.metric.lead": "átfutás",
251
+ "modal.metric.inProgress": "in_progress",
252
+ "modal.metric.status": "státusz",
253
+ "modal.dependencies": "Függőségek",
254
+ "modal.blockedBy": "⛔ Blokkolják:",
255
+ "modal.wasBlockedBy": "✅ Volt blokkolva:",
256
+ "modal.blocks": "🔒 Ezt blokkolja:",
257
+ "modal.data": "Adatok",
258
+ "modal.copyId": "⧉ Copy",
259
+ "modal.copyIdTitle": "ID másolása a vágólapra",
260
+ "modal.notes": "Jegyzetek ({n})",
261
+ "modal.history": "Előzmények ({n})",
262
+ "modal.kv.id": "ID",
263
+ "modal.kv.team": "Team",
264
+ "modal.kv.agent": "Ágens",
265
+ "modal.kv.module": "Modul",
266
+ "modal.kv.channel": "Csatorna",
267
+ "modal.kv.source": "Forrás",
268
+ "modal.kv.thread": "Thread",
269
+ "modal.kv.created": "Létrehozva",
270
+ "modal.kv.updated": "Frissítve",
271
+ "modal.kv.lastActivity": "Utolsó akt.",
272
+ "board.unknown": "Ismeretlen",
273
+ "board.empty": "— üres —",
274
+ "board.noTasks": "— nincs megjeleníthető taszk —",
275
+ "board.noActivity": "— nincs aktivitás —",
276
+ "board.noOwner": "nincs gazda",
277
+ "board.tasks": "{n} taszk",
278
+ "board.leadAvg": "átfutás átlag",
279
+ "board.inProgressAvg": "in_progress átlag",
280
+ "board.percentDone": "{p}% kész",
281
+ "board.waitingSince": "review óta – rád vár",
282
+ "badge.new": "új",
283
+ "badge.notes": "+jegyzet",
284
+ "badge.history": "+előzmény",
285
+ "badge.updated": "frissült",
286
+ "ctx.title": "📌 Kontextus",
287
+ "ctx.updated": "· frissítve {t}",
288
+ "ctx.none": "Nincs <code>context.json</code> a forrás mellett. Az agent a session során hozza létre (init prompt + döntések), hogy új session-ben is folytatható legyen a munka.",
289
+ "ctx.goal": "Cél",
290
+ "ctx.currentFocus": "Jelenlegi fókusz",
291
+ "ctx.initPrompt": "Init prompt",
292
+ "ctx.constraints": "Megkötések",
293
+ "ctx.noConstraints": "Nincs rögzített megkötés.",
294
+ "ctx.decisions": "Döntések ({n})",
295
+ "ctx.noDecisions": "Még nincs rögzített döntés.",
296
+ "ctx.openQuestions": "Nyitott kérdések",
297
+ "ctx.noOpenQuestions": "Nincs nyitott kérdés.",
298
+ "ctx.notes": "Megjegyzések",
299
+ "time.now": "most",
300
+ "time.soon": "hamarosan",
301
+ "time.years": "éve",
302
+ "time.months": "hónapja",
303
+ "time.weeks": "hete",
304
+ "time.days": "napja",
305
+ "time.hours": "órája",
306
+ "time.minutes": "perce",
307
+ "dur.days": "{d}n {h}ó",
308
+ "dur.hours": "{h}ó {m}p",
309
+ "dur.minutes": "{m}p",
310
+ "dur.seconds": "{s}mp",
311
+ "dur.none": "–",
312
+ },
313
+ };
314
+
315
+ let lang = (localStorage.getItem("tm.lang") === "hu") ? "hu" : "en";
316
+
317
+ export const I18n = {
318
+ get lang() { return lang; },
319
+ setLang(l) {
320
+ lang = (l === "hu") ? "hu" : "en";
321
+ localStorage.setItem("tm.lang", lang);
322
+ },
323
+ /** t("key", {n: 3}) → interpolates {n} placeholders in the translated string. */
324
+ t(key, vars) {
325
+ const table = STRINGS[lang] || STRINGS.en;
326
+ let s = (key in table) ? table[key] : (STRINGS.en[key] ?? key);
327
+ if (vars) {
328
+ for (const [k, v] of Object.entries(vars)) {
329
+ s = s.split("{" + k + "}").join(v);
330
+ }
331
+ }
332
+ return s;
333
+ },
334
+ /** The locale tag to use for Date#toLocaleString/toLocaleTimeString. */
335
+ locale() {
336
+ return lang === "hu" ? "hu-HU" : "en-US";
337
+ },
338
+ };
package/js/main.js ADDED
@@ -0,0 +1,3 @@
1
+ import { App } from "./App.js";
2
+
3
+ new App().init();
package/package.json ADDED
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "@mgeri1993/claude-task-manager",
3
+ "version": "1.0.0",
4
+ "description": "Standalone, dockerized, multi-project Kanban task manager and CLI for coordinating Claude Code AI agents (main agent + teammates) across projects — bilingual board, task.sh, and per-project agent wrappers.",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "bin": {
9
+ "ctm": "bin/ctm"
10
+ },
11
+ "files": [
12
+ "bin/",
13
+ "engine/",
14
+ "api/",
15
+ "js/",
16
+ "templates/",
17
+ "index.html",
18
+ "style.css",
19
+ "favicon.svg",
20
+ "favicon.ico",
21
+ "install.sh",
22
+ "docker-compose.yml",
23
+ "Dockerfile",
24
+ ".env.example",
25
+ "README.md",
26
+ "README.hu.md"
27
+ ],
28
+ "keywords": [
29
+ "claude-code",
30
+ "claude",
31
+ "anthropic",
32
+ "ai-agents",
33
+ "llm-agents",
34
+ "llm",
35
+ "ai",
36
+ "multi-agent",
37
+ "agent-orchestration",
38
+ "agent-coordination",
39
+ "sub-agents",
40
+ "teammates",
41
+ "autonomous-agents",
42
+ "coding-agent",
43
+ "kanban",
44
+ "kanban-board",
45
+ "task-manager",
46
+ "task-management",
47
+ "task-tracker",
48
+ "task-board",
49
+ "project-management",
50
+ "board",
51
+ "cli",
52
+ "cli-tool",
53
+ "command-line",
54
+ "developer-tools",
55
+ "dev-tools",
56
+ "productivity",
57
+ "workflow",
58
+ "workflow-automation",
59
+ "automation",
60
+ "docker",
61
+ "docker-compose",
62
+ "bash",
63
+ "shell",
64
+ "php",
65
+ "i18n",
66
+ "internationalization",
67
+ "bilingual",
68
+ "hungarian",
69
+ "team-collaboration",
70
+ "self-hosted"
71
+ ],
72
+ "homepage": "https://github.com/GeRiY/claude-task-manager#readme",
73
+ "repository": {
74
+ "type": "git",
75
+ "url": "git+https://github.com/GeRiY/claude-task-manager.git"
76
+ },
77
+ "bugs": {
78
+ "url": "https://github.com/GeRiY/claude-task-manager/issues"
79
+ },
80
+ "license": "MIT",
81
+ "os": [
82
+ "darwin",
83
+ "linux"
84
+ ]
85
+ }