@inkobytes/nexus 1.0.0 → 1.0.2
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/CHANGELOG.md +11 -0
- package/README.md +29 -0
- package/bin/nexus.js +96 -51
- package/nexus-dashboard/docs/index.html +82 -99
- package/nexus-dashboard/index.html +210 -19
- package/nexus-dashboard/style.css +419 -38
- package/package.json +2 -1
- package/src/commands/completion.js +124 -0
- package/src/commands/dashboard.js +67 -0
- package/src/commands/doctor.js +81 -4
- package/src/commands/init.js +2 -0
- package/src/commands/status.js +5 -0
|
@@ -133,7 +133,9 @@
|
|
|
133
133
|
</section>
|
|
134
134
|
<section class="wide report-section" id="nexus-report">
|
|
135
135
|
<h2><span data-icon="file-text"></span>Nexus Report</h2>
|
|
136
|
-
<
|
|
136
|
+
<div id="report-intro" class="report-intro"></div>
|
|
137
|
+
<div id="report-tabs" class="report-tabs"></div>
|
|
138
|
+
<div id="report-panel" class="report-panel"></div>
|
|
137
139
|
</section>
|
|
138
140
|
</div>
|
|
139
141
|
</main>
|
|
@@ -141,9 +143,11 @@
|
|
|
141
143
|
<script>
|
|
142
144
|
let currentSnapshot = null;
|
|
143
145
|
let queueView = { mode: 'queue', agent: null };
|
|
146
|
+
let reportView = '';
|
|
144
147
|
let repoRoot = '';
|
|
145
148
|
let lastQueueSig = '';
|
|
146
149
|
let lastLocksSig = '';
|
|
150
|
+
let sectionObserver = null;
|
|
147
151
|
|
|
148
152
|
function queueViewFromHash(hash) {
|
|
149
153
|
const value = String(hash || '').replace(/^#/, '').toLowerCase();
|
|
@@ -199,6 +203,8 @@
|
|
|
199
203
|
const subagentsTag = group.subagents > 0 ? '<span class="lock-group-subagents">+subagents (' + group.subagents + ')</span>' : '';
|
|
200
204
|
const modelText = Array.from(group.modelLabels).join(', ');
|
|
201
205
|
const modelTag = modelText ? '<span class="lock-group-model">' + escapeHtml(modelText) + '</span>' : '';
|
|
206
|
+
const agentName = escapeHtml(formatAgentName(agent));
|
|
207
|
+
const agentBadge = '<span class="lock-group-agent"><span class="lock-group-agent-icon"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 8V4H8"></path><rect width="16" height="12" x="4" y="8" rx="2"></rect><path d="M2 14h2"></path><path d="M20 14h2"></path><path d="M15 13v2"></path><path d="M9 13v2"></path></svg></span><span>' + agentName + '</span></span>';
|
|
202
208
|
const files = group.locks.map(lock => {
|
|
203
209
|
const href = 'vscode://file' + escapeHtml(repoRoot + '/' + lock.target);
|
|
204
210
|
const intent = lock.intent ? '<div class="lock-meta"><span class="lock-intent">' + escapeHtml(lock.intent) + '</span></div>' : '';
|
|
@@ -215,7 +221,7 @@
|
|
|
215
221
|
return '<details class="lock-group" open>'
|
|
216
222
|
+ '<summary class="lock-group-header">'
|
|
217
223
|
+ trustDot
|
|
218
|
-
+
|
|
224
|
+
+ agentBadge
|
|
219
225
|
+ modelTag
|
|
220
226
|
+ subagentsTag
|
|
221
227
|
+ '<span class="lock-group-count">' + group.locks.length + ' file' + (group.locks.length !== 1 ? 's' : '') + '</span>'
|
|
@@ -239,9 +245,7 @@
|
|
|
239
245
|
renderAgentBars(chartTasks);
|
|
240
246
|
fillFeed('standup', data.standup);
|
|
241
247
|
fillFeed('releases', data.releases);
|
|
242
|
-
|
|
243
|
-
? data.report
|
|
244
|
-
: 'No Nexus report entries yet.';
|
|
248
|
+
renderReportSection(data);
|
|
245
249
|
document.getElementById('git').innerHTML = data.dirtyFiles.length
|
|
246
250
|
? '<ul>' + data.dirtyFiles.map(line => {
|
|
247
251
|
const xy = line.slice(0, 2);
|
|
@@ -254,6 +258,7 @@
|
|
|
254
258
|
: label) + '</li>';
|
|
255
259
|
}).join('') + '</ul>'
|
|
256
260
|
: '<p class="muted">Working tree clean.</p>';
|
|
261
|
+
syncSidebarActive();
|
|
257
262
|
}
|
|
258
263
|
|
|
259
264
|
document.querySelectorAll('[data-queue-mode], [data-next-agent]').forEach((button) => {
|
|
@@ -268,6 +273,8 @@
|
|
|
268
273
|
});
|
|
269
274
|
|
|
270
275
|
window.addEventListener('hashchange', syncQueueViewFromHash);
|
|
276
|
+
window.addEventListener('hashchange', syncSidebarActive);
|
|
277
|
+
document.addEventListener('scroll', syncSidebarActive, { passive: true });
|
|
271
278
|
|
|
272
279
|
function renderQueuePanel(data) {
|
|
273
280
|
if (!data) return;
|
|
@@ -419,6 +426,8 @@
|
|
|
419
426
|
const dateText = completedAt && !Number.isNaN(completedAt.getTime())
|
|
420
427
|
? completedAt.toLocaleString()
|
|
421
428
|
: 'unknown time';
|
|
429
|
+
const agentName = formatAgentName(entry.agent || 'unknown');
|
|
430
|
+
const agentBadge = '<span class="ledger-agent"><span class="ledger-agent-icon"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 8V4H8"></path><rect width="16" height="12" x="4" y="8" rx="2"></rect><path d="M2 14h2"></path><path d="M20 14h2"></path><path d="M15 13v2"></path><path d="M9 13v2"></path></svg></span><span>' + escapeHtml(agentName) + '</span></span>';
|
|
422
431
|
const files = Array.isArray(entry.files) ? entry.files : [];
|
|
423
432
|
const fileText = files.length ? files.join(', ') : 'no files';
|
|
424
433
|
const sha = entry.sha && entry.sha !== 'unknown' ? entry.sha.slice(0, 7) : 'unknown';
|
|
@@ -434,7 +443,7 @@
|
|
|
434
443
|
+ '</div>'
|
|
435
444
|
+ '<div class="ledger-meta">'
|
|
436
445
|
+ '<span>' + escapeHtml(dateText) + '</span>'
|
|
437
|
-
+
|
|
446
|
+
+ agentBadge
|
|
438
447
|
+ '<span>' + escapeHtml(entry.epic || 'unknown epic') + '</span>'
|
|
439
448
|
+ '<span>' + escapeHtml(entry.cost || 'unknown cost') + '</span>'
|
|
440
449
|
+ '<span>' + escapeHtml(sha) + '</span>'
|
|
@@ -445,6 +454,128 @@
|
|
|
445
454
|
+ '</article>';
|
|
446
455
|
}
|
|
447
456
|
|
|
457
|
+
function renderReportSection(data) {
|
|
458
|
+
const intro = formatReportIntro(data?.reportIntro);
|
|
459
|
+
const blocks = Array.isArray(data?.reportBlocks) ? data.reportBlocks : [];
|
|
460
|
+
const introEl = document.getElementById('report-intro');
|
|
461
|
+
const tabsEl = document.getElementById('report-tabs');
|
|
462
|
+
const panelEl = document.getElementById('report-panel');
|
|
463
|
+
|
|
464
|
+
introEl.textContent = intro;
|
|
465
|
+
introEl.hidden = !intro;
|
|
466
|
+
|
|
467
|
+
if (!blocks.length) {
|
|
468
|
+
tabsEl.innerHTML = '';
|
|
469
|
+
panelEl.innerHTML = '<p class="muted">No Nexus report entries yet.</p>';
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const monthGroups = getRecentReportMonthGroups(blocks, data?.generatedAt);
|
|
474
|
+
if (!monthGroups.length) {
|
|
475
|
+
tabsEl.innerHTML = '';
|
|
476
|
+
panelEl.innerHTML = '<p class="muted">No dated Nexus report entries yet.</p>';
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (!monthGroups.some((group) => group.key === reportView)) {
|
|
481
|
+
reportView = monthGroups[0].key;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
tabsEl.innerHTML = monthGroups.map((group) => renderReportTab(group)).join('');
|
|
485
|
+
tabsEl.querySelectorAll('[data-report-view]').forEach((button) => {
|
|
486
|
+
button.addEventListener('click', () => {
|
|
487
|
+
reportView = button.dataset.reportView || '';
|
|
488
|
+
renderReportSection(currentSnapshot);
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
const activeGroup = monthGroups.find((group) => group.key === reportView) || monthGroups[0];
|
|
493
|
+
panelEl.innerHTML = activeGroup ? renderReportMonthGroup(activeGroup) : '<p class="muted">No Nexus report entries in this month.</p>';
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function formatReportIntro(value) {
|
|
497
|
+
return String(value || '')
|
|
498
|
+
.replace(/^#\s+[^\n]+\n*/m, '')
|
|
499
|
+
.trim();
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function getRecentReportMonthGroups(blocks, generatedAt) {
|
|
503
|
+
const reference = new Date(generatedAt || Date.now());
|
|
504
|
+
const refMonthIndex = reference.getFullYear() * 12 + reference.getMonth();
|
|
505
|
+
const filtered = blocks.filter((block) => {
|
|
506
|
+
if (!block.monthKey) return false;
|
|
507
|
+
const parts = String(block.monthKey).split('-');
|
|
508
|
+
if (parts.length !== 2) return false;
|
|
509
|
+
const year = Number.parseInt(parts[0], 10);
|
|
510
|
+
const month = Number.parseInt(parts[1], 10) - 1;
|
|
511
|
+
if (!Number.isInteger(year) || !Number.isInteger(month)) return false;
|
|
512
|
+
const diff = refMonthIndex - (year * 12 + month);
|
|
513
|
+
return diff >= 0 && diff < 12;
|
|
514
|
+
});
|
|
515
|
+
const groups = [];
|
|
516
|
+
const seen = new Map();
|
|
517
|
+
|
|
518
|
+
for (const block of filtered) {
|
|
519
|
+
const key = block.monthKey || 'undated';
|
|
520
|
+
if (!seen.has(key)) {
|
|
521
|
+
const group = {
|
|
522
|
+
key,
|
|
523
|
+
label: block.monthLabel || 'Undated',
|
|
524
|
+
tabLabel: formatReportTabLabel(block.monthKey, generatedAt),
|
|
525
|
+
items: [],
|
|
526
|
+
};
|
|
527
|
+
seen.set(key, group);
|
|
528
|
+
groups.push(group);
|
|
529
|
+
}
|
|
530
|
+
seen.get(key).items.push(block);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return groups;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function formatReportTabLabel(monthKey, generatedAt) {
|
|
537
|
+
const parts = String(monthKey || '').split('-');
|
|
538
|
+
if (parts.length !== 2) return 'Undated';
|
|
539
|
+
const year = Number.parseInt(parts[0], 10);
|
|
540
|
+
const month = Number.parseInt(parts[1], 10) - 1;
|
|
541
|
+
if (!Number.isInteger(year) || !Number.isInteger(month)) return 'Undated';
|
|
542
|
+
|
|
543
|
+
const date = new Date(year, month, 1);
|
|
544
|
+
const reference = new Date(generatedAt || Date.now());
|
|
545
|
+
const sameYear = date.getFullYear() === reference.getFullYear();
|
|
546
|
+
return date.toLocaleString('en-US', sameYear
|
|
547
|
+
? { month: 'short' }
|
|
548
|
+
: { month: 'short', year: 'numeric' });
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function renderReportTab(group) {
|
|
552
|
+
const active = group.key === reportView ? ' active' : '';
|
|
553
|
+
return '<button type="button" class="tab-button' + active + '" data-report-view="' + escapeHtml(group.key) + '">'
|
|
554
|
+
+ escapeHtml(group.tabLabel || group.label)
|
|
555
|
+
+ '</button>';
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function renderReportMonthGroup(group) {
|
|
559
|
+
return '<section class="report-month-group">'
|
|
560
|
+
+ '<div class="report-month-heading">' + escapeHtml(group.label) + '</div>'
|
|
561
|
+
+ '<div class="report-month-list">' + group.items.map(renderReportBlock).join('') + '</div>'
|
|
562
|
+
+ '</section>';
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function renderReportBlock(block) {
|
|
566
|
+
const details = block.details
|
|
567
|
+
? '<pre class="report-entry-body">' + escapeHtml(block.details) + '</pre>'
|
|
568
|
+
: '';
|
|
569
|
+
|
|
570
|
+
return '<article class="report-entry">'
|
|
571
|
+
+ '<div class="report-entry-head">'
|
|
572
|
+
+ '<div class="report-entry-target">' + escapeHtml(block.target || 'Untitled report entry') + '</div>'
|
|
573
|
+
+ '<div class="report-entry-time">' + escapeHtml(block.timestamp || 'Undated') + '</div>'
|
|
574
|
+
+ '</div>'
|
|
575
|
+
+ details
|
|
576
|
+
+ '</article>';
|
|
577
|
+
}
|
|
578
|
+
|
|
448
579
|
function getChartTasks(data) {
|
|
449
580
|
const seen = new Set();
|
|
450
581
|
const tasks = [];
|
|
@@ -475,6 +606,51 @@
|
|
|
475
606
|
return String(agent || '').replace(/^@/, '').toLowerCase();
|
|
476
607
|
}
|
|
477
608
|
|
|
609
|
+
function syncSidebarActive(preferredId) {
|
|
610
|
+
const links = Array.from(document.querySelectorAll('.sidebar .nav-link[href^="#"]'));
|
|
611
|
+
if (!links.length) return;
|
|
612
|
+
|
|
613
|
+
let activeId = preferredId || '';
|
|
614
|
+
if (!activeId) {
|
|
615
|
+
const hash = String(window.location.hash || '').replace(/^#/, '');
|
|
616
|
+
if (hash) activeId = hash;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (!activeId) {
|
|
620
|
+
const sections = links
|
|
621
|
+
.map((link) => document.querySelector(link.getAttribute('href')))
|
|
622
|
+
.filter(Boolean);
|
|
623
|
+
const threshold = 160;
|
|
624
|
+
const current = sections.find((section) => {
|
|
625
|
+
const rect = section.getBoundingClientRect();
|
|
626
|
+
return rect.top <= threshold && rect.bottom > threshold;
|
|
627
|
+
});
|
|
628
|
+
activeId = current?.id || sections[0]?.id || '';
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
links.forEach((link) => {
|
|
632
|
+
const href = String(link.getAttribute('href') || '');
|
|
633
|
+
const id = href.replace(/^#/, '');
|
|
634
|
+
link.classList.toggle('active', id === activeId);
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function initSectionObserver() {
|
|
639
|
+
const sections = Array.from(document.querySelectorAll('main section[id]'));
|
|
640
|
+
if (!sections.length || typeof IntersectionObserver === 'undefined') {
|
|
641
|
+
syncSidebarActive();
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
if (sectionObserver) sectionObserver.disconnect();
|
|
645
|
+
sectionObserver = new IntersectionObserver((entries) => {
|
|
646
|
+
const visible = entries
|
|
647
|
+
.filter((entry) => entry.isIntersecting)
|
|
648
|
+
.sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];
|
|
649
|
+
if (visible?.target?.id) syncSidebarActive(visible.target.id);
|
|
650
|
+
}, { rootMargin: '-18% 0px -58% 0px', threshold: [0.2, 0.35, 0.55] });
|
|
651
|
+
sections.forEach((section) => sectionObserver.observe(section));
|
|
652
|
+
}
|
|
653
|
+
|
|
478
654
|
function updateAgentStatus(data) {
|
|
479
655
|
const now = Math.floor(Date.now() / 1000);
|
|
480
656
|
const activeLockAgents = new Set((data.locks || [])
|
|
@@ -537,12 +713,14 @@
|
|
|
537
713
|
epics[epic].total++;
|
|
538
714
|
if (task.checked) epics[epic].done++;
|
|
539
715
|
}
|
|
540
|
-
el.innerHTML = '<div class="chart-list">' + Object.entries(epics).map(([epic, d]) => {
|
|
716
|
+
el.innerHTML = '<div class="chart-list">' + Object.entries(epics).map(([epic, d], index) => {
|
|
541
717
|
const pct = d.total > 0 ? (d.done / d.total) * 100 : 0;
|
|
718
|
+
const share = queue.length > 0 ? (d.total / queue.length) * 100 : 0;
|
|
542
719
|
return '<div class="chart-row">'
|
|
543
|
-
+ '<div class="chart-row-header"><span class="chart-row-label">' + escapeHtml(epic) + '</span>'
|
|
720
|
+
+ '<div class="chart-row-header"><span class="chart-row-label"><span class="chart-row-rank">0' + (index + 1) + '</span>' + escapeHtml(epic) + '</span>'
|
|
544
721
|
+ '<span class="chart-row-val">' + d.done + '/' + d.total + '</span></div>'
|
|
545
722
|
+ '<div class="chart-bar-track"><div class="chart-bar-fill clr-done" style="width:' + pct.toFixed(1) + '%"></div></div>'
|
|
723
|
+
+ '<div class="chart-row-note">' + share.toFixed(0) + '% of tracked work</div>'
|
|
546
724
|
+ '</div>';
|
|
547
725
|
}).join('') + '</div>';
|
|
548
726
|
}
|
|
@@ -577,41 +755,52 @@
|
|
|
577
755
|
}
|
|
578
756
|
const total = Object.values(agents).reduce((a, b) => a + b, 0) || 1;
|
|
579
757
|
const entries = Object.entries(agents).sort((a, b) => b[1] - a[1]);
|
|
758
|
+
const [leadAgent, leadCount] = entries[0];
|
|
759
|
+
const leadShare = ((leadCount / total) * 100).toFixed(0);
|
|
580
760
|
|
|
581
761
|
// SVG pie chart
|
|
582
|
-
const cx = 50, cy = 50,
|
|
762
|
+
const cx = 50, cy = 50, outerR = 46, innerR = 22;
|
|
583
763
|
let angle = -Math.PI / 2;
|
|
584
764
|
const slices = entries.length === 1
|
|
585
765
|
? (() => {
|
|
586
766
|
const [a, n] = entries[0];
|
|
587
767
|
const color = AGENT_COLORS[a] || '#6b7f74';
|
|
588
|
-
return '<circle cx="' + cx + '" cy="' + cy + '" r="' +
|
|
768
|
+
return '<circle cx="' + cx + '" cy="' + cy + '" r="' + outerR + '" fill="none" stroke="' + color + '" stroke-width="' + (outerR - innerR) + '" stroke-linecap="round"><title>@' + escapeHtml(a) + ': ' + n + ' tasks</title></circle>';
|
|
589
769
|
})()
|
|
590
770
|
: entries.map(([a, n]) => {
|
|
591
771
|
const sweep = (n / total) * 2 * Math.PI;
|
|
592
|
-
const x1 = cx +
|
|
593
|
-
const y1 = cy +
|
|
772
|
+
const x1 = cx + outerR * Math.cos(angle);
|
|
773
|
+
const y1 = cy + outerR * Math.sin(angle);
|
|
594
774
|
angle += sweep;
|
|
595
|
-
const x2 = cx +
|
|
596
|
-
const y2 = cy +
|
|
775
|
+
const x2 = cx + outerR * Math.cos(angle);
|
|
776
|
+
const y2 = cy + outerR * Math.sin(angle);
|
|
597
777
|
const large = sweep > Math.PI ? 1 : 0;
|
|
598
778
|
const color = AGENT_COLORS[a] || '#6b7f74';
|
|
599
779
|
return '<path d="M' + cx + ',' + cy + ' L' + x1.toFixed(2) + ',' + y1.toFixed(2)
|
|
600
|
-
+ ' A' +
|
|
601
|
-
+ ' Z" fill="' + color + '"
|
|
780
|
+
+ ' A' + outerR + ',' + outerR + ' 0 ' + large + ',1 ' + x2.toFixed(2) + ',' + y2.toFixed(2)
|
|
781
|
+
+ ' Z" fill="' + color + '" opacity="0.96"><title>@' + escapeHtml(a) + ': ' + n + ' tasks</title></path>';
|
|
602
782
|
}).join('');
|
|
603
783
|
|
|
604
784
|
const legend = entries.map(([a, n]) => {
|
|
605
785
|
const color = AGENT_COLORS[a] || '#6b7f74';
|
|
606
|
-
|
|
786
|
+
const pct = ((n / total) * 100).toFixed(0);
|
|
787
|
+
return '<div class="agent-legend-item"><div class="agent-legend-dot" style="background:' + color + '"></div><span class="agent-legend-name">@' + escapeHtml(a) + '</span><span class="agent-legend-share">' + pct + '%</span><span class="chart-row-val">' + n + '</span></div>';
|
|
607
788
|
}).join('');
|
|
608
789
|
|
|
609
790
|
const agentCount = entries.length;
|
|
610
791
|
el.innerHTML = '<div class="pie-wrap">'
|
|
611
|
-
+ '<svg class="pie-svg" viewBox="0 0 100 100"
|
|
792
|
+
+ '<div class="pie-shell"><svg class="pie-svg" viewBox="0 0 100 100">'
|
|
793
|
+
+ '<defs><filter id="pieGlow" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur stdDeviation="1.5" result="blur"></feGaussianBlur><feMerge><feMergeNode in="blur"></feMergeNode><feMergeNode in="SourceGraphic"></feMergeNode></feMerge></filter></defs>'
|
|
794
|
+
+ '<circle cx="' + cx + '" cy="' + cy + '" r="' + outerR + '" class="pie-bg"></circle>'
|
|
795
|
+
+ '<g filter="url(#pieGlow)">' + slices + '</g>'
|
|
796
|
+
+ '<circle cx="' + cx + '" cy="' + cy + '" r="' + innerR + '" class="pie-core"></circle>'
|
|
797
|
+
+ '<text class="pie-total" x="' + cx + '" y="47">' + total + '</text>'
|
|
798
|
+
+ '<text class="pie-sub" x="' + cx + '" y="60">tasks</text>'
|
|
799
|
+
+ '</svg></div>'
|
|
612
800
|
+ '<div class="agent-legend">' + legend + '</div>'
|
|
613
801
|
+ '</div>'
|
|
614
|
-
+ '<div class="pie-caption"
|
|
802
|
+
+ '<div class="pie-caption"><strong>@' + escapeHtml(leadAgent) + '</strong> is carrying ' + leadShare + '% of tracked work. '
|
|
803
|
+
+ total + ' task' + (total !== 1 ? 's' : '') + ' across ' + agentCount + ' agent' + (agentCount !== 1 ? 's' : '') + '.</div>';
|
|
615
804
|
}
|
|
616
805
|
|
|
617
806
|
function renderIcons() {
|
|
@@ -669,7 +858,9 @@
|
|
|
669
858
|
return String(value).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
670
859
|
}
|
|
671
860
|
renderIcons();
|
|
861
|
+
initSectionObserver();
|
|
672
862
|
syncQueueViewFromHash();
|
|
863
|
+
syncSidebarActive();
|
|
673
864
|
load();
|
|
674
865
|
new EventSource('/events').addEventListener('update', load);
|
|
675
866
|
</script>
|