agkan 2.13.0 → 2.14.1

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 (39) hide show
  1. package/dist/board/boardRenderer.d.ts.map +1 -1
  2. package/dist/board/boardRenderer.js +26 -2
  3. package/dist/board/boardRenderer.js.map +1 -1
  4. package/dist/board/boardRoutes.d.ts +1 -1
  5. package/dist/board/boardRoutes.d.ts.map +1 -1
  6. package/dist/board/boardRoutes.js +63 -4
  7. package/dist/board/boardRoutes.js.map +1 -1
  8. package/dist/board/boardStyles.d.ts +1 -1
  9. package/dist/board/boardStyles.d.ts.map +1 -1
  10. package/dist/board/boardStyles.js +19 -8
  11. package/dist/board/boardStyles.js.map +1 -1
  12. package/dist/board/client/board.js +474 -136
  13. package/dist/cli/commands/export.d.ts +7 -0
  14. package/dist/cli/commands/export.d.ts.map +1 -0
  15. package/dist/cli/commands/export.js +30 -0
  16. package/dist/cli/commands/export.js.map +1 -0
  17. package/dist/cli/commands/import.d.ts +7 -0
  18. package/dist/cli/commands/import.d.ts.map +1 -0
  19. package/dist/cli/commands/import.js +44 -0
  20. package/dist/cli/commands/import.js.map +1 -0
  21. package/dist/cli/index.js +6 -0
  22. package/dist/cli/index.js.map +1 -1
  23. package/dist/services/ExportImportService.d.ts +84 -0
  24. package/dist/services/ExportImportService.d.ts.map +1 -0
  25. package/dist/services/ExportImportService.js +222 -0
  26. package/dist/services/ExportImportService.js.map +1 -0
  27. package/dist/services/ProcessService.d.ts +54 -0
  28. package/dist/services/ProcessService.d.ts.map +1 -0
  29. package/dist/services/ProcessService.js +147 -0
  30. package/dist/services/ProcessService.js.map +1 -0
  31. package/dist/services/TmuxService.d.ts +2 -0
  32. package/dist/services/TmuxService.d.ts.map +1 -0
  33. package/dist/services/TmuxService.js +7 -0
  34. package/dist/services/TmuxService.js.map +1 -0
  35. package/dist/services/index.d.ts +2 -0
  36. package/dist/services/index.d.ts.map +1 -1
  37. package/dist/services/index.js +3 -1
  38. package/dist/services/index.js.map +1 -1
  39. package/package.json +1 -1
@@ -149,115 +149,6 @@
149
149
  document.addEventListener("dragend", stopAutoScroll);
150
150
  }
151
151
 
152
- // src/board/client/addTaskModal.ts
153
- function openAddModal(elements, status) {
154
- elements.addStatus.value = status;
155
- elements.addTitle.value = "";
156
- elements.addBody.value = "";
157
- elements.addPriority.value = "";
158
- elements.addModal.classList.add("show");
159
- elements.addTitle.focus();
160
- }
161
- async function submitAddTask(elements) {
162
- const title = elements.addTitle.value.trim();
163
- if (!title) {
164
- elements.addTitle.focus();
165
- return;
166
- }
167
- const status = elements.addStatus.value;
168
- elements.addModal.classList.remove("show");
169
- try {
170
- const res = await fetch("/api/tasks", {
171
- method: "POST",
172
- headers: { "Content-Type": "application/json" },
173
- body: JSON.stringify({
174
- title,
175
- body: elements.addBody.value.trim() || null,
176
- status,
177
- priority: elements.addPriority.value || null
178
- })
179
- });
180
- if (!res.ok) throw new Error("Server error");
181
- location.reload();
182
- } catch {
183
- showToast("Failed to add task");
184
- }
185
- }
186
- function initAddTaskModal() {
187
- const elements = {
188
- addModal: document.getElementById("add-modal"),
189
- addTitle: document.getElementById("add-title"),
190
- addBody: document.getElementById("add-body"),
191
- addPriority: document.getElementById("add-priority"),
192
- addStatus: document.getElementById("add-status")
193
- };
194
- document.querySelectorAll(".add-btn").forEach((btn) => {
195
- btn.addEventListener("click", (e) => {
196
- e.stopPropagation();
197
- openAddModal(elements, btn.dataset.status);
198
- });
199
- });
200
- document.getElementById("add-cancel")?.addEventListener("click", () => {
201
- elements.addModal.classList.remove("show");
202
- });
203
- elements.addModal.addEventListener("click", (e) => {
204
- if (e.target === elements.addModal) elements.addModal.classList.remove("show");
205
- });
206
- elements.addTitle.addEventListener("keydown", (e) => {
207
- if (e.key === "Enter" && !e.isComposing) {
208
- e.preventDefault();
209
- document.getElementById("add-submit").click();
210
- }
211
- });
212
- document.getElementById("add-submit")?.addEventListener("click", () => submitAddTask(elements));
213
- }
214
-
215
- // src/board/client/contextMenu.ts
216
- async function deleteCard(card) {
217
- const taskId = card.dataset.id;
218
- const status = card.dataset.status;
219
- if (!confirm("Delete task #" + taskId + "?")) return;
220
- card.remove();
221
- updateCount(status);
222
- try {
223
- const res = await fetch("/api/tasks/" + taskId, { method: "DELETE" });
224
- if (!res.ok) throw new Error("Server error");
225
- } catch {
226
- location.reload();
227
- showToast("Failed to delete task");
228
- }
229
- }
230
- function initContextMenu() {
231
- const ctxMenu = document.getElementById("context-menu");
232
- let ctxTargetCard = null;
233
- document.addEventListener("contextmenu", (e) => {
234
- const card = e.target.closest(".card");
235
- if (!card) {
236
- ctxMenu.style.display = "none";
237
- return;
238
- }
239
- e.preventDefault();
240
- ctxTargetCard = card;
241
- ctxMenu.style.left = e.clientX + "px";
242
- ctxMenu.style.top = e.clientY + "px";
243
- ctxMenu.style.display = "block";
244
- });
245
- document.addEventListener("click", (e) => {
246
- if (!e.target.closest("#context-menu")) {
247
- ctxMenu.style.display = "none";
248
- ctxTargetCard = null;
249
- }
250
- });
251
- document.getElementById("ctx-delete")?.addEventListener("click", async (e) => {
252
- e.stopPropagation();
253
- ctxMenu.style.display = "none";
254
- if (!ctxTargetCard) return;
255
- const card = ctxTargetCard;
256
- ctxTargetCard = null;
257
- await deleteCard(card);
258
- });
259
- }
260
-
261
152
  // src/board/client/tags.ts
262
153
  var allAvailableTags = [];
263
154
  var _getDetailTaskId = null;
@@ -455,11 +346,13 @@
455
346
  var _renderDetailPanel = null;
456
347
  var _showUpdateWarning = null;
457
348
  var _getDetailTaskId2 = null;
349
+ var _setActiveCard = null;
458
350
  function registerDetailPanelCallbacks(callbacks) {
459
351
  _openTaskDetail = callbacks.openTaskDetail;
460
352
  _renderDetailPanel = callbacks.renderDetailPanel;
461
353
  _showUpdateWarning = callbacks.showUpdateWarning;
462
354
  _getDetailTaskId2 = callbacks.getDetailTaskId;
355
+ _setActiveCard = callbacks.setActiveCard;
463
356
  }
464
357
  function attachCardListeners(body) {
465
358
  body.querySelectorAll(".card").forEach((card) => {
@@ -470,10 +363,52 @@
470
363
  });
471
364
  });
472
365
  }
366
+ function applyIncrementalCardUpdate(body, newHtml) {
367
+ const template = document.createElement("div");
368
+ template.innerHTML = newHtml;
369
+ const newCards = Array.from(template.querySelectorAll(".card"));
370
+ const existingCards = /* @__PURE__ */ new Map();
371
+ body.querySelectorAll(".card").forEach((card) => {
372
+ const id = card.dataset.id;
373
+ if (id) existingCards.set(id, card);
374
+ });
375
+ const newCardIds = /* @__PURE__ */ new Set();
376
+ newCards.forEach((card) => {
377
+ const id = card.dataset.id;
378
+ if (id) newCardIds.add(id);
379
+ });
380
+ existingCards.forEach((card, id) => {
381
+ if (!newCardIds.has(id)) {
382
+ card.remove();
383
+ }
384
+ });
385
+ newCards.forEach((newCard, index) => {
386
+ const id = newCard.dataset.id;
387
+ const newUpdatedAt = newCard.dataset.updatedAt;
388
+ const existing = id ? existingCards.get(id) : void 0;
389
+ if (existing) {
390
+ const existingUpdatedAt = existing.dataset.updatedAt;
391
+ let activeCard;
392
+ if (newUpdatedAt !== existingUpdatedAt) {
393
+ existing.replaceWith(newCard);
394
+ activeCard = newCard;
395
+ } else {
396
+ activeCard = existing;
397
+ }
398
+ const currentChild = body.children[index];
399
+ if (currentChild !== activeCard) {
400
+ body.insertBefore(activeCard, currentChild || null);
401
+ }
402
+ } else {
403
+ const currentChild = body.children[index];
404
+ body.insertBefore(newCard, currentChild || null);
405
+ }
406
+ });
407
+ }
473
408
  function updateColumnHtml(col) {
474
409
  const body = document.getElementById("col-" + col.status);
475
410
  if (!body) return;
476
- body.innerHTML = col.html;
411
+ applyIncrementalCardUpdate(body, col.html);
477
412
  const colEl = body.closest(".column");
478
413
  if (colEl) {
479
414
  const countEl = colEl.querySelector(".column-count");
@@ -509,6 +444,9 @@
509
444
  const data = await res.json();
510
445
  data.columns.forEach(updateColumnHtml);
511
446
  const detailTaskId2 = _getDetailTaskId2 ? _getDetailTaskId2() : null;
447
+ if (detailTaskId2 !== null && _setActiveCard) {
448
+ _setActiveCard(detailTaskId2);
449
+ }
512
450
  if (detailTaskId2 !== null) {
513
451
  await refreshOpenDetailPanel(detailTaskId2);
514
452
  }
@@ -522,16 +460,11 @@
522
460
  if (!res.ok) return;
523
461
  const data = await res.json();
524
462
  const ts = data.updatedAt;
525
- const detailPanel = document.getElementById("detail-panel");
526
463
  if (lastUpdatedAt === null) {
527
464
  lastUpdatedAt = ts;
528
465
  } else if (ts !== lastUpdatedAt) {
529
466
  lastUpdatedAt = ts;
530
- if (detailPanel.classList.contains("open")) {
531
- await refreshBoardCards();
532
- } else {
533
- location.reload();
534
- }
467
+ await refreshBoardCards();
535
468
  }
536
469
  } catch {
537
470
  }
@@ -541,16 +474,316 @@
541
474
  pollBoardUpdates();
542
475
  }
543
476
 
477
+ // src/board/client/addTaskModal.ts
478
+ var selectedTags = [];
479
+ var tagInputValue = "";
480
+ var tagFocusedIndex = -1;
481
+ function getFilteredAddTags() {
482
+ const selectedIds = new Set(selectedTags.map((t) => t.id));
483
+ const available = allAvailableTags.filter((t) => !selectedIds.has(t.id));
484
+ if (!tagInputValue.trim()) return available;
485
+ const q = tagInputValue.toLowerCase();
486
+ return available.filter((t) => t.name.toLowerCase().includes(q));
487
+ }
488
+ function renderAddTagPills(control, input) {
489
+ control.querySelectorAll(".tag-pill").forEach((p) => p.remove());
490
+ selectedTags.forEach((t) => {
491
+ const pill = document.createElement("span");
492
+ pill.className = "tag-pill";
493
+ pill.dataset.tagId = String(t.id);
494
+ const label = document.createTextNode(t.name);
495
+ const removeBtn = document.createElement("button");
496
+ removeBtn.className = "tag-pill-remove";
497
+ removeBtn.title = "Remove tag";
498
+ removeBtn.innerHTML = "×";
499
+ removeBtn.addEventListener("click", (e) => {
500
+ e.stopPropagation();
501
+ const idx = selectedTags.findIndex((x) => x.id === t.id);
502
+ if (idx !== -1) selectedTags.splice(idx, 1);
503
+ renderAddTagPills(control, input);
504
+ });
505
+ pill.appendChild(label);
506
+ pill.appendChild(removeBtn);
507
+ control.insertBefore(pill, input);
508
+ });
509
+ input.placeholder = selectedTags.length === 0 ? "Add tags..." : "";
510
+ }
511
+ function renderAddTagDropdown(dropdown) {
512
+ const filtered = getFilteredAddTags();
513
+ dropdown.innerHTML = "";
514
+ tagFocusedIndex = -1;
515
+ if (filtered.length === 0) {
516
+ const noOpt = document.createElement("div");
517
+ noOpt.className = "tag-select-no-options";
518
+ noOpt.textContent = tagInputValue ? "No matching tags" : "No tags available";
519
+ dropdown.appendChild(noOpt);
520
+ } else {
521
+ filtered.forEach((t, i) => {
522
+ const opt = document.createElement("div");
523
+ opt.className = "tag-select-option";
524
+ opt.dataset.tagId = String(t.id);
525
+ opt.textContent = t.name;
526
+ opt.addEventListener("mouseover", () => setAddTagFocused(dropdown, i));
527
+ opt.addEventListener("mousedown", (e) => {
528
+ e.preventDefault();
529
+ selectAddTag(t.id, dropdown, document.getElementById("add-tag-input"));
530
+ });
531
+ dropdown.appendChild(opt);
532
+ });
533
+ }
534
+ }
535
+ function setAddTagFocused(dropdown, index) {
536
+ const opts = dropdown.querySelectorAll(".tag-select-option");
537
+ opts.forEach((o, i) => o.classList.toggle("focused", i === index));
538
+ tagFocusedIndex = index;
539
+ }
540
+ function selectAddTag(tagId, dropdown, input) {
541
+ const tag = allAvailableTags.find((t) => t.id === tagId);
542
+ if (!tag) return;
543
+ selectedTags.push(tag);
544
+ input.value = "";
545
+ tagInputValue = "";
546
+ const control = document.getElementById("add-tag-select-control");
547
+ renderAddTagPills(control, input);
548
+ renderAddTagDropdown(dropdown);
549
+ }
550
+ function initAddTagSelector() {
551
+ const control = document.getElementById("add-tag-select-control");
552
+ const dropdown = document.getElementById("add-tag-select-dropdown");
553
+ if (!control || !dropdown) return;
554
+ const input = document.createElement("input");
555
+ input.className = "tag-select-input";
556
+ input.id = "add-tag-input";
557
+ input.type = "text";
558
+ input.autocomplete = "off";
559
+ control.appendChild(input);
560
+ control.addEventListener("click", () => input.focus());
561
+ input.addEventListener("focus", () => {
562
+ renderAddTagDropdown(dropdown);
563
+ dropdown.classList.add("open");
564
+ });
565
+ input.addEventListener(
566
+ "blur",
567
+ () => setTimeout(() => {
568
+ dropdown.classList.remove("open");
569
+ tagFocusedIndex = -1;
570
+ }, 150)
571
+ );
572
+ input.addEventListener("input", () => {
573
+ tagInputValue = input.value;
574
+ renderAddTagDropdown(dropdown);
575
+ if (!dropdown.classList.contains("open")) dropdown.classList.add("open");
576
+ });
577
+ input.addEventListener("keydown", (e) => {
578
+ const filtered = getFilteredAddTags();
579
+ const opts = dropdown.querySelectorAll(".tag-select-option");
580
+ if (e.key === "ArrowDown") {
581
+ e.preventDefault();
582
+ setAddTagFocused(dropdown, Math.min(tagFocusedIndex + 1, opts.length - 1));
583
+ } else if (e.key === "ArrowUp") {
584
+ e.preventDefault();
585
+ setAddTagFocused(dropdown, Math.max(tagFocusedIndex - 1, 0));
586
+ } else if (e.key === "Enter") {
587
+ e.preventDefault();
588
+ if (tagFocusedIndex >= 0 && filtered[tagFocusedIndex]) {
589
+ selectAddTag(filtered[tagFocusedIndex].id, dropdown, input);
590
+ }
591
+ } else if (e.key === "Escape") {
592
+ dropdown.classList.remove("open");
593
+ input.blur();
594
+ } else if (e.key === "Backspace" && input.value === "" && selectedTags.length > 0) {
595
+ e.preventDefault();
596
+ selectedTags.splice(selectedTags.length - 1, 1);
597
+ renderAddTagPills(control, input);
598
+ renderAddTagDropdown(dropdown);
599
+ }
600
+ });
601
+ }
602
+ function addMetadataRow(container) {
603
+ const row = document.createElement("div");
604
+ row.className = "metadata-row";
605
+ const keyInput = document.createElement("input");
606
+ keyInput.type = "text";
607
+ keyInput.className = "metadata-row-key";
608
+ keyInput.placeholder = "Key";
609
+ const valueInput = document.createElement("input");
610
+ valueInput.type = "text";
611
+ valueInput.className = "metadata-row-value";
612
+ valueInput.placeholder = "Value";
613
+ const removeBtn = document.createElement("button");
614
+ removeBtn.type = "button";
615
+ removeBtn.className = "metadata-row-remove";
616
+ removeBtn.title = "Remove";
617
+ removeBtn.innerHTML = "×";
618
+ removeBtn.addEventListener("click", () => {
619
+ row.remove();
620
+ });
621
+ row.appendChild(keyInput);
622
+ row.appendChild(valueInput);
623
+ row.appendChild(removeBtn);
624
+ container.appendChild(row);
625
+ }
626
+ function collectMetadata(container) {
627
+ const rows = container.querySelectorAll(".metadata-row");
628
+ const result = [];
629
+ rows.forEach((row) => {
630
+ const key = (row.querySelector(".metadata-row-key")?.value ?? "").trim();
631
+ const value = (row.querySelector(".metadata-row-value")?.value ?? "").trim();
632
+ if (key) result.push({ key, value });
633
+ });
634
+ return result;
635
+ }
636
+ function resetAddModal(elements) {
637
+ elements.addTitle.value = "";
638
+ elements.addBody.value = "";
639
+ elements.addPriority.value = "";
640
+ selectedTags = [];
641
+ tagInputValue = "";
642
+ tagFocusedIndex = -1;
643
+ const control = document.getElementById("add-tag-select-control");
644
+ const input = document.getElementById("add-tag-input");
645
+ if (control && input) {
646
+ renderAddTagPills(control, input);
647
+ }
648
+ elements.addMetadataRows.innerHTML = "";
649
+ }
650
+ function openAddModal(elements, status) {
651
+ elements.addStatus.value = status;
652
+ resetAddModal(elements);
653
+ elements.addModal.classList.add("show");
654
+ elements.addTitle.focus();
655
+ }
656
+ async function submitAddTask(elements) {
657
+ const title = elements.addTitle.value.trim();
658
+ if (!title) {
659
+ elements.addTitle.focus();
660
+ return;
661
+ }
662
+ const status = elements.addStatus.value;
663
+ elements.addModal.classList.remove("show");
664
+ const tags = selectedTags.map((t) => t.id);
665
+ const metadata = collectMetadata(elements.addMetadataRows);
666
+ try {
667
+ const res = await fetch("/api/tasks", {
668
+ method: "POST",
669
+ headers: { "Content-Type": "application/json" },
670
+ body: JSON.stringify({
671
+ title,
672
+ body: elements.addBody.value.trim() || null,
673
+ status,
674
+ priority: elements.addPriority.value || null,
675
+ tags: tags.length > 0 ? tags : void 0,
676
+ metadata: metadata.length > 0 ? metadata : void 0
677
+ })
678
+ });
679
+ if (!res.ok) throw new Error("Server error");
680
+ await refreshBoardCards();
681
+ } catch {
682
+ showToast("Failed to add task");
683
+ }
684
+ }
685
+ function initAddTaskModal() {
686
+ const elements = {
687
+ addModal: document.getElementById("add-modal"),
688
+ addTitle: document.getElementById("add-title"),
689
+ addBody: document.getElementById("add-body"),
690
+ addPriority: document.getElementById("add-priority"),
691
+ addStatus: document.getElementById("add-status"),
692
+ addTagControl: document.getElementById("add-tag-select-control"),
693
+ addTagDropdown: document.getElementById("add-tag-select-dropdown"),
694
+ addMetadataRows: document.getElementById("add-metadata-rows")
695
+ };
696
+ initAddTagSelector();
697
+ document.querySelectorAll(".add-btn").forEach((btn) => {
698
+ btn.addEventListener("click", (e) => {
699
+ e.stopPropagation();
700
+ openAddModal(elements, btn.dataset.status);
701
+ });
702
+ });
703
+ document.getElementById("add-cancel")?.addEventListener("click", () => {
704
+ elements.addModal.classList.remove("show");
705
+ });
706
+ elements.addModal.addEventListener("click", (e) => {
707
+ if (e.target === elements.addModal) elements.addModal.classList.remove("show");
708
+ });
709
+ elements.addTitle.addEventListener("keydown", (e) => {
710
+ if (e.key === "Enter" && !e.isComposing) {
711
+ e.preventDefault();
712
+ document.getElementById("add-submit").click();
713
+ }
714
+ });
715
+ document.getElementById("add-metadata-add-row")?.addEventListener("click", () => {
716
+ addMetadataRow(elements.addMetadataRows);
717
+ });
718
+ document.getElementById("add-submit")?.addEventListener("click", () => submitAddTask(elements));
719
+ }
720
+
721
+ // src/board/client/contextMenu.ts
722
+ async function deleteCard(card) {
723
+ const taskId = card.dataset.id;
724
+ const status = card.dataset.status;
725
+ if (!confirm("Delete task #" + taskId + "?")) return;
726
+ card.remove();
727
+ updateCount(status);
728
+ try {
729
+ const res = await fetch("/api/tasks/" + taskId, { method: "DELETE" });
730
+ if (!res.ok) throw new Error("Server error");
731
+ } catch {
732
+ location.reload();
733
+ showToast("Failed to delete task");
734
+ }
735
+ }
736
+ function initContextMenu() {
737
+ const ctxMenu = document.getElementById("context-menu");
738
+ let ctxTargetCard = null;
739
+ document.addEventListener("contextmenu", (e) => {
740
+ const card = e.target.closest(".card");
741
+ if (!card) {
742
+ ctxMenu.style.display = "none";
743
+ return;
744
+ }
745
+ e.preventDefault();
746
+ ctxTargetCard = card;
747
+ ctxMenu.style.left = e.clientX + "px";
748
+ ctxMenu.style.top = e.clientY + "px";
749
+ ctxMenu.style.display = "block";
750
+ });
751
+ document.addEventListener("click", (e) => {
752
+ if (!e.target.closest("#context-menu")) {
753
+ ctxMenu.style.display = "none";
754
+ ctxTargetCard = null;
755
+ }
756
+ });
757
+ document.getElementById("ctx-delete")?.addEventListener("click", async (e) => {
758
+ e.stopPropagation();
759
+ ctxMenu.style.display = "none";
760
+ if (!ctxTargetCard) return;
761
+ const card = ctxTargetCard;
762
+ ctxTargetCard = null;
763
+ await deleteCard(card);
764
+ });
765
+ }
766
+
544
767
  // src/board/client/detailPanel.ts
545
768
  var detailTaskId = null;
546
769
  var lastTab = "details";
547
770
  function getDetailTaskId() {
548
771
  return detailTaskId;
549
772
  }
773
+ function setActiveCard(taskId) {
774
+ document.querySelectorAll(".card.active").forEach((card) => {
775
+ card.classList.remove("active");
776
+ });
777
+ if (taskId !== null) {
778
+ const card = document.querySelector('.card[data-id="' + taskId + '"]');
779
+ if (card) card.classList.add("active");
780
+ }
781
+ }
550
782
  function closeDetailPanel() {
551
783
  const detailPanel = document.getElementById("detail-panel");
552
784
  detailPanel.classList.remove("open");
553
785
  detailPanel.style.width = "";
786
+ setActiveCard(null);
554
787
  detailTaskId = null;
555
788
  }
556
789
  function switchTab(tabName) {
@@ -794,6 +1027,10 @@
794
1027
  html += "</div>";
795
1028
  return html;
796
1029
  }
1030
+ function autoResizeTextarea(el) {
1031
+ el.style.height = "auto";
1032
+ el.style.height = el.scrollHeight + "px";
1033
+ }
797
1034
  function renderMetadataTable(metadata) {
798
1035
  const otherMeta = metadata.filter((m) => m.key !== "priority");
799
1036
  if (otherMeta.length === 0) return "";
@@ -833,9 +1070,8 @@
833
1070
  if (hasRelations) {
834
1071
  html += renderRelationsHtml(parent, blockedBy, blocking);
835
1072
  }
836
- html += renderEditableTextFields(task);
837
1073
  html += renderMetadataTable(metadata);
838
- html += '<div class="detail-timestamp">created ' + relativeTime(task.created_at) + " &middot; updated " + relativeTime(task.updated_at) + "</div>";
1074
+ html += renderEditableTextFields(task);
839
1075
  return html;
840
1076
  }
841
1077
  function renderDetailPanel(data) {
@@ -850,6 +1086,20 @@
850
1086
  detailsPane.innerHTML = renderDetailPanelHtml(data);
851
1087
  detailsPane.style.padding = "20px";
852
1088
  }
1089
+ const footer = document.getElementById("detail-panel-footer");
1090
+ if (footer) {
1091
+ footer.innerHTML = '<span class="detail-footer-timestamp">created ' + relativeTime(task.created_at) + " &middot; updated " + relativeTime(task.updated_at) + '</span><button id="detail-save-btn">Save</button>';
1092
+ document.getElementById("detail-save-btn")?.addEventListener("click", saveDetailTask);
1093
+ }
1094
+ const textarea = document.getElementById("detail-edit-body");
1095
+ if (textarea) {
1096
+ requestAnimationFrame(() => {
1097
+ autoResizeTextarea(textarea);
1098
+ });
1099
+ textarea.addEventListener("input", () => {
1100
+ autoResizeTextarea(textarea);
1101
+ });
1102
+ }
853
1103
  loadAllTags().then(() => renderTagsSection([...tags])).catch((err) => {
854
1104
  console.error("[agkan] renderDetailPanel tags failed", err);
855
1105
  });
@@ -864,6 +1114,7 @@
864
1114
  if (!res.ok) throw new Error("Server error");
865
1115
  const data = await res.json();
866
1116
  renderDetailPanel(data);
1117
+ setActiveCard(Number(taskId));
867
1118
  if (!detailPanel.classList.contains("open")) {
868
1119
  const preferredWidth = detailPanel.dataset.preferredWidth || String(PANEL_DEFAULT_WIDTH2);
869
1120
  detailPanel.style.width = preferredWidth + "px";
@@ -1036,7 +1287,6 @@
1036
1287
  switchTab(btn.dataset.tab);
1037
1288
  });
1038
1289
  initPanelResize(detailPanel);
1039
- document.getElementById("detail-save-btn")?.addEventListener("click", saveDetailTask);
1040
1290
  document.querySelectorAll(".card").forEach((card) => {
1041
1291
  card.addEventListener("click", async (e) => {
1042
1292
  if (e.defaultPrevented) return;
@@ -1047,7 +1297,8 @@
1047
1297
  openTaskDetail,
1048
1298
  renderDetailPanel,
1049
1299
  showUpdateWarning,
1050
- getDetailTaskId
1300
+ getDetailTaskId,
1301
+ setActiveCard
1051
1302
  });
1052
1303
  registerGetDetailTaskId(getDetailTaskId);
1053
1304
  }
@@ -1279,30 +1530,17 @@
1279
1530
  }
1280
1531
  });
1281
1532
  }
1282
- async function executePurge(purgeConfirmBtn, purgeModal, purgeResultEl) {
1283
- purgeConfirmBtn.disabled = true;
1284
- purgeConfirmBtn.textContent = "Purging...";
1533
+ async function executePurge() {
1285
1534
  try {
1286
1535
  const res = await fetch("/api/tasks/purge", {
1287
1536
  method: "POST",
1288
1537
  headers: { "Content-Type": "application/json" },
1289
1538
  body: JSON.stringify({})
1290
1539
  });
1291
- const data = await res.json();
1292
1540
  if (res.ok) {
1293
- purgeResultEl.textContent = "Purged " + data.count + " task(s).";
1294
- setTimeout(() => {
1295
- purgeModal.classList.remove("show");
1296
- }, 1500);
1297
- location.reload();
1298
- } else {
1299
- purgeResultEl.textContent = "Error: " + (data.error || "Unknown error");
1541
+ await refreshBoardCards();
1300
1542
  }
1301
1543
  } catch {
1302
- purgeResultEl.textContent = "Failed to purge tasks.";
1303
- } finally {
1304
- purgeConfirmBtn.disabled = false;
1305
- purgeConfirmBtn.textContent = "Purge";
1306
1544
  }
1307
1545
  }
1308
1546
  function initPurgeModal(burgerDropdown) {
@@ -1318,7 +1556,10 @@
1318
1556
  purgeCancelBtn.addEventListener("click", () => {
1319
1557
  purgeModal.classList.remove("show");
1320
1558
  });
1321
- purgeConfirmBtn.addEventListener("click", () => executePurge(purgeConfirmBtn, purgeModal, purgeResultEl));
1559
+ purgeConfirmBtn.addEventListener("click", () => {
1560
+ purgeModal.classList.remove("show");
1561
+ void executePurge();
1562
+ });
1322
1563
  }
1323
1564
  function initVersionModal(burgerDropdown) {
1324
1565
  const versionModal = document.getElementById("version-info-modal");
@@ -1340,11 +1581,108 @@
1340
1581
  versionModal.classList.remove("show");
1341
1582
  });
1342
1583
  }
1584
+ function initExportModal(burgerDropdown) {
1585
+ document.getElementById("burger-export-tasks")?.addEventListener("click", () => {
1586
+ burgerDropdown.classList.remove("open");
1587
+ const a = document.createElement("a");
1588
+ a.href = "/api/export";
1589
+ a.download = "";
1590
+ document.body.appendChild(a);
1591
+ a.click();
1592
+ document.body.removeChild(a);
1593
+ });
1594
+ }
1595
+ function initImportModal(burgerDropdown) {
1596
+ const importModal = document.getElementById("import-modal");
1597
+ const importCancelBtn = document.getElementById("import-cancel-btn");
1598
+ const importConfirmBtn = document.getElementById("import-confirm-btn");
1599
+ const importResultEl = document.getElementById("import-result");
1600
+ const importDropZone = document.getElementById("import-drop-zone");
1601
+ const importFileInput = document.getElementById("import-file-input");
1602
+ if (!importModal || !importCancelBtn || !importConfirmBtn || !importResultEl || !importDropZone || !importFileInput) {
1603
+ return;
1604
+ }
1605
+ const safeImportModal = importModal;
1606
+ const safeImportCancelBtn = importCancelBtn;
1607
+ const safeImportConfirmBtn = importConfirmBtn;
1608
+ const safeImportResultEl = importResultEl;
1609
+ const safeImportDropZone = importDropZone;
1610
+ const safeImportFileInput = importFileInput;
1611
+ let selectedFile = null;
1612
+ function setFile(file) {
1613
+ selectedFile = file;
1614
+ safeImportResultEl.textContent = `Selected: ${file.name}`;
1615
+ safeImportResultEl.style.color = "#64748b";
1616
+ safeImportConfirmBtn.disabled = false;
1617
+ }
1618
+ document.getElementById("burger-import-tasks")?.addEventListener("click", () => {
1619
+ burgerDropdown.classList.remove("open");
1620
+ selectedFile = null;
1621
+ safeImportResultEl.textContent = "";
1622
+ safeImportConfirmBtn.disabled = true;
1623
+ safeImportFileInput.value = "";
1624
+ safeImportModal.classList.add("show");
1625
+ });
1626
+ safeImportCancelBtn.addEventListener("click", () => {
1627
+ safeImportModal.classList.remove("show");
1628
+ });
1629
+ safeImportFileInput.addEventListener("change", () => {
1630
+ const file = safeImportFileInput.files?.[0];
1631
+ if (file) setFile(file);
1632
+ });
1633
+ safeImportDropZone.addEventListener("dragover", (e) => {
1634
+ e.preventDefault();
1635
+ safeImportDropZone.style.borderColor = "#3b82f6";
1636
+ });
1637
+ safeImportDropZone.addEventListener("dragleave", () => {
1638
+ safeImportDropZone.style.borderColor = "#94a3b8";
1639
+ });
1640
+ safeImportDropZone.addEventListener("drop", (e) => {
1641
+ e.preventDefault();
1642
+ safeImportDropZone.style.borderColor = "#94a3b8";
1643
+ const file = e.dataTransfer?.files?.[0];
1644
+ if (file) setFile(file);
1645
+ });
1646
+ safeImportConfirmBtn.addEventListener("click", async () => {
1647
+ if (!selectedFile) return;
1648
+ safeImportConfirmBtn.disabled = true;
1649
+ safeImportConfirmBtn.textContent = "Importing...";
1650
+ try {
1651
+ const text = await selectedFile.text();
1652
+ const data = JSON.parse(text);
1653
+ const res = await fetch("/api/import", {
1654
+ method: "POST",
1655
+ headers: { "Content-Type": "application/json" },
1656
+ body: JSON.stringify(data)
1657
+ });
1658
+ const result = await res.json();
1659
+ if (res.ok) {
1660
+ safeImportResultEl.textContent = `Imported ${result.importedCount} task(s) successfully.`;
1661
+ safeImportResultEl.style.color = "#16a34a";
1662
+ setTimeout(() => {
1663
+ safeImportModal.classList.remove("show");
1664
+ location.reload();
1665
+ }, 1500);
1666
+ } else {
1667
+ safeImportResultEl.textContent = "Error: " + (result.error || "Unknown error");
1668
+ safeImportResultEl.style.color = "#dc2626";
1669
+ }
1670
+ } catch {
1671
+ safeImportResultEl.textContent = "Failed to import tasks. Invalid JSON file.";
1672
+ safeImportResultEl.style.color = "#dc2626";
1673
+ } finally {
1674
+ safeImportConfirmBtn.disabled = false;
1675
+ safeImportConfirmBtn.textContent = "Import";
1676
+ }
1677
+ });
1678
+ }
1343
1679
  function initBurgerMenu() {
1344
1680
  const burgerBtn = document.getElementById("burger-menu-btn");
1345
1681
  const burgerDropdown = document.getElementById("burger-menu-dropdown");
1346
1682
  initBurgerToggle(burgerBtn, burgerDropdown);
1347
1683
  initPurgeModal(burgerDropdown);
1684
+ initExportModal(burgerDropdown);
1685
+ initImportModal(burgerDropdown);
1348
1686
  initVersionModal(burgerDropdown);
1349
1687
  initDarkMode();
1350
1688
  }