@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 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="status"></div>
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">&times;</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 statusEl = $("status");
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) { statusEl.textContent = 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, "&amp;")
1185
+ .replace(/</g, "&lt;")
1186
+ .replace(/>/g, "&gt;")
1187
+ .replace(/"/g, "&quot;");
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_${randomBytes(16).toString("hex")}`;
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seedvault/server",
3
- "version": "0.1.5",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "seedvault-server": "bin/seedvault-server.mjs"