@seedvault/server 0.1.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.html +423 -3
- package/dist/server.js +231 -3
- package/package.json +1 -1
package/dist/index.html
CHANGED
|
@@ -394,6 +394,242 @@
|
|
|
394
394
|
font-size: 11px;
|
|
395
395
|
opacity: 0.6;
|
|
396
396
|
flex-shrink: 0;
|
|
397
|
+
display: flex;
|
|
398
|
+
align-items: center;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
#status-text {
|
|
402
|
+
flex: 1;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
#activity-btn {
|
|
406
|
+
border: none;
|
|
407
|
+
font-size: 11px;
|
|
408
|
+
padding: 0 4px;
|
|
409
|
+
opacity: 0.6;
|
|
410
|
+
display: flex;
|
|
411
|
+
align-items: center;
|
|
412
|
+
gap: 3px;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
#activity-btn:hover {
|
|
416
|
+
opacity: 1;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
#activity-modal {
|
|
420
|
+
display: none;
|
|
421
|
+
position: fixed;
|
|
422
|
+
inset: 0;
|
|
423
|
+
z-index: 300;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
#activity-modal.open {
|
|
427
|
+
display: flex;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
#activity-overlay {
|
|
431
|
+
position: absolute;
|
|
432
|
+
inset: 0;
|
|
433
|
+
background: rgba(0, 0, 0, 0.4);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
#activity-dialog {
|
|
437
|
+
position: absolute;
|
|
438
|
+
inset: 0;
|
|
439
|
+
background: Canvas;
|
|
440
|
+
color: CanvasText;
|
|
441
|
+
display: flex;
|
|
442
|
+
flex-direction: column;
|
|
443
|
+
overflow: hidden;
|
|
444
|
+
z-index: 1;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
#activity-dialog-head {
|
|
448
|
+
padding: 8px 12px;
|
|
449
|
+
border-bottom: 1px solid color-mix(in srgb, currentColor 20%, transparent);
|
|
450
|
+
font-weight: bold;
|
|
451
|
+
font-size: 11px;
|
|
452
|
+
text-transform: uppercase;
|
|
453
|
+
letter-spacing: 0.05em;
|
|
454
|
+
display: flex;
|
|
455
|
+
align-items: center;
|
|
456
|
+
flex-shrink: 0;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
#activity-dialog-head button {
|
|
460
|
+
margin-left: auto;
|
|
461
|
+
border: none;
|
|
462
|
+
font-size: 16px;
|
|
463
|
+
padding: 0 4px;
|
|
464
|
+
line-height: 1;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
#activity-dialog-body {
|
|
468
|
+
flex: 1;
|
|
469
|
+
overflow-y: auto;
|
|
470
|
+
scrollbar-width: thin;
|
|
471
|
+
scrollbar-color: color-mix(in srgb, currentColor 20%, transparent) transparent;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
.activity-list {
|
|
475
|
+
padding: 16px 0;
|
|
476
|
+
margin: 0;
|
|
477
|
+
list-style: none;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
.activity-item {
|
|
481
|
+
display: grid;
|
|
482
|
+
grid-template-columns: 90px 20px 1fr;
|
|
483
|
+
gap: 0 8px;
|
|
484
|
+
min-height: 40px;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
.activity-time-col {
|
|
488
|
+
text-align: right;
|
|
489
|
+
padding-top: 6px;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
.activity-time {
|
|
493
|
+
font-size: 11px;
|
|
494
|
+
opacity: 0.7;
|
|
495
|
+
white-space: nowrap;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
.activity-date {
|
|
499
|
+
font-size: 10px;
|
|
500
|
+
opacity: 0.35;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
.activity-dot-col {
|
|
504
|
+
position: relative;
|
|
505
|
+
display: flex;
|
|
506
|
+
flex-direction: column;
|
|
507
|
+
align-items: center;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
.activity-dot-col::after {
|
|
511
|
+
content: '';
|
|
512
|
+
position: absolute;
|
|
513
|
+
left: 50%;
|
|
514
|
+
top: 0;
|
|
515
|
+
bottom: 0;
|
|
516
|
+
width: 1px;
|
|
517
|
+
transform: translateX(-50%);
|
|
518
|
+
background: color-mix(in srgb, currentColor 15%, transparent);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.activity-item:first-child .activity-dot-col::after {
|
|
522
|
+
top: 13px;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
.activity-item:last-child .activity-dot-col::after {
|
|
526
|
+
bottom: calc(100% - 14px);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
.activity-dot {
|
|
530
|
+
width: 5px;
|
|
531
|
+
height: 5px;
|
|
532
|
+
border-radius: 50%;
|
|
533
|
+
background: CanvasText;
|
|
534
|
+
opacity: 0.35;
|
|
535
|
+
flex-shrink: 0;
|
|
536
|
+
margin-top: 11px;
|
|
537
|
+
position: relative;
|
|
538
|
+
z-index: 1;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
.activity-line {
|
|
542
|
+
display: none;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
.activity-content {
|
|
546
|
+
padding: 6px 0 0;
|
|
547
|
+
min-width: 0;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
.activity-action {
|
|
551
|
+
font-family: ui-monospace, monospace;
|
|
552
|
+
font-size: 11px;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
.activity-by {
|
|
556
|
+
font-size: 11px;
|
|
557
|
+
opacity: 0.45;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
.activity-contributor {
|
|
561
|
+
font-weight: 600;
|
|
562
|
+
opacity: 1;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
.activity-detail {
|
|
566
|
+
margin-top: 2px;
|
|
567
|
+
font-size: 11px;
|
|
568
|
+
opacity: 0.45;
|
|
569
|
+
font-family: ui-monospace, monospace;
|
|
570
|
+
word-break: break-all;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
.activity-diff {
|
|
574
|
+
margin-top: 4px;
|
|
575
|
+
font-family: ui-monospace, monospace;
|
|
576
|
+
font-size: 11px;
|
|
577
|
+
line-height: 1.4;
|
|
578
|
+
white-space: pre-wrap;
|
|
579
|
+
word-break: break-all;
|
|
580
|
+
padding: 4px 6px;
|
|
581
|
+
background: color-mix(in srgb, currentColor 4%, transparent);
|
|
582
|
+
border-radius: 3px;
|
|
583
|
+
overflow-x: auto;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
.activity-diff .diff-add {
|
|
587
|
+
color: #22863a;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
.activity-diff .diff-del {
|
|
591
|
+
color: #cb2431;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
.activity-diff .diff-hunk {
|
|
595
|
+
color: #6f42c1;
|
|
596
|
+
opacity: 0.7;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
@media (prefers-color-scheme: dark) {
|
|
600
|
+
.activity-diff .diff-add {
|
|
601
|
+
color: #85e89d;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
.activity-diff .diff-del {
|
|
605
|
+
color: #f97583;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
.activity-diff .diff-hunk {
|
|
609
|
+
color: #b392f0;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
.activity-diff-truncated {
|
|
614
|
+
font-size: 10px;
|
|
615
|
+
opacity: 0.5;
|
|
616
|
+
font-style: italic;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
#activity-load-more {
|
|
620
|
+
width: 100%;
|
|
621
|
+
padding: 8px;
|
|
622
|
+
border: none;
|
|
623
|
+
border-top: 1px solid color-mix(in srgb, currentColor 10%, transparent);
|
|
624
|
+
font-size: 11px;
|
|
625
|
+
text-transform: uppercase;
|
|
626
|
+
letter-spacing: 0.05em;
|
|
627
|
+
opacity: 0.6;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
#activity-load-more:hover {
|
|
631
|
+
opacity: 1;
|
|
632
|
+
background: color-mix(in srgb, currentColor 5%, transparent);
|
|
397
633
|
}
|
|
398
634
|
</style>
|
|
399
635
|
</head>
|
|
@@ -420,7 +656,17 @@
|
|
|
420
656
|
</div>
|
|
421
657
|
</section>
|
|
422
658
|
</div>
|
|
423
|
-
<div id="
|
|
659
|
+
<div id="activity-modal">
|
|
660
|
+
<div id="activity-overlay"></div>
|
|
661
|
+
<div id="activity-dialog">
|
|
662
|
+
<div id="activity-dialog-head">Activity Log<button id="activity-close" aria-label="Close">×</button></div>
|
|
663
|
+
<div id="activity-dialog-body"></div>
|
|
664
|
+
</div>
|
|
665
|
+
</div>
|
|
666
|
+
<div id="status">
|
|
667
|
+
<span id="status-text"></span>
|
|
668
|
+
<button id="activity-btn" aria-label="Activity log"><i class="ph ph-pulse"></i> Activity Log</button>
|
|
669
|
+
</div>
|
|
424
670
|
<script type="module">
|
|
425
671
|
const { marked } = await import("https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js");
|
|
426
672
|
const matter = (await import("https://cdn.jsdelivr.net/npm/gray-matter@4.0.3/+esm")).default;
|
|
@@ -447,7 +693,7 @@
|
|
|
447
693
|
const tokenEl = $("token");
|
|
448
694
|
const filesEl = $("files");
|
|
449
695
|
const contentEl = $("content");
|
|
450
|
-
const
|
|
696
|
+
const statusTextEl = $("status-text");
|
|
451
697
|
const backdropEl = $("backdrop");
|
|
452
698
|
const menuToggleEl = $("menu-toggle");
|
|
453
699
|
|
|
@@ -468,7 +714,7 @@
|
|
|
468
714
|
let token = localStorage.getItem("sv-token") || "";
|
|
469
715
|
tokenEl.value = token;
|
|
470
716
|
|
|
471
|
-
function status(msg) {
|
|
717
|
+
function status(msg) { statusTextEl.textContent = msg; }
|
|
472
718
|
|
|
473
719
|
async function api(url, opts = {}) {
|
|
474
720
|
const res = await fetch(url, {
|
|
@@ -814,6 +1060,10 @@
|
|
|
814
1060
|
}
|
|
815
1061
|
});
|
|
816
1062
|
|
|
1063
|
+
evtSource.addEventListener("activity", (e) => {
|
|
1064
|
+
handleActivitySSE(e);
|
|
1065
|
+
});
|
|
1066
|
+
|
|
817
1067
|
evtSource.onerror = () => {
|
|
818
1068
|
// EventSource auto-reconnects
|
|
819
1069
|
};
|
|
@@ -855,6 +1105,176 @@
|
|
|
855
1105
|
document.addEventListener("mousemove", onMove);
|
|
856
1106
|
document.addEventListener("mouseup", onUp);
|
|
857
1107
|
});
|
|
1108
|
+
|
|
1109
|
+
// --- Activity modal ---
|
|
1110
|
+
|
|
1111
|
+
const activityModal = $("activity-modal");
|
|
1112
|
+
const activityBody = $("activity-dialog-body");
|
|
1113
|
+
const ACTIVITY_PAGE_SIZE = 1000;
|
|
1114
|
+
let activitySeenIds = new Set();
|
|
1115
|
+
let activityOffset = 0;
|
|
1116
|
+
let activityHasMore = false;
|
|
1117
|
+
|
|
1118
|
+
function openActivityModal() {
|
|
1119
|
+
activitySeenIds = new Set();
|
|
1120
|
+
activityOffset = 0;
|
|
1121
|
+
activityBody.innerHTML = "";
|
|
1122
|
+
activityModal.classList.add("open");
|
|
1123
|
+
loadActivityPage();
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
function closeActivityModal() {
|
|
1127
|
+
activityModal.classList.remove("open");
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
function handleActivitySSE(ev) {
|
|
1131
|
+
if (!activityModal.classList.contains("open")) return;
|
|
1132
|
+
const event = JSON.parse(ev.detail || ev.data);
|
|
1133
|
+
if (activitySeenIds.has(event.id)) return;
|
|
1134
|
+
activitySeenIds.add(event.id);
|
|
1135
|
+
activityOffset++;
|
|
1136
|
+
let list = activityBody.querySelector(".activity-list");
|
|
1137
|
+
if (!list) {
|
|
1138
|
+
const empty = activityBody.querySelector("p");
|
|
1139
|
+
if (empty) empty.remove();
|
|
1140
|
+
list = document.createElement("ul");
|
|
1141
|
+
list.className = "activity-list";
|
|
1142
|
+
activityBody.prepend(list);
|
|
1143
|
+
}
|
|
1144
|
+
list.insertAdjacentHTML("afterbegin", renderActivityItems([event]));
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
$("activity-btn").addEventListener("click", openActivityModal);
|
|
1148
|
+
$("activity-close").addEventListener("click", closeActivityModal);
|
|
1149
|
+
$("activity-overlay").addEventListener("click", closeActivityModal);
|
|
1150
|
+
|
|
1151
|
+
document.addEventListener("keydown", (e) => {
|
|
1152
|
+
if (e.key === "Escape" && activityModal.classList.contains("open")) {
|
|
1153
|
+
closeActivityModal();
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
function formatActivityTime(iso) {
|
|
1158
|
+
if (!iso) return { time: "", date: "" };
|
|
1159
|
+
const d = new Date(iso);
|
|
1160
|
+
const hh = String(d.getHours()).padStart(2, "0");
|
|
1161
|
+
const mm = String(d.getMinutes()).padStart(2, "0");
|
|
1162
|
+
const ss = String(d.getSeconds()).padStart(2, "0");
|
|
1163
|
+
const ms = String(d.getMilliseconds()).padStart(3, "0");
|
|
1164
|
+
const mon = d.toLocaleString(undefined, { month: "short" });
|
|
1165
|
+
const day = d.getDate();
|
|
1166
|
+
const yr = d.getFullYear();
|
|
1167
|
+
const now = new Date();
|
|
1168
|
+
const dateStr = yr === now.getFullYear()
|
|
1169
|
+
? mon + " " + day
|
|
1170
|
+
: mon + " " + day + ", " + yr;
|
|
1171
|
+
return {
|
|
1172
|
+
time: hh + ":" + mm + ":" + ss + "." + ms,
|
|
1173
|
+
date: dateStr,
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
function parseDetail(raw) {
|
|
1178
|
+
if (!raw) return null;
|
|
1179
|
+
try { return JSON.parse(raw); } catch { return null; }
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function esc(str) {
|
|
1183
|
+
return String(str)
|
|
1184
|
+
.replace(/&/g, "&")
|
|
1185
|
+
.replace(/</g, "<")
|
|
1186
|
+
.replace(/>/g, ">")
|
|
1187
|
+
.replace(/"/g, """);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function formatDetail(detail) {
|
|
1191
|
+
if (!detail || typeof detail !== "object") return "";
|
|
1192
|
+
return Object.entries(detail)
|
|
1193
|
+
.filter(([k]) => k !== "diff" && k !== "diff_truncated")
|
|
1194
|
+
.map(([k, v]) => k + "=" + v)
|
|
1195
|
+
.join(" ");
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
function renderDiff(raw) {
|
|
1199
|
+
if (!raw) return "";
|
|
1200
|
+
const lines = raw.split("\n");
|
|
1201
|
+
const htmlLines = lines.map((line) => {
|
|
1202
|
+
const escaped = esc(line);
|
|
1203
|
+
if (line.startsWith("@@")) return '<span class="diff-hunk">' + escaped + '</span>';
|
|
1204
|
+
if (line.startsWith("+")) return '<span class="diff-add">' + escaped + '</span>';
|
|
1205
|
+
if (line.startsWith("-")) return '<span class="diff-del">' + escaped + '</span>';
|
|
1206
|
+
return escaped;
|
|
1207
|
+
});
|
|
1208
|
+
return htmlLines.join("\n");
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
function renderActivityItems(events) {
|
|
1212
|
+
return events.map((ev) => {
|
|
1213
|
+
const { time, date } = formatActivityTime(ev.created_at);
|
|
1214
|
+
const parsed = parseDetail(ev.detail);
|
|
1215
|
+
const detail = formatDetail(parsed);
|
|
1216
|
+
const diff = parsed?.diff || null;
|
|
1217
|
+
const truncated = parsed?.diff_truncated || false;
|
|
1218
|
+
|
|
1219
|
+
let content = '<span class="activity-action">' + esc(ev.action) + '</span>' +
|
|
1220
|
+
' <span class="activity-by">by</span>' +
|
|
1221
|
+
' <span class="activity-contributor">' + esc(ev.contributor) + '</span>';
|
|
1222
|
+
if (detail) content += '<div class="activity-detail">' + esc(detail) + '</div>';
|
|
1223
|
+
if (diff) {
|
|
1224
|
+
content += '<div class="activity-diff">' + renderDiff(diff) + '</div>';
|
|
1225
|
+
if (truncated) content += '<span class="activity-diff-truncated">diff truncated</span>';
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
return '<li class="activity-item">' +
|
|
1229
|
+
'<div class="activity-time-col">' +
|
|
1230
|
+
'<div class="activity-time">' + time + '</div>' +
|
|
1231
|
+
'<div class="activity-date">' + date + '</div>' +
|
|
1232
|
+
'</div>' +
|
|
1233
|
+
'<div class="activity-dot-col">' +
|
|
1234
|
+
'<div class="activity-dot"></div>' +
|
|
1235
|
+
'<div class="activity-line"></div>' +
|
|
1236
|
+
'</div>' +
|
|
1237
|
+
'<div class="activity-content">' + content + '</div>' +
|
|
1238
|
+
'</li>';
|
|
1239
|
+
}).join("");
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
async function loadActivityPage() {
|
|
1243
|
+
const existingBtn = activityBody.querySelector("#activity-load-more");
|
|
1244
|
+
if (existingBtn) existingBtn.remove();
|
|
1245
|
+
|
|
1246
|
+
const limit = ACTIVITY_PAGE_SIZE;
|
|
1247
|
+
const offset = activityOffset;
|
|
1248
|
+
const url = "/v1/activity?limit=" + (limit + 1) + "&offset=" + offset;
|
|
1249
|
+
const { events } = await (await api(url)).json();
|
|
1250
|
+
|
|
1251
|
+
activityHasMore = events.length > limit;
|
|
1252
|
+
const page = activityHasMore ? events.slice(0, limit) : events;
|
|
1253
|
+
for (const ev of page) activitySeenIds.add(ev.id);
|
|
1254
|
+
activityOffset += page.length;
|
|
1255
|
+
|
|
1256
|
+
let list = activityBody.querySelector(".activity-list");
|
|
1257
|
+
if (!list) {
|
|
1258
|
+
list = document.createElement("ul");
|
|
1259
|
+
list.className = "activity-list";
|
|
1260
|
+
activityBody.appendChild(list);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
if (activitySeenIds.size === 0) {
|
|
1264
|
+
activityBody.innerHTML = '<p style="padding:12px;opacity:0.5;font-size:12px">No activity.</p>';
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
list.insertAdjacentHTML("beforeend", renderActivityItems(page));
|
|
1269
|
+
|
|
1270
|
+
if (activityHasMore) {
|
|
1271
|
+
const btn = document.createElement("button");
|
|
1272
|
+
btn.id = "activity-load-more";
|
|
1273
|
+
btn.textContent = "Load more";
|
|
1274
|
+
btn.addEventListener("click", () => loadActivityPage());
|
|
1275
|
+
activityBody.appendChild(btn);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
858
1278
|
</script>
|
|
859
1279
|
</body>
|
|
860
1280
|
|
package/dist/server.js
CHANGED
|
@@ -6,7 +6,7 @@ import { mkdir } from "fs/promises";
|
|
|
6
6
|
|
|
7
7
|
// src/db.ts
|
|
8
8
|
import { Database } from "bun:sqlite";
|
|
9
|
-
import { randomUUID } from "crypto";
|
|
9
|
+
import { randomBytes, randomUUID } from "crypto";
|
|
10
10
|
var db;
|
|
11
11
|
function getDb() {
|
|
12
12
|
if (!db)
|
|
@@ -50,6 +50,17 @@ function initDb(dbPath) {
|
|
|
50
50
|
PRIMARY KEY (contributor, path),
|
|
51
51
|
FOREIGN KEY (contributor) REFERENCES contributors(username)
|
|
52
52
|
);
|
|
53
|
+
|
|
54
|
+
CREATE TABLE IF NOT EXISTS activity (
|
|
55
|
+
id TEXT PRIMARY KEY,
|
|
56
|
+
contributor TEXT NOT NULL REFERENCES contributors(username),
|
|
57
|
+
action TEXT NOT NULL,
|
|
58
|
+
detail TEXT,
|
|
59
|
+
created_at TEXT NOT NULL
|
|
60
|
+
);
|
|
61
|
+
CREATE INDEX IF NOT EXISTS idx_activity_contributor ON activity(contributor);
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_activity_created_at ON activity(created_at);
|
|
63
|
+
CREATE INDEX IF NOT EXISTS idx_activity_action ON activity(action);
|
|
53
64
|
`);
|
|
54
65
|
const hasFts = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='items_fts'").get();
|
|
55
66
|
if (!hasFts) {
|
|
@@ -137,6 +148,7 @@ function deleteContributor(username) {
|
|
|
137
148
|
const exists = d.prepare("SELECT 1 FROM contributors WHERE username = ?").get(username);
|
|
138
149
|
if (!exists)
|
|
139
150
|
return false;
|
|
151
|
+
d.prepare("DELETE FROM activity WHERE contributor = ?").run(username);
|
|
140
152
|
d.prepare("DELETE FROM items WHERE contributor = ?").run(username);
|
|
141
153
|
d.prepare("DELETE FROM api_keys WHERE contributor = ?").run(username);
|
|
142
154
|
d.prepare("DELETE FROM contributors WHERE username = ?").run(username);
|
|
@@ -252,6 +264,33 @@ function searchItems(query, contributor, limit = 10) {
|
|
|
252
264
|
ORDER BY rank
|
|
253
265
|
LIMIT ?`).all(query, limit);
|
|
254
266
|
}
|
|
267
|
+
function createActivityEvent(contributor, action, detail) {
|
|
268
|
+
const id = `act_${randomBytes(6).toString("hex")}`;
|
|
269
|
+
const now = new Date().toISOString();
|
|
270
|
+
const detailJson = detail ? JSON.stringify(detail) : null;
|
|
271
|
+
getDb().prepare("INSERT INTO activity (id, contributor, action, detail, created_at) VALUES (?, ?, ?, ?, ?)").run(id, contributor, action, detailJson, now);
|
|
272
|
+
return { id, contributor, action, detail: detailJson, created_at: now };
|
|
273
|
+
}
|
|
274
|
+
function listActivityEvents(opts) {
|
|
275
|
+
const conditions = [];
|
|
276
|
+
const params = [];
|
|
277
|
+
if (opts?.contributor) {
|
|
278
|
+
conditions.push("contributor = ?");
|
|
279
|
+
params.push(opts.contributor);
|
|
280
|
+
}
|
|
281
|
+
if (opts?.action) {
|
|
282
|
+
conditions.push("action = ?");
|
|
283
|
+
params.push(opts.action);
|
|
284
|
+
}
|
|
285
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
286
|
+
const limit = opts?.limit ?? 50;
|
|
287
|
+
const offset = opts?.offset ?? 0;
|
|
288
|
+
params.push(limit, offset);
|
|
289
|
+
return getDb().prepare(`SELECT id, contributor, action, detail, created_at
|
|
290
|
+
FROM activity ${where}
|
|
291
|
+
ORDER BY created_at DESC
|
|
292
|
+
LIMIT ? OFFSET ?`).all(...params);
|
|
293
|
+
}
|
|
255
294
|
|
|
256
295
|
class ItemTooLargeError extends Error {
|
|
257
296
|
size;
|
|
@@ -1786,9 +1825,9 @@ import { readFileSync } from "fs";
|
|
|
1786
1825
|
import { resolve } from "path";
|
|
1787
1826
|
|
|
1788
1827
|
// src/auth.ts
|
|
1789
|
-
import { createHash, randomBytes } from "crypto";
|
|
1828
|
+
import { createHash, randomBytes as randomBytes2 } from "crypto";
|
|
1790
1829
|
function generateToken() {
|
|
1791
|
-
return `sv_${
|
|
1830
|
+
return `sv_${randomBytes2(16).toString("hex")}`;
|
|
1792
1831
|
}
|
|
1793
1832
|
function hashToken(raw2) {
|
|
1794
1833
|
return createHash("sha256").update(raw2).digest("hex");
|
|
@@ -1847,9 +1886,161 @@ data: ${JSON.stringify(data)}
|
|
|
1847
1886
|
}
|
|
1848
1887
|
}
|
|
1849
1888
|
|
|
1889
|
+
// src/diff.ts
|
|
1890
|
+
var DIFF_MAX_BYTES = 5000;
|
|
1891
|
+
function computeDiff(oldText, newText) {
|
|
1892
|
+
if (oldText === newText)
|
|
1893
|
+
return null;
|
|
1894
|
+
if (oldText === "")
|
|
1895
|
+
return null;
|
|
1896
|
+
const oldLines = oldText.split(`
|
|
1897
|
+
`);
|
|
1898
|
+
const newLines = newText.split(`
|
|
1899
|
+
`);
|
|
1900
|
+
const editScript = myersDiff(oldLines, newLines);
|
|
1901
|
+
const hunks = buildHunks(editScript, oldLines, newLines, 3);
|
|
1902
|
+
let diff = "";
|
|
1903
|
+
let truncated = false;
|
|
1904
|
+
for (const hunk of hunks) {
|
|
1905
|
+
const header = `@@ -${hunk.oldStart},${hunk.oldCount}` + ` +${hunk.newStart},${hunk.newCount} @@
|
|
1906
|
+
`;
|
|
1907
|
+
diff += header;
|
|
1908
|
+
for (const line of hunk.lines) {
|
|
1909
|
+
diff += line + `
|
|
1910
|
+
`;
|
|
1911
|
+
}
|
|
1912
|
+
if (diff.length > DIFF_MAX_BYTES) {
|
|
1913
|
+
diff = diff.slice(0, DIFF_MAX_BYTES);
|
|
1914
|
+
truncated = true;
|
|
1915
|
+
break;
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
return { diff, truncated };
|
|
1919
|
+
}
|
|
1920
|
+
function myersDiff(a, b) {
|
|
1921
|
+
const n = a.length;
|
|
1922
|
+
const m = b.length;
|
|
1923
|
+
const max = n + m;
|
|
1924
|
+
const vSize = 2 * max + 1;
|
|
1925
|
+
const v = new Int32Array(vSize);
|
|
1926
|
+
v.fill(-1);
|
|
1927
|
+
const offset = max;
|
|
1928
|
+
v[offset + 1] = 0;
|
|
1929
|
+
const trace = [];
|
|
1930
|
+
outer:
|
|
1931
|
+
for (let d = 0;d <= max; d++) {
|
|
1932
|
+
trace.push(v.slice());
|
|
1933
|
+
for (let k = -d;k <= d; k += 2) {
|
|
1934
|
+
let x2;
|
|
1935
|
+
if (k === -d || k !== d && v[offset + k - 1] < v[offset + k + 1]) {
|
|
1936
|
+
x2 = v[offset + k + 1];
|
|
1937
|
+
} else {
|
|
1938
|
+
x2 = v[offset + k - 1] + 1;
|
|
1939
|
+
}
|
|
1940
|
+
let y2 = x2 - k;
|
|
1941
|
+
while (x2 < n && y2 < m && a[x2] === b[y2]) {
|
|
1942
|
+
x2++;
|
|
1943
|
+
y2++;
|
|
1944
|
+
}
|
|
1945
|
+
v[offset + k] = x2;
|
|
1946
|
+
if (x2 >= n && y2 >= m)
|
|
1947
|
+
break outer;
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
const edits = [];
|
|
1951
|
+
let x = n;
|
|
1952
|
+
let y = m;
|
|
1953
|
+
for (let d = trace.length - 1;d >= 0; d--) {
|
|
1954
|
+
const tv = trace[d];
|
|
1955
|
+
const k = x - y;
|
|
1956
|
+
let prevK;
|
|
1957
|
+
if (k === -d || k !== d && tv[offset + k - 1] < tv[offset + k + 1]) {
|
|
1958
|
+
prevK = k + 1;
|
|
1959
|
+
} else {
|
|
1960
|
+
prevK = k - 1;
|
|
1961
|
+
}
|
|
1962
|
+
const prevX = tv[offset + prevK];
|
|
1963
|
+
const prevY = prevX - prevK;
|
|
1964
|
+
while (x > prevX && y > prevY) {
|
|
1965
|
+
x--;
|
|
1966
|
+
y--;
|
|
1967
|
+
edits.push({ op: 0 /* Equal */, oldIdx: x, newIdx: y });
|
|
1968
|
+
}
|
|
1969
|
+
if (d > 0) {
|
|
1970
|
+
if (x === prevX) {
|
|
1971
|
+
edits.push({ op: 1 /* Insert */, oldIdx: x, newIdx: prevY });
|
|
1972
|
+
y--;
|
|
1973
|
+
} else {
|
|
1974
|
+
edits.push({ op: 2 /* Delete */, oldIdx: prevX, newIdx: y });
|
|
1975
|
+
x--;
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
edits.reverse();
|
|
1980
|
+
return edits;
|
|
1981
|
+
}
|
|
1982
|
+
function buildHunks(edits, oldLines, newLines, context) {
|
|
1983
|
+
const hunks = [];
|
|
1984
|
+
let i = 0;
|
|
1985
|
+
while (i < edits.length) {
|
|
1986
|
+
while (i < edits.length && edits[i].op === 0 /* Equal */)
|
|
1987
|
+
i++;
|
|
1988
|
+
if (i >= edits.length)
|
|
1989
|
+
break;
|
|
1990
|
+
let start = i;
|
|
1991
|
+
for (let c = 0;c < context && start > 0; c++)
|
|
1992
|
+
start--;
|
|
1993
|
+
let end = i;
|
|
1994
|
+
while (end < edits.length) {
|
|
1995
|
+
if (edits[end].op !== 0 /* Equal */) {
|
|
1996
|
+
end++;
|
|
1997
|
+
continue;
|
|
1998
|
+
}
|
|
1999
|
+
let run = 0;
|
|
2000
|
+
let j = end;
|
|
2001
|
+
while (j < edits.length && edits[j].op === 0 /* Equal */) {
|
|
2002
|
+
run++;
|
|
2003
|
+
j++;
|
|
2004
|
+
}
|
|
2005
|
+
if (j >= edits.length || run > context * 2) {
|
|
2006
|
+
end = Math.min(end + context, edits.length);
|
|
2007
|
+
break;
|
|
2008
|
+
}
|
|
2009
|
+
end = j;
|
|
2010
|
+
}
|
|
2011
|
+
const hunkEdits = edits.slice(start, end);
|
|
2012
|
+
const firstEdit = hunkEdits[0];
|
|
2013
|
+
let oldStart = firstEdit.oldIdx + 1;
|
|
2014
|
+
let newStart = firstEdit.newIdx + 1;
|
|
2015
|
+
let oldCount = 0;
|
|
2016
|
+
let newCount = 0;
|
|
2017
|
+
const lines = [];
|
|
2018
|
+
for (const e of hunkEdits) {
|
|
2019
|
+
if (e.op === 0 /* Equal */) {
|
|
2020
|
+
lines.push(" " + oldLines[e.oldIdx]);
|
|
2021
|
+
oldCount++;
|
|
2022
|
+
newCount++;
|
|
2023
|
+
} else if (e.op === 2 /* Delete */) {
|
|
2024
|
+
lines.push("-" + oldLines[e.oldIdx]);
|
|
2025
|
+
oldCount++;
|
|
2026
|
+
} else {
|
|
2027
|
+
lines.push("+" + newLines[e.newIdx]);
|
|
2028
|
+
newCount++;
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
hunks.push({ oldStart, oldCount, newStart, newCount, lines });
|
|
2032
|
+
i = end;
|
|
2033
|
+
}
|
|
2034
|
+
return hunks;
|
|
2035
|
+
}
|
|
2036
|
+
|
|
1850
2037
|
// src/routes.ts
|
|
1851
2038
|
var uiPath = resolve(import.meta.dirname, "index.html");
|
|
1852
2039
|
var isDev = true;
|
|
2040
|
+
function logActivity(contributor, action, detail) {
|
|
2041
|
+
const event = createActivityEvent(contributor, action, detail);
|
|
2042
|
+
broadcast("activity", event);
|
|
2043
|
+
}
|
|
1853
2044
|
var uiHtmlCached = readFileSync(uiPath, "utf-8");
|
|
1854
2045
|
function extractFileInfo(reqPath) {
|
|
1855
2046
|
const raw2 = reqPath.replace("/v1/files/", "");
|
|
@@ -1907,6 +2098,9 @@ function createApp() {
|
|
|
1907
2098
|
if (!isFirstUser && body.invite) {
|
|
1908
2099
|
markInviteUsed(body.invite, contributor.username);
|
|
1909
2100
|
}
|
|
2101
|
+
logActivity(contributor.username, "contributor_created", {
|
|
2102
|
+
username: contributor.username
|
|
2103
|
+
});
|
|
1910
2104
|
return c.json({
|
|
1911
2105
|
contributor: {
|
|
1912
2106
|
username: contributor.username,
|
|
@@ -1930,6 +2124,9 @@ function createApp() {
|
|
|
1930
2124
|
return c.json({ error: "Only the admin can generate invite codes" }, 403);
|
|
1931
2125
|
}
|
|
1932
2126
|
const invite = createInvite(contributor.username);
|
|
2127
|
+
logActivity(contributor.username, "invite_created", {
|
|
2128
|
+
invite: invite.id
|
|
2129
|
+
});
|
|
1933
2130
|
return c.json({
|
|
1934
2131
|
invite: invite.id,
|
|
1935
2132
|
createdAt: invite.created_at
|
|
@@ -1957,6 +2154,9 @@ function createApp() {
|
|
|
1957
2154
|
if (!found) {
|
|
1958
2155
|
return c.json({ error: "Contributor not found" }, 404);
|
|
1959
2156
|
}
|
|
2157
|
+
logActivity(contributor.username, "contributor_deleted", {
|
|
2158
|
+
username: target
|
|
2159
|
+
});
|
|
1960
2160
|
return c.body(null, 204);
|
|
1961
2161
|
});
|
|
1962
2162
|
authed.put("/v1/files/*", async (c) => {
|
|
@@ -1979,8 +2179,20 @@ function createApp() {
|
|
|
1979
2179
|
const now = new Date().toISOString();
|
|
1980
2180
|
const originCtime = c.req.header("X-Origin-Ctime") || now;
|
|
1981
2181
|
const originMtime = c.req.header("X-Origin-Mtime") || now;
|
|
2182
|
+
const existing = getItem(parsed.username, parsed.filePath);
|
|
1982
2183
|
try {
|
|
1983
2184
|
const item = upsertItem(parsed.username, parsed.filePath, content, originCtime, originMtime);
|
|
2185
|
+
const detail = {
|
|
2186
|
+
path: item.path,
|
|
2187
|
+
size: Buffer.byteLength(item.content)
|
|
2188
|
+
};
|
|
2189
|
+
const diffResult = computeDiff(existing?.content ?? "", content);
|
|
2190
|
+
if (diffResult) {
|
|
2191
|
+
detail.diff = diffResult.diff;
|
|
2192
|
+
if (diffResult.truncated)
|
|
2193
|
+
detail.diff_truncated = true;
|
|
2194
|
+
}
|
|
2195
|
+
logActivity(contributor.username, "file_upserted", detail);
|
|
1984
2196
|
broadcast("file_updated", {
|
|
1985
2197
|
contributor: parsed.username,
|
|
1986
2198
|
path: item.path,
|
|
@@ -2017,6 +2229,9 @@ function createApp() {
|
|
|
2017
2229
|
if (!found) {
|
|
2018
2230
|
return c.json({ error: "File not found" }, 404);
|
|
2019
2231
|
}
|
|
2232
|
+
logActivity(contributor.username, "file_deleted", {
|
|
2233
|
+
path: parsed.filePath
|
|
2234
|
+
});
|
|
2020
2235
|
broadcast("file_deleted", {
|
|
2021
2236
|
contributor: parsed.username,
|
|
2022
2237
|
path: parsed.filePath
|
|
@@ -2111,6 +2326,19 @@ data: {}
|
|
|
2111
2326
|
const results = searchItems(q, contributorParam, limit);
|
|
2112
2327
|
return c.json({ results });
|
|
2113
2328
|
});
|
|
2329
|
+
authed.get("/v1/activity", (c) => {
|
|
2330
|
+
const contributor = c.req.query("contributor") || undefined;
|
|
2331
|
+
const action = c.req.query("action") || undefined;
|
|
2332
|
+
const limit = c.req.query("limit") ? parseInt(c.req.query("limit"), 10) : undefined;
|
|
2333
|
+
const offset = c.req.query("offset") ? parseInt(c.req.query("offset"), 10) : undefined;
|
|
2334
|
+
const events = listActivityEvents({
|
|
2335
|
+
contributor,
|
|
2336
|
+
action,
|
|
2337
|
+
limit,
|
|
2338
|
+
offset
|
|
2339
|
+
});
|
|
2340
|
+
return c.json({ events });
|
|
2341
|
+
});
|
|
2114
2342
|
app.route("/", authed);
|
|
2115
2343
|
return app;
|
|
2116
2344
|
}
|