claude-pager 0.2.2 → 0.3.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/README.md +85 -4
- package/dist/channels/telegram/provider.d.ts.map +1 -1
- package/dist/channels/telegram/provider.js +14 -0
- package/dist/channels/telegram/provider.js.map +1 -1
- package/dist/cli/index.js +45 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/daemon/handlers.d.ts.map +1 -1
- package/dist/daemon/handlers.js +4 -3
- package/dist/daemon/handlers.js.map +1 -1
- package/dist/daemon/server.d.ts.map +1 -1
- package/dist/daemon/server.js +160 -2
- package/dist/daemon/server.js.map +1 -1
- package/dist/dashboard/enricher.d.ts +3 -0
- package/dist/dashboard/enricher.d.ts.map +1 -1
- package/dist/dashboard/enricher.js +11 -6
- package/dist/dashboard/enricher.js.map +1 -1
- package/dist/dashboard/git-status.d.ts +1 -1
- package/dist/dashboard/git-status.d.ts.map +1 -1
- package/dist/dashboard/git-status.js +14 -12
- package/dist/dashboard/git-status.js.map +1 -1
- package/dist/dashboard/html.d.ts +1 -1
- package/dist/dashboard/html.d.ts.map +1 -1
- package/dist/dashboard/html.js +481 -6
- package/dist/dashboard/html.js.map +1 -1
- package/dist/dashboard/routes.d.ts.map +1 -1
- package/dist/dashboard/routes.js +12 -0
- package/dist/dashboard/routes.js.map +1 -1
- package/dist/dashboard/sse.d.ts +5 -0
- package/dist/dashboard/sse.d.ts.map +1 -0
- package/dist/dashboard/sse.js +45 -0
- package/dist/dashboard/sse.js.map +1 -0
- package/dist/hooks/index.js +40 -5
- package/dist/hooks/index.js.map +1 -1
- package/dist/injectors/composite.d.ts +10 -0
- package/dist/injectors/composite.d.ts.map +1 -0
- package/dist/injectors/composite.js +27 -0
- package/dist/injectors/composite.js.map +1 -0
- package/dist/injectors/factory.d.ts.map +1 -1
- package/dist/injectors/factory.js +8 -5
- package/dist/injectors/factory.js.map +1 -1
- package/dist/injectors/vscode/injector.d.ts +8 -0
- package/dist/injectors/vscode/injector.d.ts.map +1 -0
- package/dist/injectors/vscode/injector.js +38 -0
- package/dist/injectors/vscode/injector.js.map +1 -0
- package/dist/notes/__tests__/store.test.d.ts +2 -0
- package/dist/notes/__tests__/store.test.d.ts.map +1 -0
- package/dist/notes/__tests__/store.test.js +88 -0
- package/dist/notes/__tests__/store.test.js.map +1 -0
- package/dist/notes/store.d.ts +27 -0
- package/dist/notes/store.d.ts.map +1 -0
- package/dist/notes/store.js +174 -0
- package/dist/notes/store.js.map +1 -0
- package/dist/sessions/helpers.d.ts +4 -0
- package/dist/sessions/helpers.d.ts.map +1 -0
- package/dist/sessions/helpers.js +8 -0
- package/dist/sessions/helpers.js.map +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/voice/transcribe.js +1 -1
- package/dist/voice/transcribe.js.map +1 -1
- package/package.json +1 -1
package/dist/dashboard/html.js
CHANGED
|
@@ -438,6 +438,152 @@ exports.DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
438
438
|
margin-left: auto;
|
|
439
439
|
}
|
|
440
440
|
|
|
441
|
+
.notes-panel {
|
|
442
|
+
background: #1a1e2e;
|
|
443
|
+
border: 1px solid #2d333b;
|
|
444
|
+
border-radius: 8px;
|
|
445
|
+
padding: 12px;
|
|
446
|
+
min-width: 0;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
.notes-header {
|
|
450
|
+
display: flex;
|
|
451
|
+
align-items: center;
|
|
452
|
+
justify-content: space-between;
|
|
453
|
+
margin-bottom: 8px;
|
|
454
|
+
font-size: 12px;
|
|
455
|
+
font-weight: 600;
|
|
456
|
+
color: #8b949e;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
.notes-header .count {
|
|
460
|
+
background: #2d333b;
|
|
461
|
+
color: #c9d1d9;
|
|
462
|
+
font-size: 10px;
|
|
463
|
+
padding: 1px 7px;
|
|
464
|
+
border-radius: 8px;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
.note-item {
|
|
468
|
+
display: flex;
|
|
469
|
+
align-items: center;
|
|
470
|
+
gap: 8px;
|
|
471
|
+
padding: 6px 8px;
|
|
472
|
+
border-radius: 4px;
|
|
473
|
+
font-size: 12px;
|
|
474
|
+
color: #c9d1d9;
|
|
475
|
+
transition: background 0.15s;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
.note-item:hover {
|
|
479
|
+
background: #21262d;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
.note-item .note-text {
|
|
483
|
+
flex: 1;
|
|
484
|
+
overflow: hidden;
|
|
485
|
+
text-overflow: ellipsis;
|
|
486
|
+
white-space: nowrap;
|
|
487
|
+
cursor: text;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
.note-clock {
|
|
491
|
+
font-size: 11px;
|
|
492
|
+
cursor: default;
|
|
493
|
+
flex-shrink: 0;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.note-thumb {
|
|
497
|
+
width: 32px;
|
|
498
|
+
height: 32px;
|
|
499
|
+
object-fit: cover;
|
|
500
|
+
border-radius: 4px;
|
|
501
|
+
cursor: pointer;
|
|
502
|
+
flex-shrink: 0;
|
|
503
|
+
border: 1px solid #30363d;
|
|
504
|
+
transition: transform 0.15s;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
.note-thumb:hover {
|
|
508
|
+
transform: scale(1.1);
|
|
509
|
+
border-color: #58a6ff;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
.note-lightbox {
|
|
513
|
+
position: fixed;
|
|
514
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
515
|
+
background: rgba(0,0,0,0.85);
|
|
516
|
+
display: flex;
|
|
517
|
+
align-items: center;
|
|
518
|
+
justify-content: center;
|
|
519
|
+
z-index: 1000;
|
|
520
|
+
cursor: pointer;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
.note-lightbox img {
|
|
524
|
+
max-width: 90vw;
|
|
525
|
+
max-height: 90vh;
|
|
526
|
+
border-radius: 8px;
|
|
527
|
+
box-shadow: 0 0 40px rgba(0,0,0,0.5);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
.note-grip {
|
|
531
|
+
cursor: grab;
|
|
532
|
+
color: #484f58;
|
|
533
|
+
font-size: 10px;
|
|
534
|
+
user-select: none;
|
|
535
|
+
}
|
|
536
|
+
.note-item.dragging {
|
|
537
|
+
opacity: 0.4;
|
|
538
|
+
}
|
|
539
|
+
.note-item.drag-over {
|
|
540
|
+
border-top: 2px solid #58a6ff;
|
|
541
|
+
margin-top: -2px;
|
|
542
|
+
}
|
|
543
|
+
.note-btn.move {
|
|
544
|
+
background: none;
|
|
545
|
+
color: #484f58;
|
|
546
|
+
padding: 0 2px;
|
|
547
|
+
font-size: 8px;
|
|
548
|
+
min-width: 16px;
|
|
549
|
+
}
|
|
550
|
+
.note-btn.move:hover {
|
|
551
|
+
color: #58a6ff;
|
|
552
|
+
}
|
|
553
|
+
.note-btn {
|
|
554
|
+
font-family: 'JetBrains Mono', monospace;
|
|
555
|
+
font-size: 10px;
|
|
556
|
+
font-weight: 600;
|
|
557
|
+
padding: 2px 8px;
|
|
558
|
+
border-radius: 4px;
|
|
559
|
+
border: none;
|
|
560
|
+
cursor: pointer;
|
|
561
|
+
transition: opacity 0.2s;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
.note-btn:hover { opacity: 0.85; }
|
|
565
|
+
|
|
566
|
+
.note-btn.send {
|
|
567
|
+
background: #238636;
|
|
568
|
+
color: #fff;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
.note-btn.delete {
|
|
572
|
+
background: #21262d;
|
|
573
|
+
color: #8b949e;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.note-btn.delete:hover {
|
|
577
|
+
background: #da3633;
|
|
578
|
+
color: #fff;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
.note-add-row {
|
|
582
|
+
display: flex;
|
|
583
|
+
gap: 6px;
|
|
584
|
+
margin-top: 8px;
|
|
585
|
+
}
|
|
586
|
+
|
|
441
587
|
.empty {
|
|
442
588
|
text-align: center;
|
|
443
589
|
padding: 60px 20px;
|
|
@@ -547,6 +693,7 @@ exports.DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
547
693
|
|
|
548
694
|
<script>
|
|
549
695
|
let data = null;
|
|
696
|
+
const sentMessages = new Map(); // sessionId → { icon, text, at }
|
|
550
697
|
|
|
551
698
|
function getPinnedOrder() {
|
|
552
699
|
try { return JSON.parse(localStorage.getItem('dashboard-pin-order') || '[]'); }
|
|
@@ -599,6 +746,32 @@ exports.DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
599
746
|
return Math.floor(s / 86400) + 'd ago';
|
|
600
747
|
}
|
|
601
748
|
|
|
749
|
+
function noteAgeColor(epochMs) {
|
|
750
|
+
const m = (Date.now() - epochMs) / 60000;
|
|
751
|
+
if (m < 10) return '#3fb950'; // green — fresh
|
|
752
|
+
if (m < 60) return '#d29922'; // yellow — aging
|
|
753
|
+
if (m < 360) return '#f0883e'; // orange — old
|
|
754
|
+
return '#f85149'; // red — stale
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function formatToolInput(input) {
|
|
758
|
+
if (!input) return '';
|
|
759
|
+
const hasDiff = input.includes('--- old') && input.includes('+++ new');
|
|
760
|
+
if (hasDiff) {
|
|
761
|
+
const lines = input.slice(0, 800).split('\\n');
|
|
762
|
+
const html = lines.map(line => {
|
|
763
|
+
const escaped = escapeHtml(line);
|
|
764
|
+
if (line.startsWith('+++ new')) return '<span style="color:#3fb950;font-weight:600">' + escaped + '</span>';
|
|
765
|
+
if (line.startsWith('--- old')) return '<span style="color:#f85149;font-weight:600">' + escaped + '</span>';
|
|
766
|
+
if (line.startsWith('+')) return '<span style="color:#3fb950">' + escaped + '</span>';
|
|
767
|
+
if (line.startsWith('-')) return '<span style="color:#f85149">' + escaped + '</span>';
|
|
768
|
+
return escaped;
|
|
769
|
+
}).join('\\n');
|
|
770
|
+
return '<pre style="font-size:10px;color:#8b949e;word-break:break-all;white-space:pre-wrap;margin:4px 0;max-height:200px;overflow-y:auto">' + html + '</pre>';
|
|
771
|
+
}
|
|
772
|
+
return '<code style="font-size:10px;color:#8b949e;word-break:break-all">' + escapeHtml(input.slice(0, 400)) + '</code>';
|
|
773
|
+
}
|
|
774
|
+
|
|
602
775
|
function stateLabel(state) {
|
|
603
776
|
const labels = {
|
|
604
777
|
working: 'Working',
|
|
@@ -621,9 +794,12 @@ exports.DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
621
794
|
if (s.pendingQuestion) {
|
|
622
795
|
const q = s.pendingQuestion;
|
|
623
796
|
const isPermission = q.type === 'permission_prompt';
|
|
797
|
+
const contextInfo = q.context
|
|
798
|
+
? '<div style="font-size:11px;color:#c9d1d9;margin-bottom:6px;white-space:pre-wrap">' + escapeHtml(q.context.slice(-300)) + '</div>'
|
|
799
|
+
: '';
|
|
624
800
|
const toolInfo = q.toolName
|
|
625
|
-
? '<span class="tool">' + escapeHtml(q.toolName) + '</span>' +
|
|
626
|
-
(q.toolInput ? '<br
|
|
801
|
+
? contextInfo + '<span class="tool">' + escapeHtml(q.toolName) + '</span>' +
|
|
802
|
+
(q.toolInput ? '<br>' + formatToolInput(q.toolInput) : '')
|
|
627
803
|
: escapeHtml(q.message.slice(0, 150));
|
|
628
804
|
|
|
629
805
|
const actions = isPermission
|
|
@@ -682,6 +858,16 @@ exports.DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
682
858
|
<button class="dismiss-btn" onclick="dismissSession('\${s.sessionId}')" title="Dismiss session">🗑</button>
|
|
683
859
|
</div>
|
|
684
860
|
\${expandBtn}
|
|
861
|
+
\${(() => {
|
|
862
|
+
const sent = sentMessages.get(s.sessionId);
|
|
863
|
+
if (sent && Date.now() - sent.at < 300_000) {
|
|
864
|
+
const label = sent.icon === '▶' ? 'Working on' : 'Replied';
|
|
865
|
+
return '<div style="background:#0d2818;border:1px solid #238636;border-radius:6px;padding:6px 10px;margin:8px 0;font-size:12px;color:#c9d1d9;white-space:pre-wrap">'
|
|
866
|
+
+ '<span style="color:#3fb950;font-weight:600">' + label + ' :</span> '
|
|
867
|
+
+ escapeHtml(sent.text.length > 150 ? sent.text.slice(0, 150) + '...' : sent.text) + '</div>';
|
|
868
|
+
}
|
|
869
|
+
return '';
|
|
870
|
+
})()}
|
|
685
871
|
\${pending}
|
|
686
872
|
\${idleInput}
|
|
687
873
|
<div class="card-footer">
|
|
@@ -712,6 +898,52 @@ exports.DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
712
898
|
return main + staging;
|
|
713
899
|
}
|
|
714
900
|
|
|
901
|
+
function renderNotes(project, notes) {
|
|
902
|
+
if (!notes || notes.length === 0) {
|
|
903
|
+
return \`
|
|
904
|
+
<div class="notes-panel">
|
|
905
|
+
<div class="notes-header">Notes</div>
|
|
906
|
+
<div class="note-add-row">
|
|
907
|
+
<input type="text" class="reply-input" id="note-add-\${escapeHtml(project)}" placeholder="Add a note..." onkeydown="if(event.key==='Enter')addNote('\${escapeHtml(project)}',this.value,this)" onpaste="handleNotePaste(event,'\${escapeHtml(project)}')">
|
|
908
|
+
<button class="note-btn send" onclick="var i=document.getElementById('note-add-\${escapeHtml(project)}');addNote('\${escapeHtml(project)}',i.value,i)">+</button>
|
|
909
|
+
</div>
|
|
910
|
+
</div>
|
|
911
|
+
\`;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
const items = notes.map((n, idx) => {
|
|
915
|
+
const thumb = n.image
|
|
916
|
+
? '<img src="/api/v1/notes/images/' + escapeHtml(n.image) + '" class="note-thumb" onclick="openNoteImage(this.src)" title="Click to enlarge">'
|
|
917
|
+
: '';
|
|
918
|
+
return \`
|
|
919
|
+
<div class="note-item" draggable="true" data-note-id="\${n.id}" data-project="\${escapeHtml(project)}"
|
|
920
|
+
ondragstart="onNoteDragStart(event)" ondragover="onNoteDragOver(event)" ondrop="onNoteDrop(event)" ondragend="onNoteDragEnd(event)">
|
|
921
|
+
<span class="note-grip" title="Drag to reorder">⠿</span>
|
|
922
|
+
\${thumb}
|
|
923
|
+
<span class="note-text" title="\${escapeHtml(n.text)}" onclick="editNote(this,'\${n.id}')">\${escapeHtml(n.text)}</span>
|
|
924
|
+
<span class="note-clock" title="\${timeAgo(n.createdAt)}" style="color:\${noteAgeColor(n.createdAt)}">⏱</span>
|
|
925
|
+
\${idx > 0 ? '<button class="note-btn move" onclick="moveNote(\\'' + escapeHtml(project) + '\\',' + idx + ',-1)" title="Move up">▲</button>' : '<span class="note-btn move" style="visibility:hidden">▲</span>'}
|
|
926
|
+
\${idx < notes.length - 1 ? '<button class="note-btn move" onclick="moveNote(\\'' + escapeHtml(project) + '\\',' + idx + ',1)" title="Move down">▼</button>' : '<span class="note-btn move" style="visibility:hidden">▼</span>'}
|
|
927
|
+
<button class="note-btn send" onclick="sendNote('\${n.id}',this)" title="Send to session">▶</button>
|
|
928
|
+
<button class="note-btn delete" onclick="deleteNote('\${n.id}')" title="Delete">✕</button>
|
|
929
|
+
</div>
|
|
930
|
+
\`}).join('');
|
|
931
|
+
|
|
932
|
+
return \`
|
|
933
|
+
<div class="notes-panel">
|
|
934
|
+
<div class="notes-header">
|
|
935
|
+
<span>Notes</span>
|
|
936
|
+
<span class="count">\${notes.length}</span>
|
|
937
|
+
</div>
|
|
938
|
+
\${items}
|
|
939
|
+
<div class="note-add-row">
|
|
940
|
+
<input type="text" class="reply-input" id="note-add-\${escapeHtml(project)}" placeholder="Add a note..." onkeydown="if(event.key==='Enter')addNote('\${escapeHtml(project)}',this.value,this)" onpaste="handleNotePaste(event,'\${escapeHtml(project)}')">
|
|
941
|
+
<button class="note-btn send" onclick="var i=document.getElementById('note-add-\${escapeHtml(project)}');addNote('\${escapeHtml(project)}',i.value,i)">+</button>
|
|
942
|
+
</div>
|
|
943
|
+
</div>
|
|
944
|
+
\`;
|
|
945
|
+
}
|
|
946
|
+
|
|
715
947
|
function renderProject(p) {
|
|
716
948
|
const isPinned = getPinnedOrder().includes(p.name);
|
|
717
949
|
const anyNeedsTesting = p.sessions.some(s => s.needsTesting);
|
|
@@ -730,6 +962,7 @@ exports.DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
730
962
|
\${infoRow}
|
|
731
963
|
<div class="sessions">
|
|
732
964
|
\${p.sessions.map(renderSession).join('')}
|
|
965
|
+
\${renderNotes(p.name, p.notes)}
|
|
733
966
|
</div>
|
|
734
967
|
</div>
|
|
735
968
|
\`;
|
|
@@ -767,10 +1000,17 @@ exports.DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
767
1000
|
allowAllBtn.style.display = 'none';
|
|
768
1001
|
}
|
|
769
1002
|
|
|
770
|
-
// Skip DOM update if user is
|
|
771
|
-
|
|
1003
|
+
// Skip DOM update if user is focused on an input field
|
|
1004
|
+
const focused = document.activeElement;
|
|
1005
|
+
if (focused && focused.tagName === 'INPUT' && focused.classList.contains('reply-input')) return;
|
|
772
1006
|
|
|
773
|
-
// Preserve
|
|
1007
|
+
// Preserve input values and cursor position across re-renders
|
|
1008
|
+
const savedInputs = new Map();
|
|
1009
|
+
const focusedId = focused?.id;
|
|
1010
|
+
const cursorPos = focused?.selectionStart;
|
|
1011
|
+
document.querySelectorAll('.reply-input').forEach(inp => {
|
|
1012
|
+
if (inp.id && inp.value.trim()) savedInputs.set(inp.id, inp.value);
|
|
1013
|
+
});
|
|
774
1014
|
const expandedTitles = new Set();
|
|
775
1015
|
document.querySelectorAll('.card-title.expanded').forEach(el => expandedTitles.add(el.id));
|
|
776
1016
|
|
|
@@ -785,10 +1025,32 @@ exports.DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
785
1025
|
if (btn) btn.textContent = '▲';
|
|
786
1026
|
}
|
|
787
1027
|
});
|
|
1028
|
+
|
|
1029
|
+
// Restore input values and focus
|
|
1030
|
+
savedInputs.forEach((val, id) => {
|
|
1031
|
+
const inp = document.getElementById(id);
|
|
1032
|
+
if (inp) inp.value = val;
|
|
1033
|
+
});
|
|
1034
|
+
if (focusedId) {
|
|
1035
|
+
const el = document.getElementById(focusedId);
|
|
1036
|
+
if (el) { el.focus(); if (cursorPos != null) el.selectionStart = el.selectionEnd = cursorPos; }
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function showSentBanner(card, icon, text, sessionId) {
|
|
1041
|
+
if (!card) return;
|
|
1042
|
+
// Track for persistent rendering across re-renders
|
|
1043
|
+
const sid = sessionId || card.querySelector('.card-title')?.id?.replace('title-', '');
|
|
1044
|
+
if (sid) {
|
|
1045
|
+
// Find full sessionId from data
|
|
1046
|
+
const fullId = data?.projects?.flatMap(p => p.sessions).find(s => s.sessionId.startsWith(sid))?.sessionId || sid;
|
|
1047
|
+
sentMessages.set(fullId, { icon, text, at: Date.now() });
|
|
1048
|
+
}
|
|
788
1049
|
}
|
|
789
1050
|
|
|
790
1051
|
async function respondTo(eventId, response, btn) {
|
|
791
1052
|
if (btn) btn.disabled = true;
|
|
1053
|
+
const card = btn?.closest('.card');
|
|
792
1054
|
try {
|
|
793
1055
|
const res = await fetch('/api/v1/respond-to', {
|
|
794
1056
|
method: 'POST',
|
|
@@ -796,6 +1058,8 @@ exports.DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
796
1058
|
body: JSON.stringify({ eventId, response }),
|
|
797
1059
|
});
|
|
798
1060
|
if (res.ok) {
|
|
1061
|
+
const label = response === 'allow' ? '✓ Allowed' : response === 'deny' ? '✗ Denied' : response;
|
|
1062
|
+
showSentBanner(card, '↩', label);
|
|
799
1063
|
fetchDashboard();
|
|
800
1064
|
} else {
|
|
801
1065
|
const err = await res.json();
|
|
@@ -823,6 +1087,9 @@ exports.DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
823
1087
|
async function sendToSession(sessionId, text, btn) {
|
|
824
1088
|
if (!text || !text.trim()) return;
|
|
825
1089
|
if (btn) btn.disabled = true;
|
|
1090
|
+
const card = btn?.closest('.card');
|
|
1091
|
+
const input = card?.querySelector('input');
|
|
1092
|
+
if (input) { input.value = ''; input.blur(); }
|
|
826
1093
|
try {
|
|
827
1094
|
const res = await fetch('/api/v1/send-to', {
|
|
828
1095
|
method: 'POST',
|
|
@@ -830,6 +1097,7 @@ exports.DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
830
1097
|
body: JSON.stringify({ sessionId, text: text.trim() }),
|
|
831
1098
|
});
|
|
832
1099
|
if (res.ok) {
|
|
1100
|
+
showSentBanner(card, '▶', text.trim());
|
|
833
1101
|
fetchDashboard();
|
|
834
1102
|
} else {
|
|
835
1103
|
const err = await res.json();
|
|
@@ -841,6 +1109,196 @@ exports.DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
841
1109
|
if (btn) btn.disabled = false;
|
|
842
1110
|
}
|
|
843
1111
|
|
|
1112
|
+
let addingNote = false;
|
|
1113
|
+
async function addNote(project, text, input) {
|
|
1114
|
+
if (!text || !text.trim() || addingNote) return;
|
|
1115
|
+
addingNote = true;
|
|
1116
|
+
if (input) { input.value = ''; input.blur(); }
|
|
1117
|
+
try {
|
|
1118
|
+
await fetch('/api/v1/notes', {
|
|
1119
|
+
method: 'POST',
|
|
1120
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1121
|
+
body: JSON.stringify({ project, text: text.trim(), source: 'dashboard' }),
|
|
1122
|
+
});
|
|
1123
|
+
fetchDashboard();
|
|
1124
|
+
} catch (e) {
|
|
1125
|
+
console.error('add-note error:', e);
|
|
1126
|
+
}
|
|
1127
|
+
addingNote = false;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
async function handleNotePaste(event, project) {
|
|
1131
|
+
const items = event.clipboardData?.items;
|
|
1132
|
+
if (!items) return;
|
|
1133
|
+
for (const item of items) {
|
|
1134
|
+
if (item.type.startsWith('image/')) {
|
|
1135
|
+
event.preventDefault();
|
|
1136
|
+
const blob = item.getAsFile();
|
|
1137
|
+
if (!blob) return;
|
|
1138
|
+
const reader = new FileReader();
|
|
1139
|
+
reader.onload = async () => {
|
|
1140
|
+
const base64 = reader.result.split(',')[1];
|
|
1141
|
+
const text = event.target.value?.trim() || '';
|
|
1142
|
+
try {
|
|
1143
|
+
await fetch('/api/v1/notes/with-image', {
|
|
1144
|
+
method: 'POST',
|
|
1145
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1146
|
+
body: JSON.stringify({ project, text: text || '(image)', imageBase64: base64, source: 'dashboard' }),
|
|
1147
|
+
});
|
|
1148
|
+
event.target.value = '';
|
|
1149
|
+
fetchDashboard();
|
|
1150
|
+
} catch (e) {
|
|
1151
|
+
console.error('paste-image error:', e);
|
|
1152
|
+
}
|
|
1153
|
+
};
|
|
1154
|
+
reader.readAsDataURL(blob);
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
function openNoteImage(src) {
|
|
1161
|
+
const lb = document.createElement('div');
|
|
1162
|
+
lb.className = 'note-lightbox';
|
|
1163
|
+
const img = document.createElement('img');
|
|
1164
|
+
img.src = src;
|
|
1165
|
+
lb.appendChild(img);
|
|
1166
|
+
lb.onclick = () => lb.remove();
|
|
1167
|
+
document.body.appendChild(lb);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
function editNote(span, noteId) {
|
|
1171
|
+
const input = document.createElement('input');
|
|
1172
|
+
input.type = 'text';
|
|
1173
|
+
input.className = 'reply-input';
|
|
1174
|
+
input.value = span.textContent;
|
|
1175
|
+
input.style.cssText = 'flex:1;font-size:12px;';
|
|
1176
|
+
span.replaceWith(input);
|
|
1177
|
+
input.focus();
|
|
1178
|
+
input.select();
|
|
1179
|
+
async function save() {
|
|
1180
|
+
const text = input.value.trim();
|
|
1181
|
+
if (text && text !== span.textContent) {
|
|
1182
|
+
await fetch('/api/v1/notes/' + noteId, {
|
|
1183
|
+
method: 'PATCH',
|
|
1184
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1185
|
+
body: JSON.stringify({ text }),
|
|
1186
|
+
});
|
|
1187
|
+
span.textContent = text;
|
|
1188
|
+
span.title = text;
|
|
1189
|
+
}
|
|
1190
|
+
input.replaceWith(span);
|
|
1191
|
+
}
|
|
1192
|
+
input.onblur = save;
|
|
1193
|
+
input.onkeydown = (e) => { if (e.key === 'Enter') { e.preventDefault(); save(); } if (e.key === 'Escape') input.replaceWith(span); };
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
async function deleteNote(noteId) {
|
|
1197
|
+
try {
|
|
1198
|
+
await fetch('/api/v1/notes/' + noteId, { method: 'DELETE' });
|
|
1199
|
+
fetchDashboard();
|
|
1200
|
+
} catch (e) {
|
|
1201
|
+
console.error('delete-note error:', e);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
async function sendNote(noteId, btn) {
|
|
1206
|
+
if (btn) btn.disabled = true;
|
|
1207
|
+
const noteText = btn?.closest('.note-item')?.querySelector('.note-text')?.textContent || '';
|
|
1208
|
+
try {
|
|
1209
|
+
const res = await fetch('/api/v1/notes/' + noteId + '/send', { method: 'POST' });
|
|
1210
|
+
const result = await res.json();
|
|
1211
|
+
if (!res.ok) {
|
|
1212
|
+
showToast(btn, result.error || 'Failed to send', true);
|
|
1213
|
+
} else {
|
|
1214
|
+
if (result.sessionId) {
|
|
1215
|
+
const card = document.querySelector('#title-' + result.sessionId.slice(0, 8))?.closest('.card');
|
|
1216
|
+
showSentBanner(card, '▶', noteText);
|
|
1217
|
+
}
|
|
1218
|
+
showToast(btn, 'Sent', false);
|
|
1219
|
+
}
|
|
1220
|
+
fetchDashboard();
|
|
1221
|
+
} catch (e) {
|
|
1222
|
+
console.error('send-note error:', e);
|
|
1223
|
+
}
|
|
1224
|
+
if (btn) btn.disabled = false;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
function getProjectNoteIds(project) {
|
|
1228
|
+
return Array.from(document.querySelectorAll('.note-item[data-project="' + project + '"]'))
|
|
1229
|
+
.map(el => el.dataset.noteId);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
async function saveNoteOrder(project) {
|
|
1233
|
+
const orderedIds = getProjectNoteIds(project);
|
|
1234
|
+
try {
|
|
1235
|
+
await fetch('/api/v1/notes/reorder', {
|
|
1236
|
+
method: 'PATCH',
|
|
1237
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1238
|
+
body: JSON.stringify({ project, orderedIds }),
|
|
1239
|
+
});
|
|
1240
|
+
} catch (e) {
|
|
1241
|
+
console.error('reorder error:', e);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
function moveNote(project, idx, direction) {
|
|
1246
|
+
const items = document.querySelectorAll('.note-item[data-project="' + project + '"]');
|
|
1247
|
+
const target = idx + direction;
|
|
1248
|
+
if (target < 0 || target >= items.length) return;
|
|
1249
|
+
const parent = items[0].parentNode;
|
|
1250
|
+
if (direction === -1) parent.insertBefore(items[idx], items[target]);
|
|
1251
|
+
else parent.insertBefore(items[target], items[idx]);
|
|
1252
|
+
saveNoteOrder(project);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
let draggedNote = null;
|
|
1256
|
+
function onNoteDragStart(e) {
|
|
1257
|
+
draggedNote = e.currentTarget;
|
|
1258
|
+
draggedNote.classList.add('dragging');
|
|
1259
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
1260
|
+
}
|
|
1261
|
+
function onNoteDragOver(e) {
|
|
1262
|
+
e.preventDefault();
|
|
1263
|
+
const item = e.currentTarget;
|
|
1264
|
+
if (item !== draggedNote) item.classList.add('drag-over');
|
|
1265
|
+
}
|
|
1266
|
+
function onNoteDrop(e) {
|
|
1267
|
+
e.preventDefault();
|
|
1268
|
+
const target = e.currentTarget;
|
|
1269
|
+
target.classList.remove('drag-over');
|
|
1270
|
+
if (!draggedNote || target === draggedNote) return;
|
|
1271
|
+
const parent = target.parentNode;
|
|
1272
|
+
const items = Array.from(parent.querySelectorAll('.note-item'));
|
|
1273
|
+
const fromIdx = items.indexOf(draggedNote);
|
|
1274
|
+
const toIdx = items.indexOf(target);
|
|
1275
|
+
if (fromIdx < toIdx) parent.insertBefore(draggedNote, target.nextSibling);
|
|
1276
|
+
else parent.insertBefore(draggedNote, target);
|
|
1277
|
+
saveNoteOrder(draggedNote.dataset.project);
|
|
1278
|
+
}
|
|
1279
|
+
function onNoteDragEnd(e) {
|
|
1280
|
+
document.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
|
|
1281
|
+
if (draggedNote) draggedNote.classList.remove('dragging');
|
|
1282
|
+
draggedNote = null;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
function showToast(anchor, text, isError) {
|
|
1286
|
+
const toast = document.createElement('span');
|
|
1287
|
+
toast.textContent = (isError ? '✗ ' : '✓ ') + text;
|
|
1288
|
+
toast.style.cssText = 'position:absolute;padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600;z-index:10;pointer-events:none;'
|
|
1289
|
+
+ (isError ? 'background:#490202;color:#f85149;' : 'background:#0d2818;color:#3fb950;');
|
|
1290
|
+
if (anchor) {
|
|
1291
|
+
const rect = anchor.getBoundingClientRect();
|
|
1292
|
+
toast.style.left = rect.left + 'px';
|
|
1293
|
+
toast.style.top = (rect.top - 24) + 'px';
|
|
1294
|
+
} else {
|
|
1295
|
+
toast.style.right = '24px';
|
|
1296
|
+
toast.style.top = '70px';
|
|
1297
|
+
}
|
|
1298
|
+
document.body.appendChild(toast);
|
|
1299
|
+
setTimeout(() => toast.remove(), 2000);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
844
1302
|
async function allowAll() {
|
|
845
1303
|
if (!data) return;
|
|
846
1304
|
const pending = [];
|
|
@@ -871,7 +1329,24 @@ exports.DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
871
1329
|
}
|
|
872
1330
|
|
|
873
1331
|
fetchDashboard();
|
|
874
|
-
|
|
1332
|
+
|
|
1333
|
+
// SSE for instant push — polling as fallback
|
|
1334
|
+
let fallbackTimer = setInterval(fetchDashboard, 10000);
|
|
1335
|
+
function connectSSE() {
|
|
1336
|
+
const es = new EventSource('/api/v1/sse');
|
|
1337
|
+
es.addEventListener('refresh', () => fetchDashboard());
|
|
1338
|
+
es.onopen = () => {
|
|
1339
|
+
clearInterval(fallbackTimer);
|
|
1340
|
+
fallbackTimer = setInterval(fetchDashboard, 10000);
|
|
1341
|
+
};
|
|
1342
|
+
es.onerror = () => {
|
|
1343
|
+
es.close();
|
|
1344
|
+
clearInterval(fallbackTimer);
|
|
1345
|
+
fallbackTimer = setInterval(fetchDashboard, 2000);
|
|
1346
|
+
setTimeout(connectSSE, 3000);
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
connectSSE();
|
|
875
1350
|
</script>
|
|
876
1351
|
</body>
|
|
877
1352
|
</html>`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"html.js","sourceRoot":"","sources":["../../src/dashboard/html.ts"],"names":[],"mappings":";;;AAAa,QAAA,cAAc,GAAG
|
|
1
|
+
{"version":3,"file":"html.js","sourceRoot":"","sources":["../../src/dashboard/html.ts"],"names":[],"mappings":";;;AAAa,QAAA,cAAc,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAo0CtB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/dashboard/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/dashboard/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAK/C,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,eAAe,GAAG,IAAI,CAoBlE"}
|
package/dist/dashboard/routes.js
CHANGED
|
@@ -3,10 +3,22 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.registerDashboardRoutes = registerDashboardRoutes;
|
|
4
4
|
const enricher_js_1 = require("./enricher.js");
|
|
5
5
|
const html_js_1 = require("./html.js");
|
|
6
|
+
const sse_js_1 = require("./sse.js");
|
|
6
7
|
function registerDashboardRoutes(app) {
|
|
7
8
|
app.get('/api/v1/dashboard', async () => {
|
|
8
9
|
return (0, enricher_js_1.getDashboardData)();
|
|
9
10
|
});
|
|
11
|
+
app.get('/api/v1/sse', async (request, reply) => {
|
|
12
|
+
reply.hijack();
|
|
13
|
+
request.raw.setTimeout(0);
|
|
14
|
+
reply.raw.writeHead(200, {
|
|
15
|
+
'Content-Type': 'text/event-stream',
|
|
16
|
+
'Cache-Control': 'no-cache',
|
|
17
|
+
Connection: 'keep-alive',
|
|
18
|
+
});
|
|
19
|
+
reply.raw.write(':\n\n');
|
|
20
|
+
(0, sse_js_1.addSSEClient)(reply);
|
|
21
|
+
});
|
|
10
22
|
app.get('/dashboard', async (_request, reply) => {
|
|
11
23
|
reply.type('text/html').send(html_js_1.DASHBOARD_HTML);
|
|
12
24
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../../src/dashboard/routes.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../../src/dashboard/routes.ts"],"names":[],"mappings":";;AAKA,0DAoBC;AAxBD,+CAAiD;AACjD,uCAA2C;AAC3C,qCAAwC;AAExC,SAAgB,uBAAuB,CAAC,GAAoB;IAC1D,GAAG,CAAC,GAAG,CAAC,mBAAmB,EAAE,KAAK,IAAI,EAAE;QACtC,OAAO,IAAA,8BAAgB,GAAE,CAAC;IAC5B,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QAC9C,KAAK,CAAC,MAAM,EAAE,CAAC;QACf,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QAC1B,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE;YACvB,cAAc,EAAE,mBAAmB;YACnC,eAAe,EAAE,UAAU;YAC3B,UAAU,EAAE,YAAY;SACzB,CAAC,CAAC;QACH,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACzB,IAAA,qBAAY,EAAC,KAAK,CAAC,CAAC;IACtB,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,GAAG,CAAC,YAAY,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE;QAC9C,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,wBAAc,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse.d.ts","sourceRoot":"","sources":["../../src/dashboard/sse.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAsB5C,wBAAgB,YAAY,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAItD;AAED,wBAAgB,YAAY,CAAC,SAAS,SAAY,GAAG,IAAI,CASxD;AAED,wBAAgB,cAAc,IAAI,MAAM,CAEvC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.addSSEClient = addSSEClient;
|
|
4
|
+
exports.broadcastSSE = broadcastSSE;
|
|
5
|
+
exports.sseClientCount = sseClientCount;
|
|
6
|
+
const clients = new Set();
|
|
7
|
+
let heartbeatTimer = null;
|
|
8
|
+
function startHeartbeat() {
|
|
9
|
+
if (heartbeatTimer)
|
|
10
|
+
return;
|
|
11
|
+
heartbeatTimer = setInterval(() => {
|
|
12
|
+
for (const client of clients) {
|
|
13
|
+
try {
|
|
14
|
+
client.raw.write(':\n\n');
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
clients.delete(client);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (clients.size === 0 && heartbeatTimer) {
|
|
21
|
+
clearInterval(heartbeatTimer);
|
|
22
|
+
heartbeatTimer = null;
|
|
23
|
+
}
|
|
24
|
+
}, 15_000);
|
|
25
|
+
}
|
|
26
|
+
function addSSEClient(reply) {
|
|
27
|
+
clients.add(reply);
|
|
28
|
+
reply.raw.on('close', () => clients.delete(reply));
|
|
29
|
+
startHeartbeat();
|
|
30
|
+
}
|
|
31
|
+
function broadcastSSE(eventType = 'refresh') {
|
|
32
|
+
const payload = `event: ${eventType}\ndata: ${Date.now()}\n\n`;
|
|
33
|
+
for (const client of clients) {
|
|
34
|
+
try {
|
|
35
|
+
client.raw.write(payload);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
clients.delete(client);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function sseClientCount() {
|
|
43
|
+
return clients.size;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=sse.js.map
|