@tenonhq/dovetail-dashboard 0.0.13

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/public/app.js ADDED
@@ -0,0 +1,1001 @@
1
+ // --- State ---
2
+
3
+ var scopesData = [];
4
+ var updateSetsCache = {};
5
+ var modalScopeKey = null;
6
+ var modalScopeSysId = null;
7
+
8
+ // ClickUp state
9
+ var activeTask = null;
10
+ var clickupTasks = {};
11
+ var clickupConfigured = false;
12
+ var availableStatuses = [];
13
+ var activeStatuses = ["in progress"];
14
+ var sidebarOpen = false;
15
+ var tasksLoading = false;
16
+ var taskSearchQuery = "";
17
+ var myTasksOnly = false;
18
+ var clickupUserId = null;
19
+
20
+ // --- API helpers ---
21
+
22
+ async function api(method, path, body) {
23
+ var opts = {
24
+ method: method,
25
+ headers: { "Content-Type": "application/json" },
26
+ };
27
+ if (body) opts.body = JSON.stringify(body);
28
+ var resp = await fetch(path, opts);
29
+ if (!resp.ok) {
30
+ var err = await resp.json().catch(function () {
31
+ return { error: "Request failed" };
32
+ });
33
+ throw new Error(err.error || "Request failed");
34
+ }
35
+ return resp.json();
36
+ }
37
+
38
+ // --- Toast ---
39
+
40
+ function toast(message, type) {
41
+ type = type || "success";
42
+ var container = document.getElementById("toast-container");
43
+ var el = document.createElement("div");
44
+ el.className = "toast " + type;
45
+ el.textContent = message;
46
+ container.appendChild(el);
47
+ setTimeout(function () {
48
+ el.remove();
49
+ }, 4000);
50
+ }
51
+
52
+ // --- Load scopes ---
53
+
54
+ async function loadScopes() {
55
+ try {
56
+ var data = await api("GET", "/api/scopes");
57
+ scopesData = data.scopes;
58
+ renderScopes();
59
+ } catch (e) {
60
+ document.getElementById("scope-grid").innerHTML =
61
+ '<div class="loading">Failed to load scopes: ' + e.message + "</div>";
62
+ }
63
+ }
64
+
65
+ async function loadConfig() {
66
+ try {
67
+ var data = await api("GET", "/api/config");
68
+ document.getElementById("instance-badge").textContent = data.instance;
69
+ } catch (e) {
70
+ // ignore
71
+ }
72
+ }
73
+
74
+ // --- Render Scopes ---
75
+
76
+ function renderScopes() {
77
+ var grid = document.getElementById("scope-grid");
78
+ grid.innerHTML = "";
79
+
80
+ scopesData.forEach(function (scope) {
81
+ var card = document.createElement("div");
82
+ card.className = "scope-card" + (scope.selected_update_set ? " has-selection" : "");
83
+ card.id = "card-" + scope.scope;
84
+
85
+ var selectedName = scope.selected_update_set ? scope.selected_update_set.name : "None";
86
+ var selectedId = scope.selected_update_set ? scope.selected_update_set.sys_id : "";
87
+
88
+ // Build activate button HTML if there's an active task
89
+ var activateHtml = "";
90
+ if (activeTask && scope.sys_id) {
91
+ var scopeActivated = activeTask.scopes && activeTask.scopes[scope.scope];
92
+ if (scopeActivated) {
93
+ activateHtml =
94
+ '<button class="btn-activate activated" disabled>Activated</button>';
95
+ } else {
96
+ activateHtml =
97
+ '<button class="btn-activate" onclick="activateScope(\'' +
98
+ scope.scope + "', '" + scope.sys_id +
99
+ "')\">Activate CU-" + activeTask.taskId + "</button>";
100
+ }
101
+ }
102
+
103
+ // Build task badge if scope is activated for current task
104
+ var taskBadgeHtml = "";
105
+ if (activeTask && activeTask.scopes && activeTask.scopes[scope.scope]) {
106
+ taskBadgeHtml =
107
+ '<div class="scope-task-badge">CU-' + activeTask.taskId + "</div>";
108
+ }
109
+
110
+ card.innerHTML =
111
+ '<div class="scope-header">' +
112
+ '<span class="scope-name">' + scope.scope + "</span>" +
113
+ '<span class="scope-id">' + (scope.sys_id ? scope.sys_id.substring(0, 8) + "..." : "not found") + "</span>" +
114
+ "</div>" +
115
+ '<div class="display-name">' + scope.display_name + "</div>" +
116
+ '<select class="update-set-select" id="select-' + scope.scope + '" onchange="onSelectChange(\'' + scope.scope + '\')">' +
117
+ '<option value="">-- No Update Set --</option>' +
118
+ '<option value="__loading" disabled>Loading...</option>' +
119
+ "</select>" +
120
+ '<div class="card-actions">' +
121
+ '<button class="btn btn-primary" onclick="openCreateModal(\'' + scope.scope + "', '" + scope.sys_id + "')\" " + (!scope.sys_id ? "disabled" : "") + ">New</button>" +
122
+ '<button class="btn btn-danger" id="close-btn-' + scope.scope + '" onclick="closeUpdateSet(\'' + scope.scope + '\')" disabled>Close</button>' +
123
+ '<button class="btn btn-clear" id="clear-btn-' + scope.scope + '" onclick="clearSelection(\'' + scope.scope + '\')" ' + (!scope.selected_update_set ? "disabled" : "") + ">Clear</button>" +
124
+ activateHtml +
125
+ "</div>" +
126
+ '<div class="quick-create">' +
127
+ '<input type="text" id="quick-name-' + scope.scope + '" placeholder="Update set name..." class="quick-create-input" onkeydown="if(event.key===\'Enter\')quickCreateUpdateSet(\'' + scope.scope + "', '" + scope.sys_id + "')\" />" +
128
+ '<button class="btn btn-primary btn-small" onclick="quickCreateUpdateSet(\'' + scope.scope + "', '" + scope.sys_id + "')\" " + (!scope.sys_id ? "disabled" : "") + ">Create</button>" +
129
+ "</div>" +
130
+ (scope.selected_update_set ? '<div class="selected-badge">Active for push</div>' : "") +
131
+ taskBadgeHtml;
132
+
133
+ grid.appendChild(card);
134
+ loadUpdateSets(scope.scope, selectedId);
135
+ });
136
+ }
137
+
138
+ async function loadUpdateSets(scope, selectedId) {
139
+ try {
140
+ var data = await api("GET", "/api/update-sets/" + scope);
141
+ updateSetsCache[scope] = data.update_sets;
142
+
143
+ var select = document.getElementById("select-" + scope);
144
+ select.innerHTML = '<option value="">-- No Update Set --</option>';
145
+
146
+ data.update_sets.forEach(function (us) {
147
+ var option = document.createElement("option");
148
+ option.value = us.sys_id;
149
+ option.textContent = us.name;
150
+ if (us.sys_id === selectedId) option.selected = true;
151
+ select.appendChild(option);
152
+ });
153
+
154
+ var closeBtn = document.getElementById("close-btn-" + scope);
155
+ if (closeBtn) closeBtn.disabled = !selectedId;
156
+ } catch (e) {
157
+ var selectEl = document.getElementById("select-" + scope);
158
+ if (selectEl) {
159
+ selectEl.innerHTML = '<option value="" disabled>Failed to load</option>';
160
+ }
161
+ }
162
+ }
163
+
164
+ // --- Update Set Actions ---
165
+
166
+ async function onSelectChange(scope) {
167
+ var select = document.getElementById("select-" + scope);
168
+ var sysId = select.value;
169
+ var name = select.options[select.selectedIndex] ? select.options[select.selectedIndex].textContent : "";
170
+
171
+ try {
172
+ await api("POST", "/api/select-update-set", {
173
+ scope: scope,
174
+ update_set_sys_id: sysId,
175
+ update_set_name: sysId ? name : null,
176
+ });
177
+
178
+ var scopeData = scopesData.find(function (s) { return s.scope === scope; });
179
+ if (scopeData) {
180
+ scopeData.selected_update_set = sysId ? { sys_id: sysId, name: name } : null;
181
+ }
182
+
183
+ var card = document.getElementById("card-" + scope);
184
+ if (card) {
185
+ if (sysId) {
186
+ card.classList.add("has-selection");
187
+ } else {
188
+ card.classList.remove("has-selection");
189
+ }
190
+ }
191
+
192
+ var closeBtn = document.getElementById("close-btn-" + scope);
193
+ if (closeBtn) closeBtn.disabled = !sysId;
194
+ var clearBtn = document.getElementById("clear-btn-" + scope);
195
+ if (clearBtn) clearBtn.disabled = !sysId;
196
+
197
+ var badge = card ? card.querySelector(".selected-badge") : null;
198
+ if (sysId && !badge) {
199
+ var b = document.createElement("div");
200
+ b.className = "selected-badge";
201
+ b.textContent = "Active for push";
202
+ card.appendChild(b);
203
+ } else if (!sysId && badge) {
204
+ badge.remove();
205
+ }
206
+
207
+ toast(sysId ? "Selected: " + name : "Cleared selection for " + scope);
208
+ } catch (e) {
209
+ toast("Failed to save: " + e.message, "error");
210
+ }
211
+ }
212
+
213
+ async function clearSelection(scope) {
214
+ var select = document.getElementById("select-" + scope);
215
+ if (select) select.value = "";
216
+ await onSelectChange(scope);
217
+ }
218
+
219
+ async function closeUpdateSet(scope) {
220
+ var select = document.getElementById("select-" + scope);
221
+ var sysId = select ? select.value : "";
222
+ if (!sysId) return;
223
+
224
+ var name = select.options[select.selectedIndex] ? select.options[select.selectedIndex].textContent : "";
225
+ if (!confirm("Close update set \"" + name + "\"?")) return;
226
+
227
+ try {
228
+ await api("PATCH", "/api/update-set/" + sysId + "/close");
229
+
230
+ await api("POST", "/api/select-update-set", {
231
+ scope: scope,
232
+ update_set_sys_id: "",
233
+ update_set_name: null,
234
+ });
235
+
236
+ var scopeData = scopesData.find(function (s) { return s.scope === scope; });
237
+ if (scopeData) scopeData.selected_update_set = null;
238
+
239
+ toast("Closed: " + name);
240
+ loadUpdateSets(scope, "");
241
+
242
+ var card = document.getElementById("card-" + scope);
243
+ if (card) {
244
+ card.classList.remove("has-selection");
245
+ var badge = card.querySelector(".selected-badge");
246
+ if (badge) badge.remove();
247
+ }
248
+ var clearBtn = document.getElementById("clear-btn-" + scope);
249
+ if (clearBtn) clearBtn.disabled = true;
250
+ var closeBtn = document.getElementById("close-btn-" + scope);
251
+ if (closeBtn) closeBtn.disabled = true;
252
+ } catch (e) {
253
+ toast("Failed to close: " + e.message, "error");
254
+ }
255
+ }
256
+
257
+ // --- Modal ---
258
+
259
+ function openCreateModal(scope, scopeSysId) {
260
+ modalScopeKey = scope;
261
+ modalScopeSysId = scopeSysId;
262
+ document.getElementById("modal-scope").value = scope;
263
+ document.getElementById("modal-name").value = activeTask ? activeTask.updateSetName : "";
264
+ document.getElementById("modal-description").value = activeTask ? activeTask.description : "";
265
+ document.getElementById("create-modal").classList.add("active");
266
+ document.getElementById("modal-name").focus();
267
+ }
268
+
269
+ function closeModal() {
270
+ document.getElementById("create-modal").classList.remove("active");
271
+ modalScopeKey = null;
272
+ modalScopeSysId = null;
273
+ }
274
+
275
+ async function createUpdateSet() {
276
+ var name = document.getElementById("modal-name").value.trim();
277
+ if (!name) {
278
+ toast("Name is required", "error");
279
+ return;
280
+ }
281
+
282
+ var btn = document.getElementById("modal-create-btn");
283
+ btn.disabled = true;
284
+ btn.textContent = "Creating...";
285
+
286
+ try {
287
+ var data = await api("POST", "/api/update-set", {
288
+ name: name,
289
+ scope: modalScopeKey,
290
+ scope_sys_id: modalScopeSysId,
291
+ description: document.getElementById("modal-description").value.trim(),
292
+ });
293
+
294
+ var newSysId = data.update_set.sys_id;
295
+
296
+ await api("POST", "/api/select-update-set", {
297
+ scope: modalScopeKey,
298
+ update_set_sys_id: newSysId,
299
+ update_set_name: name,
300
+ });
301
+
302
+ var scopeData = scopesData.find(function (s) { return s.scope === modalScopeKey; });
303
+ if (scopeData) {
304
+ scopeData.selected_update_set = { sys_id: newSysId, name: name };
305
+ }
306
+
307
+ toast("Created: " + name);
308
+ closeModal();
309
+
310
+ loadUpdateSets(modalScopeKey, newSysId);
311
+
312
+ var card = document.getElementById("card-" + modalScopeKey);
313
+ if (card) {
314
+ card.classList.add("has-selection");
315
+ var badge = card.querySelector(".selected-badge");
316
+ if (!badge) {
317
+ var b = document.createElement("div");
318
+ b.className = "selected-badge";
319
+ b.textContent = "Active for push";
320
+ card.appendChild(b);
321
+ }
322
+ }
323
+ var closeBtn = document.getElementById("close-btn-" + modalScopeKey);
324
+ if (closeBtn) closeBtn.disabled = false;
325
+ var clearBtn = document.getElementById("clear-btn-" + modalScopeKey);
326
+ if (clearBtn) clearBtn.disabled = false;
327
+ } catch (e) {
328
+ toast("Failed to create: " + e.message, "error");
329
+ } finally {
330
+ btn.disabled = false;
331
+ btn.textContent = "Create";
332
+ }
333
+ }
334
+
335
+ // --- Quick Create (inline per-scope) ---
336
+
337
+ async function quickCreateUpdateSet(scope, scopeSysId) {
338
+ var input = document.getElementById("quick-name-" + scope);
339
+ var name = input ? input.value.trim() : "";
340
+ if (!name) {
341
+ toast("Enter a name first", "error");
342
+ if (input) input.focus();
343
+ return;
344
+ }
345
+
346
+ // Find and disable the create button
347
+ var card = document.getElementById("card-" + scope);
348
+ var btn = card ? card.querySelector(".quick-create .btn") : null;
349
+ if (btn) {
350
+ btn.disabled = true;
351
+ btn.textContent = "Creating...";
352
+ }
353
+
354
+ try {
355
+ var data = await api("POST", "/api/update-set", {
356
+ name: name,
357
+ scope: scope,
358
+ scope_sys_id: scopeSysId,
359
+ });
360
+
361
+ var newSysId = data.update_set.sys_id;
362
+
363
+ await api("POST", "/api/select-update-set", {
364
+ scope: scope,
365
+ update_set_sys_id: newSysId,
366
+ update_set_name: name,
367
+ });
368
+
369
+ var scopeData = scopesData.find(function (s) { return s.scope === scope; });
370
+ if (scopeData) {
371
+ scopeData.selected_update_set = { sys_id: newSysId, name: name };
372
+ }
373
+
374
+ toast("Created: " + name);
375
+ if (input) input.value = "";
376
+
377
+ loadUpdateSets(scope, newSysId);
378
+
379
+ if (card) {
380
+ card.classList.add("has-selection");
381
+ var badge = card.querySelector(".selected-badge");
382
+ if (!badge) {
383
+ var b = document.createElement("div");
384
+ b.className = "selected-badge";
385
+ b.textContent = "Active for push";
386
+ card.appendChild(b);
387
+ }
388
+ }
389
+ var closeBtn = document.getElementById("close-btn-" + scope);
390
+ if (closeBtn) closeBtn.disabled = false;
391
+ var clearBtn = document.getElementById("clear-btn-" + scope);
392
+ if (clearBtn) clearBtn.disabled = false;
393
+ } catch (e) {
394
+ toast("Failed to create: " + e.message, "error");
395
+ } finally {
396
+ if (btn) {
397
+ btn.disabled = false;
398
+ btn.textContent = "Create";
399
+ }
400
+ }
401
+ }
402
+
403
+ // =============================================================================
404
+ // ClickUp Sidebar
405
+ // =============================================================================
406
+
407
+ // --- Sidebar toggle ---
408
+
409
+ function toggleSidebar() {
410
+ var sidebar = document.getElementById("task-sidebar");
411
+ var toggleBtn = document.getElementById("sidebar-toggle");
412
+
413
+ sidebarOpen = !sidebarOpen;
414
+ if (sidebarOpen) {
415
+ sidebar.classList.remove("collapsed");
416
+ toggleBtn.classList.add("active");
417
+ if (Object.keys(clickupTasks).length === 0 && activeStatuses.length > 0) {
418
+ loadClickUpTasks();
419
+ }
420
+ } else {
421
+ sidebar.classList.add("collapsed");
422
+ toggleBtn.classList.remove("active");
423
+ }
424
+ }
425
+
426
+ // --- Load ClickUp status ---
427
+
428
+ async function loadClickUpStatus() {
429
+ try {
430
+ var data = await api("GET", "/api/clickup/status");
431
+ clickupConfigured = data.configured;
432
+
433
+ if (clickupConfigured) {
434
+ document.getElementById("sidebar-toggle").style.display = "";
435
+ renderTaskToggles();
436
+
437
+ // Fetch current user for "My Tasks" filter
438
+ if (!clickupUserId) {
439
+ try {
440
+ var me = await api("GET", "/api/clickup/me");
441
+ clickupUserId = me.id;
442
+ } catch (meErr) {
443
+ // Non-critical — My Tasks filter just won't work
444
+ }
445
+ }
446
+ }
447
+
448
+ if (data.activeTask) {
449
+ activeTask = data.activeTask;
450
+ renderActiveTaskBanner();
451
+ renderActiveTaskChip();
452
+ renderScopes();
453
+ fillQuickCreateDefaults();
454
+ }
455
+ } catch (e) {
456
+ // ClickUp not available — just hide the button
457
+ }
458
+ }
459
+
460
+ // --- Load ClickUp tasks ---
461
+
462
+ async function loadClickUpTasks() {
463
+ if (tasksLoading) return;
464
+ tasksLoading = true;
465
+
466
+ var taskList = document.getElementById("task-list");
467
+ taskList.innerHTML = '<div class="sidebar-loading">Loading tasks...</div>';
468
+
469
+ try {
470
+ var statusParam = activeStatuses.join(",");
471
+ var data = await api("GET", "/api/clickup/tasks?statuses=" + encodeURIComponent(statusParam));
472
+ clickupTasks = data.byStatus || {};
473
+
474
+ // Merge discovered statuses with what we know
475
+ var newStatuses = data.statuses || [];
476
+ newStatuses.forEach(function (s) {
477
+ if (availableStatuses.indexOf(s) === -1) {
478
+ availableStatuses.push(s);
479
+ }
480
+ });
481
+
482
+ renderStatusFilters();
483
+ renderTaskList();
484
+ } catch (e) {
485
+ taskList.innerHTML =
486
+ '<div class="sidebar-loading">Failed to load: ' + e.message + "</div>";
487
+ } finally {
488
+ tasksLoading = false;
489
+ }
490
+ }
491
+
492
+ // --- Render status filters ---
493
+
494
+ function renderStatusFilters() {
495
+ var container = document.getElementById("status-filters");
496
+
497
+ // Ensure common statuses are always available
498
+ var defaults = ["in progress", "open", "review", "to do"];
499
+ defaults.forEach(function (s) {
500
+ if (availableStatuses.indexOf(s) === -1) {
501
+ availableStatuses.push(s);
502
+ }
503
+ });
504
+
505
+ container.innerHTML = "";
506
+ availableStatuses.forEach(function (status) {
507
+ var chip = document.createElement("button");
508
+ chip.className = "filter-chip" + (activeStatuses.indexOf(status) !== -1 ? " active" : "");
509
+ chip.textContent = status;
510
+ chip.onclick = function () {
511
+ var idx = activeStatuses.indexOf(status);
512
+ if (idx !== -1) {
513
+ activeStatuses.splice(idx, 1);
514
+ } else {
515
+ activeStatuses.push(status);
516
+ }
517
+ chip.classList.toggle("active");
518
+ loadClickUpTasks();
519
+ };
520
+ container.appendChild(chip);
521
+ });
522
+ }
523
+
524
+ // --- Render task list ---
525
+
526
+ function filterTasks(tasks) {
527
+ var filtered = tasks;
528
+ var query = taskSearchQuery.toLowerCase();
529
+ if (query) {
530
+ filtered = filtered.filter(function (task) {
531
+ var name = task.name.toLowerCase();
532
+ var id = (task.customId || task.id || "").toLowerCase();
533
+ return name.indexOf(query) !== -1 || id.indexOf(query) !== -1;
534
+ });
535
+ }
536
+ if (myTasksOnly && clickupUserId) {
537
+ filtered = filtered.filter(function (task) {
538
+ return task.assignees && task.assignees.some(function (a) {
539
+ return String(a.id) === String(clickupUserId);
540
+ });
541
+ });
542
+ }
543
+ return filtered;
544
+ }
545
+
546
+ function renderTaskList() {
547
+ var container = document.getElementById("task-list");
548
+ container.innerHTML = "";
549
+
550
+ var statusKeys = Object.keys(clickupTasks);
551
+ if (statusKeys.length === 0) {
552
+ container.innerHTML = '<div class="sidebar-loading">No tasks found</div>';
553
+ return;
554
+ }
555
+
556
+ var totalVisible = 0;
557
+
558
+ statusKeys.forEach(function (status) {
559
+ var tasks = filterTasks(clickupTasks[status] || []);
560
+ if (tasks.length === 0) return;
561
+
562
+ totalVisible += tasks.length;
563
+
564
+ var group = document.createElement("div");
565
+ group.className = "task-status-group";
566
+
567
+ var label = document.createElement("div");
568
+ label.className = "task-status-label";
569
+ label.textContent = status + " (" + tasks.length + ")";
570
+ group.appendChild(label);
571
+
572
+ tasks.forEach(function (task) {
573
+ var card = document.createElement("div");
574
+ card.className = "task-card" + (activeTask && activeTask.taskId === task.id ? " selected" : "");
575
+ card.onclick = function () {
576
+ selectTask(task);
577
+ };
578
+
579
+ var priorityHtml = "";
580
+ if (task.priority) {
581
+ var colors = { urgent: "#f50057", high: "#ff7043", normal: "#ffab40", low: "#29b6f6" };
582
+ var color = colors[task.priority] || "#888";
583
+ priorityHtml = '<span class="task-priority-dot" style="background:' + color + '"></span>';
584
+ }
585
+
586
+ var assigneeHtml = "";
587
+ if (task.assignees && task.assignees.length > 0) {
588
+ var initials = task.assignees.map(function (a) {
589
+ return a.initials || (a.username || "?").substring(0, 2).toUpperCase();
590
+ }).join(", ");
591
+ assigneeHtml = '<span class="task-card-assignee">' + escapeHtml(initials) + "</span>";
592
+ }
593
+
594
+ card.innerHTML =
595
+ '<div class="task-card-name">' + escapeHtml(task.name) + "</div>" +
596
+ '<div class="task-card-meta">' +
597
+ '<span class="task-card-id">' + (task.customId || task.id) + "</span>" +
598
+ priorityHtml +
599
+ assigneeHtml +
600
+ "</div>";
601
+
602
+ group.appendChild(card);
603
+ });
604
+
605
+ container.appendChild(group);
606
+ });
607
+
608
+ if (totalVisible === 0) {
609
+ container.innerHTML = '<div class="sidebar-loading">No matching tasks</div>';
610
+ }
611
+ }
612
+
613
+ // --- Select task ---
614
+
615
+ async function selectTask(task) {
616
+ // If already selected, deselect
617
+ if (activeTask && activeTask.taskId === task.id) {
618
+ await deselectTask();
619
+ return;
620
+ }
621
+
622
+ try {
623
+ var data = await api("POST", "/api/clickup/select-task", {
624
+ taskId: task.id,
625
+ taskName: task.name,
626
+ taskDescription: task.description || "",
627
+ taskUrl: task.url || "",
628
+ });
629
+
630
+ activeTask = data.activeTask;
631
+ toast("Task selected: " + task.name);
632
+
633
+ renderActiveTaskBanner();
634
+ renderActiveTaskChip();
635
+ renderTaskList();
636
+ renderScopes();
637
+
638
+ // Fill quick-create inputs with the generated update set name
639
+ fillQuickCreateDefaults();
640
+ } catch (e) {
641
+ toast("Failed to select task: " + e.message, "error");
642
+ }
643
+ }
644
+
645
+ // --- Deselect task ---
646
+
647
+ async function deselectTask() {
648
+ if (!confirm("Deselect active task? Update sets will remain.")) return;
649
+
650
+ try {
651
+ await api("POST", "/api/clickup/deselect-task");
652
+ activeTask = null;
653
+ toast("Task deselected");
654
+
655
+ clearQuickCreateDefaults();
656
+ renderActiveTaskBanner();
657
+ renderActiveTaskChip();
658
+ renderTaskList();
659
+ renderScopes();
660
+ } catch (e) {
661
+ toast("Failed to deselect: " + e.message, "error");
662
+ }
663
+ }
664
+
665
+ // --- Render active task banner (sidebar) ---
666
+
667
+ function renderActiveTaskBanner() {
668
+ var banner = document.getElementById("active-task-banner");
669
+ if (!activeTask) {
670
+ banner.style.display = "none";
671
+ banner.innerHTML = "";
672
+ return;
673
+ }
674
+
675
+ banner.style.display = "";
676
+
677
+ var scopeKeys = activeTask.scopes ? Object.keys(activeTask.scopes) : [];
678
+ var scopeText = scopeKeys.length > 0
679
+ ? "<span>" + scopeKeys.join(", ") + "</span>"
680
+ : "None activated yet";
681
+
682
+ banner.innerHTML =
683
+ '<div class="active-task-name">' + escapeHtml(activeTask.taskName) + "</div>" +
684
+ '<div class="active-task-us-name">' + escapeHtml(activeTask.updateSetName) + "</div>" +
685
+ '<div class="active-task-scopes">Scopes: ' + scopeText + "</div>" +
686
+ '<button class="btn-deselect" onclick="deselectTask()">Deselect</button>';
687
+ }
688
+
689
+ // --- Render active task chip (header) ---
690
+
691
+ function renderActiveTaskChip() {
692
+ var chip = document.getElementById("active-task-chip");
693
+ if (!activeTask) {
694
+ chip.style.display = "none";
695
+ chip.textContent = "";
696
+ return;
697
+ }
698
+
699
+ chip.style.display = "";
700
+ chip.textContent = "CU-" + activeTask.taskId;
701
+ chip.title = activeTask.updateSetName;
702
+ chip.onclick = function () {
703
+ if (!sidebarOpen) toggleSidebar();
704
+ };
705
+ }
706
+
707
+ // --- Activate scope for current task ---
708
+
709
+ async function activateScope(scope, scopeSysId) {
710
+ if (!activeTask) {
711
+ toast("No active task selected", "error");
712
+ return;
713
+ }
714
+
715
+ // Find and disable the activate button
716
+ var card = document.getElementById("card-" + scope);
717
+ var activateBtn = card ? card.querySelector(".btn-activate") : null;
718
+ if (activateBtn) {
719
+ activateBtn.disabled = true;
720
+ activateBtn.textContent = "Activating...";
721
+ }
722
+
723
+ try {
724
+ var data = await api("POST", "/api/clickup/activate-scope", {
725
+ scope: scope,
726
+ scope_sys_id: scopeSysId,
727
+ });
728
+
729
+ var us = data.update_set;
730
+ var verb = data.created ? "Created" : "Found";
731
+ toast(verb + " update set for " + scope + ": " + us.name);
732
+
733
+ // Update activeTask scopes
734
+ if (!activeTask.scopes) activeTask.scopes = {};
735
+ activeTask.scopes[scope] = { sys_id: us.sys_id, name: us.name };
736
+
737
+ // Update scopesData so the dropdown reflects the new selection
738
+ var scopeData = scopesData.find(function (s) { return s.scope === scope; });
739
+ if (scopeData) {
740
+ scopeData.selected_update_set = { sys_id: us.sys_id, name: us.name };
741
+ }
742
+
743
+ // Re-render to update all UI elements
744
+ renderActiveTaskBanner();
745
+ renderScopes();
746
+ } catch (e) {
747
+ toast("Failed to activate scope: " + e.message, "error");
748
+ if (activateBtn) {
749
+ activateBtn.disabled = false;
750
+ activateBtn.textContent = "Activate CU-" + activeTask.taskId;
751
+ }
752
+ }
753
+ }
754
+
755
+ // --- Auto-activate all scopes ---
756
+
757
+ async function autoActivateAllScopes() {
758
+ if (!activeTask) return;
759
+
760
+ // Disable all activate buttons and show progress
761
+ scopesData.forEach(function (scope) {
762
+ var card = document.getElementById("card-" + scope.scope);
763
+ var btn = card ? card.querySelector(".btn-activate") : null;
764
+ if (btn && !btn.classList.contains("activated")) {
765
+ btn.disabled = true;
766
+ btn.textContent = "Activating...";
767
+ }
768
+ });
769
+
770
+ try {
771
+ var data = await api("POST", "/api/clickup/activate-all-scopes");
772
+ var results = data.results || [];
773
+
774
+ // Update activeTask from server response
775
+ if (data.activeTask) {
776
+ activeTask = data.activeTask;
777
+ }
778
+
779
+ var created = 0;
780
+ var found = 0;
781
+ var errors = 0;
782
+
783
+ results.forEach(function (r) {
784
+ if (r.error) {
785
+ errors++;
786
+ return;
787
+ }
788
+ if (r.created) {
789
+ created++;
790
+ } else {
791
+ found++;
792
+ }
793
+
794
+ // Update scopesData
795
+ var scopeData = scopesData.find(function (s) { return s.scope === r.scope; });
796
+ if (scopeData) {
797
+ scopeData.selected_update_set = { sys_id: r.update_set.sys_id, name: r.update_set.name };
798
+ }
799
+ });
800
+
801
+ var parts = [];
802
+ if (created > 0) parts.push(created + " created");
803
+ if (found > 0) parts.push(found + " found");
804
+ if (errors > 0) parts.push(errors + " failed");
805
+ toast("Update sets: " + parts.join(", "));
806
+
807
+ renderActiveTaskBanner();
808
+ renderScopes();
809
+ } catch (e) {
810
+ toast("Failed to auto-activate scopes: " + e.message, "error");
811
+ renderScopes();
812
+ }
813
+ }
814
+
815
+ // --- Task toggles (My Tasks) ---
816
+
817
+ function renderTaskToggles() {
818
+ var container = document.getElementById("task-toggles");
819
+ container.innerHTML = "";
820
+
821
+ if (!clickupConfigured) return;
822
+
823
+ var btn = document.createElement("button");
824
+ btn.className = "filter-chip" + (myTasksOnly ? " active" : "");
825
+ btn.textContent = "my tasks";
826
+ btn.onclick = function () {
827
+ myTasksOnly = !myTasksOnly;
828
+ btn.classList.toggle("active");
829
+ renderTaskList();
830
+ };
831
+ container.appendChild(btn);
832
+ }
833
+
834
+ // --- Quick-create defaults from active task ---
835
+
836
+ function fillQuickCreateDefaults() {
837
+ if (!activeTask) return;
838
+ scopesData.forEach(function (scope) {
839
+ var input = document.getElementById("quick-name-" + scope.scope);
840
+ if (input && !input.value.trim()) {
841
+ input.value = activeTask.updateSetName;
842
+ }
843
+ });
844
+ }
845
+
846
+ function clearQuickCreateDefaults() {
847
+ scopesData.forEach(function (scope) {
848
+ var input = document.getElementById("quick-name-" + scope.scope);
849
+ if (input) {
850
+ input.value = "";
851
+ }
852
+ });
853
+ }
854
+
855
+ // --- Recent Edits ---
856
+
857
+ var recentEdits = [];
858
+ var recentEditsInterval = null;
859
+
860
+ async function loadRecentEdits() {
861
+ try {
862
+ var data = await api("GET", "/api/recent-edits");
863
+ recentEdits = data.edits || [];
864
+ renderRecentEdits();
865
+ } catch (e) {
866
+ // Silently fail — panel just stays hidden or stale
867
+ }
868
+ }
869
+
870
+ function timeAgo(timestamp) {
871
+ var now = new Date();
872
+ var then = new Date(timestamp);
873
+ var seconds = Math.floor((now - then) / 1000);
874
+ if (seconds < 60) return seconds + "s ago";
875
+ var minutes = Math.floor(seconds / 60);
876
+ if (minutes < 60) return minutes + "m ago";
877
+ var hours = Math.floor(minutes / 60);
878
+ if (hours < 24) return hours + "h ago";
879
+ return Math.floor(hours / 24) + "d ago";
880
+ }
881
+
882
+ function renderRecentEdits() {
883
+ var panel = document.getElementById("recent-edits-panel");
884
+ var list = document.getElementById("recent-edits-list");
885
+
886
+ if (recentEdits.length === 0) {
887
+ panel.style.display = "none";
888
+ return;
889
+ }
890
+
891
+ panel.style.display = "";
892
+ list.innerHTML = "";
893
+
894
+ // Build a lookup of expected update sets from scopesData
895
+ var expectedUpdateSets = {};
896
+ scopesData.forEach(function (scope) {
897
+ if (scope.selected_update_set) {
898
+ expectedUpdateSets[scope.scope] = scope.selected_update_set.name;
899
+ }
900
+ });
901
+
902
+ recentEdits.forEach(function (edit) {
903
+ var row = document.createElement("div");
904
+ row.className = "recent-edit-row";
905
+
906
+ var nameEl = document.createElement("span");
907
+ nameEl.className = "recent-edit-name";
908
+ nameEl.textContent = edit.name;
909
+ nameEl.title = edit.tableName + "/" + edit.name;
910
+
911
+ var scopeEl = document.createElement("span");
912
+ scopeEl.className = "recent-edit-scope";
913
+ scopeEl.textContent = edit.scope;
914
+
915
+ var updateSetEl = document.createElement("span");
916
+ var isDefault = edit.updateSet.toLowerCase().indexOf("default") !== -1;
917
+ var expected = expectedUpdateSets[edit.scope];
918
+ var isMismatch = expected && edit.updateSet !== expected && edit.updateSet !== "unknown";
919
+ updateSetEl.className = "recent-edit-update-set" + (isDefault || isMismatch ? " warning" : "");
920
+ updateSetEl.textContent = edit.updateSet;
921
+ if (isMismatch && expected) {
922
+ updateSetEl.title = "Expected: " + expected;
923
+ }
924
+
925
+ var timeEl = document.createElement("span");
926
+ timeEl.className = "recent-edit-time";
927
+ timeEl.textContent = timeAgo(edit.timestamp);
928
+
929
+ row.appendChild(nameEl);
930
+ row.appendChild(scopeEl);
931
+ row.appendChild(updateSetEl);
932
+ row.appendChild(timeEl);
933
+ list.appendChild(row);
934
+ });
935
+ }
936
+
937
+ // --- Utility ---
938
+
939
+ function escapeHtml(str) {
940
+ var div = document.createElement("div");
941
+ div.appendChild(document.createTextNode(str));
942
+ return div.innerHTML;
943
+ }
944
+
945
+ // --- Keyboard ---
946
+
947
+ document.addEventListener("keydown", function (e) {
948
+ if (e.key === "Escape") {
949
+ closeModal();
950
+ if (sidebarOpen) toggleSidebar();
951
+ }
952
+ });
953
+
954
+ // --- Refresh ---
955
+
956
+ async function refreshDashboard() {
957
+ var btn = document.getElementById("refresh-btn");
958
+ btn.disabled = true;
959
+ btn.textContent = "Refreshing...";
960
+
961
+ try {
962
+ await loadScopes();
963
+ await loadRecentEdits();
964
+ if (clickupConfigured && activeTask) {
965
+ await loadClickUpStatus();
966
+ }
967
+ toast("Dashboard refreshed");
968
+ } catch (e) {
969
+ toast("Refresh failed: " + e.message, "error");
970
+ } finally {
971
+ btn.disabled = false;
972
+ btn.textContent = "Refresh";
973
+ }
974
+ }
975
+
976
+ // --- Init ---
977
+
978
+ loadConfig();
979
+ loadScopes();
980
+ loadRecentEdits();
981
+ loadClickUpStatus();
982
+
983
+ // Poll recent edits every 10 seconds
984
+ recentEditsInterval = setInterval(loadRecentEdits, 60000);
985
+
986
+ // Wire up sidebar toggle and close buttons
987
+ document.getElementById("sidebar-toggle").addEventListener("click", toggleSidebar);
988
+ document.getElementById("sidebar-close").addEventListener("click", toggleSidebar);
989
+ document.getElementById("refresh-btn").addEventListener("click", refreshDashboard);
990
+
991
+ // Render initial status filter chips
992
+ renderStatusFilters();
993
+
994
+ // Wire up task search input
995
+ document.getElementById("task-search").addEventListener("input", function (e) {
996
+ taskSearchQuery = e.target.value;
997
+ renderTaskList();
998
+ });
999
+
1000
+ // Render task toggles (My Tasks)
1001
+ renderTaskToggles();