codedash-app 1.4.0 → 1.5.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/package.json +1 -1
- package/src/data.js +95 -1
- package/src/frontend/app.js +185 -4
- package/src/frontend/styles.css +98 -0
- package/src/server.js +18 -1
package/package.json
CHANGED
package/src/data.js
CHANGED
|
@@ -330,6 +330,99 @@ function exportSessionMarkdown(sessionId, project) {
|
|
|
330
330
|
return parts.join('');
|
|
331
331
|
}
|
|
332
332
|
|
|
333
|
+
// ── Session Preview (first N messages, lightweight) ────────
|
|
334
|
+
|
|
335
|
+
function getSessionPreview(sessionId, project, limit) {
|
|
336
|
+
limit = limit || 10;
|
|
337
|
+
const projectKey = project.replace(/\//g, '-').replace(/^-/, '');
|
|
338
|
+
const sessionFile = path.join(PROJECTS_DIR, projectKey, `${sessionId}.jsonl`);
|
|
339
|
+
|
|
340
|
+
if (!fs.existsSync(sessionFile)) return [];
|
|
341
|
+
|
|
342
|
+
const messages = [];
|
|
343
|
+
const lines = fs.readFileSync(sessionFile, 'utf8').split('\n').filter(Boolean);
|
|
344
|
+
|
|
345
|
+
for (const line of lines) {
|
|
346
|
+
if (messages.length >= limit) break;
|
|
347
|
+
try {
|
|
348
|
+
const entry = JSON.parse(line);
|
|
349
|
+
if (entry.type === 'user' || entry.type === 'assistant') {
|
|
350
|
+
const msg = entry.message || {};
|
|
351
|
+
let content = msg.content || '';
|
|
352
|
+
if (Array.isArray(content)) {
|
|
353
|
+
content = content
|
|
354
|
+
.map(b => (typeof b === 'string' ? b : (b.type === 'text' ? b.text : '')))
|
|
355
|
+
.filter(Boolean)
|
|
356
|
+
.join('\n');
|
|
357
|
+
}
|
|
358
|
+
messages.push({
|
|
359
|
+
role: entry.type,
|
|
360
|
+
content: content.slice(0, 300), // short preview
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
} catch {}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return messages;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ── Full-text search across all sessions ──────────────────
|
|
370
|
+
|
|
371
|
+
function searchFullText(query, sessions) {
|
|
372
|
+
if (!query || query.length < 2) return [];
|
|
373
|
+
const q = query.toLowerCase();
|
|
374
|
+
const results = [];
|
|
375
|
+
|
|
376
|
+
for (const s of sessions) {
|
|
377
|
+
if (s.tool !== 'claude' || !s.has_detail) continue;
|
|
378
|
+
|
|
379
|
+
const projectKey = s.project.replace(/\//g, '-').replace(/^-/, '');
|
|
380
|
+
const sessionFile = path.join(PROJECTS_DIR, projectKey, `${s.id}.jsonl`);
|
|
381
|
+
if (!fs.existsSync(sessionFile)) continue;
|
|
382
|
+
|
|
383
|
+
try {
|
|
384
|
+
const data = fs.readFileSync(sessionFile, 'utf8');
|
|
385
|
+
// Quick check before parsing
|
|
386
|
+
if (data.toLowerCase().indexOf(q) === -1) continue;
|
|
387
|
+
|
|
388
|
+
// Find matching messages
|
|
389
|
+
const lines = data.split('\n').filter(Boolean);
|
|
390
|
+
const matches = [];
|
|
391
|
+
for (const line of lines) {
|
|
392
|
+
if (matches.length >= 3) break; // max 3 matches per session
|
|
393
|
+
try {
|
|
394
|
+
const entry = JSON.parse(line);
|
|
395
|
+
if (entry.type !== 'user' && entry.type !== 'assistant') continue;
|
|
396
|
+
const msg = entry.message || {};
|
|
397
|
+
let content = msg.content || '';
|
|
398
|
+
if (Array.isArray(content)) {
|
|
399
|
+
content = content
|
|
400
|
+
.map(b => (typeof b === 'string' ? b : (b.type === 'text' ? b.text : '')))
|
|
401
|
+
.filter(Boolean)
|
|
402
|
+
.join('\n');
|
|
403
|
+
}
|
|
404
|
+
if (content.toLowerCase().indexOf(q) >= 0) {
|
|
405
|
+
// Extract snippet around match
|
|
406
|
+
const idx = content.toLowerCase().indexOf(q);
|
|
407
|
+
const start = Math.max(0, idx - 50);
|
|
408
|
+
const end = Math.min(content.length, idx + q.length + 50);
|
|
409
|
+
matches.push({
|
|
410
|
+
role: entry.type,
|
|
411
|
+
snippet: (start > 0 ? '...' : '') + content.slice(start, end) + (end < content.length ? '...' : ''),
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
} catch {}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (matches.length > 0) {
|
|
418
|
+
results.push({ sessionId: s.id, matches });
|
|
419
|
+
}
|
|
420
|
+
} catch {}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return results;
|
|
424
|
+
}
|
|
425
|
+
|
|
333
426
|
// ── Exports ────────────────────────────────────────────────
|
|
334
427
|
|
|
335
428
|
module.exports = {
|
|
@@ -338,7 +431,8 @@ module.exports = {
|
|
|
338
431
|
deleteSession,
|
|
339
432
|
getGitCommits,
|
|
340
433
|
exportSessionMarkdown,
|
|
341
|
-
|
|
434
|
+
getSessionPreview,
|
|
435
|
+
searchFullText,
|
|
342
436
|
CLAUDE_DIR,
|
|
343
437
|
CODEX_DIR,
|
|
344
438
|
HISTORY_FILE,
|
package/src/frontend/app.js
CHANGED
|
@@ -288,6 +288,12 @@ function applyFilters() {
|
|
|
288
288
|
function onSearch(val) {
|
|
289
289
|
searchQuery = val;
|
|
290
290
|
applyFilters();
|
|
291
|
+
|
|
292
|
+
// Trigger deep search after debounce
|
|
293
|
+
clearTimeout(deepSearchTimeout);
|
|
294
|
+
if (val && val.length >= 3) {
|
|
295
|
+
deepSearchTimeout = setTimeout(function() { deepSearch(val); }, 600);
|
|
296
|
+
}
|
|
291
297
|
}
|
|
292
298
|
|
|
293
299
|
function onTagFilter(val) {
|
|
@@ -379,12 +385,15 @@ function renderCard(s, idx) {
|
|
|
379
385
|
html += '<span class="card-meta">' + escHtml(s.last_time || '') + '</span>';
|
|
380
386
|
html += '<span class="card-id">' + s.id.slice(0, 8) + '</span>';
|
|
381
387
|
// Tags
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
388
|
+
html += '<span class="card-tags">' + tagHtml;
|
|
389
|
+
html += '<button class="tag-add-btn" onclick="showTagDropdown(event, \'' + s.id + '\')" title="Add tag">+</button>';
|
|
390
|
+
html += '</span>';
|
|
391
|
+
if (s.has_detail) {
|
|
392
|
+
html += '<button class="card-expand-btn" onclick="event.stopPropagation();toggleExpand(\'' + s.id + '\',\'' + escHtml(s.project || '').replace(/'/g, "\\'") + '\',this)" title="Preview messages">▾</button>';
|
|
386
393
|
}
|
|
387
394
|
html += '</div>';
|
|
395
|
+
// Expandable preview area (hidden by default)
|
|
396
|
+
html += '<div class="card-preview-area" id="preview-' + s.id + '"></div>';
|
|
388
397
|
html += '</div>';
|
|
389
398
|
return html;
|
|
390
399
|
}
|
|
@@ -425,6 +434,177 @@ function renderListCard(s, idx) {
|
|
|
425
434
|
return html;
|
|
426
435
|
}
|
|
427
436
|
|
|
437
|
+
// ── Card expand (inline preview) ──────────────────────────────
|
|
438
|
+
|
|
439
|
+
async function toggleExpand(sessionId, project, btn) {
|
|
440
|
+
var area = document.getElementById('preview-' + sessionId);
|
|
441
|
+
if (!area) return;
|
|
442
|
+
|
|
443
|
+
if (area.classList.contains('open')) {
|
|
444
|
+
area.classList.remove('open');
|
|
445
|
+
area.innerHTML = '';
|
|
446
|
+
btn.innerHTML = '▾';
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
btn.innerHTML = '⌛';
|
|
451
|
+
area.innerHTML = '<div class="loading">Loading...</div>';
|
|
452
|
+
area.classList.add('open');
|
|
453
|
+
|
|
454
|
+
try {
|
|
455
|
+
var resp = await fetch('/api/preview/' + sessionId + '?project=' + encodeURIComponent(project) + '&limit=10');
|
|
456
|
+
var messages = await resp.json();
|
|
457
|
+
|
|
458
|
+
if (messages.length === 0) {
|
|
459
|
+
area.innerHTML = '<div class="preview-empty">No messages</div>';
|
|
460
|
+
} else {
|
|
461
|
+
var html = '';
|
|
462
|
+
messages.forEach(function(m) {
|
|
463
|
+
var cls = m.role === 'user' ? 'preview-user' : 'preview-assistant';
|
|
464
|
+
var label = m.role === 'user' ? 'You' : 'AI';
|
|
465
|
+
html += '<div class="preview-msg ' + cls + '">';
|
|
466
|
+
html += '<span class="preview-role">' + label + '</span> ';
|
|
467
|
+
html += escHtml(m.content);
|
|
468
|
+
html += '</div>';
|
|
469
|
+
});
|
|
470
|
+
area.innerHTML = html;
|
|
471
|
+
}
|
|
472
|
+
btn.innerHTML = '▴';
|
|
473
|
+
} catch (e) {
|
|
474
|
+
area.innerHTML = '<div class="preview-empty">Failed to load</div>';
|
|
475
|
+
btn.innerHTML = '▾';
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// ── Hover tooltip (show first messages on hover) ──────────────
|
|
480
|
+
|
|
481
|
+
var hoverTimer = null;
|
|
482
|
+
var hoverTooltip = null;
|
|
483
|
+
|
|
484
|
+
function initHoverPreview() {
|
|
485
|
+
document.addEventListener('mouseover', function(e) {
|
|
486
|
+
var card = e.target.closest('.card');
|
|
487
|
+
if (!card) { hideHoverTooltip(); return; }
|
|
488
|
+
|
|
489
|
+
var id = card.getAttribute('data-id');
|
|
490
|
+
if (!id) return;
|
|
491
|
+
|
|
492
|
+
clearTimeout(hoverTimer);
|
|
493
|
+
hoverTimer = setTimeout(function() {
|
|
494
|
+
var s = allSessions.find(function(x) { return x.id === id; });
|
|
495
|
+
if (!s || !s.has_detail) return;
|
|
496
|
+
showHoverTooltip(card, s);
|
|
497
|
+
}, 400); // 400ms delay
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
document.addEventListener('mouseout', function(e) {
|
|
501
|
+
var card = e.target.closest('.card');
|
|
502
|
+
if (!card) { clearTimeout(hoverTimer); hideHoverTooltip(); }
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async function showHoverTooltip(card, session) {
|
|
507
|
+
hideHoverTooltip();
|
|
508
|
+
|
|
509
|
+
try {
|
|
510
|
+
var resp = await fetch('/api/preview/' + session.id + '?project=' + encodeURIComponent(session.project || '') + '&limit=6');
|
|
511
|
+
var messages = await resp.json();
|
|
512
|
+
if (messages.length === 0) return;
|
|
513
|
+
|
|
514
|
+
var tip = document.createElement('div');
|
|
515
|
+
tip.className = 'hover-tooltip';
|
|
516
|
+
|
|
517
|
+
var html = '';
|
|
518
|
+
messages.forEach(function(m) {
|
|
519
|
+
var label = m.role === 'user' ? 'You' : 'AI';
|
|
520
|
+
var cls = m.role === 'user' ? 'preview-user' : 'preview-assistant';
|
|
521
|
+
html += '<div class="preview-msg ' + cls + '">';
|
|
522
|
+
html += '<span class="preview-role">' + label + '</span> ';
|
|
523
|
+
html += escHtml(m.content.slice(0, 150));
|
|
524
|
+
if (m.content.length > 150) html += '...';
|
|
525
|
+
html += '</div>';
|
|
526
|
+
});
|
|
527
|
+
tip.innerHTML = html;
|
|
528
|
+
|
|
529
|
+
document.body.appendChild(tip);
|
|
530
|
+
hoverTooltip = tip;
|
|
531
|
+
|
|
532
|
+
// Position near card
|
|
533
|
+
var rect = card.getBoundingClientRect();
|
|
534
|
+
tip.style.top = Math.min(rect.bottom + 4, window.innerHeight - tip.offsetHeight - 8) + 'px';
|
|
535
|
+
tip.style.left = Math.max(8, rect.left) + 'px';
|
|
536
|
+
tip.style.maxWidth = Math.min(500, window.innerWidth - rect.left - 20) + 'px';
|
|
537
|
+
|
|
538
|
+
requestAnimationFrame(function() { tip.classList.add('visible'); });
|
|
539
|
+
} catch {}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function hideHoverTooltip() {
|
|
543
|
+
if (hoverTooltip) {
|
|
544
|
+
hoverTooltip.remove();
|
|
545
|
+
hoverTooltip = null;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// ── Deep search (full-text across session content) ────────────
|
|
550
|
+
|
|
551
|
+
var deepSearchCache = {};
|
|
552
|
+
var deepSearchTimeout = null;
|
|
553
|
+
|
|
554
|
+
async function deepSearch(query) {
|
|
555
|
+
if (!query || query.length < 3) return;
|
|
556
|
+
if (deepSearchCache[query]) {
|
|
557
|
+
applyDeepSearchResults(deepSearchCache[query]);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
try {
|
|
562
|
+
var resp = await fetch('/api/search?q=' + encodeURIComponent(query));
|
|
563
|
+
var results = await resp.json();
|
|
564
|
+
deepSearchCache[query] = results;
|
|
565
|
+
applyDeepSearchResults(results);
|
|
566
|
+
} catch {}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function applyDeepSearchResults(results) {
|
|
570
|
+
if (!results || results.length === 0) return;
|
|
571
|
+
|
|
572
|
+
// Highlight matching session IDs in filtered list
|
|
573
|
+
var matchIds = results.map(function(r) { return r.sessionId; });
|
|
574
|
+
|
|
575
|
+
// Boost matching sessions to top if not already visible
|
|
576
|
+
var boosted = [];
|
|
577
|
+
var rest = [];
|
|
578
|
+
filteredSessions.forEach(function(s) {
|
|
579
|
+
if (matchIds.indexOf(s.id) >= 0) {
|
|
580
|
+
s._deepMatch = results.find(function(r) { return r.sessionId === s.id; });
|
|
581
|
+
boosted.push(s);
|
|
582
|
+
} else {
|
|
583
|
+
rest.push(s);
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
// Also add sessions that weren't in filteredSessions but match
|
|
588
|
+
matchIds.forEach(function(id) {
|
|
589
|
+
if (!boosted.find(function(s) { return s.id === id; }) && !rest.find(function(s) { return s.id === id; })) {
|
|
590
|
+
var s = allSessions.find(function(x) { return x.id === id; });
|
|
591
|
+
if (s) {
|
|
592
|
+
s._deepMatch = results.find(function(r) { return r.sessionId === id; });
|
|
593
|
+
boosted.push(s);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
filteredSessions = boosted.concat(rest);
|
|
599
|
+
render();
|
|
600
|
+
|
|
601
|
+
// Show deep search indicator
|
|
602
|
+
var stats = document.getElementById('stats');
|
|
603
|
+
if (stats && boosted.length > 0) {
|
|
604
|
+
stats.textContent += ' | ' + boosted.length + ' deep matches';
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
428
608
|
function onCardClick(id, event) {
|
|
429
609
|
if (selectMode) {
|
|
430
610
|
toggleSelect(id, event);
|
|
@@ -1196,6 +1376,7 @@ function dismissUpdate() {
|
|
|
1196
1376
|
loadSessions();
|
|
1197
1377
|
loadTerminals();
|
|
1198
1378
|
checkForUpdates();
|
|
1379
|
+
initHoverPreview();
|
|
1199
1380
|
|
|
1200
1381
|
// Apply saved theme
|
|
1201
1382
|
var savedTheme = localStorage.getItem('codedash-theme') || 'dark';
|
package/src/frontend/styles.css
CHANGED
|
@@ -1370,6 +1370,104 @@ body {
|
|
|
1370
1370
|
color: #fff;
|
|
1371
1371
|
}
|
|
1372
1372
|
|
|
1373
|
+
/* ── Card expand preview ────────────────────────────────────── */
|
|
1374
|
+
|
|
1375
|
+
.card-expand-btn {
|
|
1376
|
+
background: none;
|
|
1377
|
+
border: 1px solid var(--border);
|
|
1378
|
+
border-radius: 4px;
|
|
1379
|
+
color: var(--text-muted);
|
|
1380
|
+
font-size: 12px;
|
|
1381
|
+
cursor: pointer;
|
|
1382
|
+
padding: 1px 6px;
|
|
1383
|
+
margin-left: auto;
|
|
1384
|
+
transition: all 0.15s;
|
|
1385
|
+
}
|
|
1386
|
+
.card-expand-btn:hover {
|
|
1387
|
+
color: var(--text-primary);
|
|
1388
|
+
border-color: var(--accent-blue);
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
.card-preview-area {
|
|
1392
|
+
display: none;
|
|
1393
|
+
border-top: 1px solid var(--border);
|
|
1394
|
+
margin-top: 10px;
|
|
1395
|
+
padding-top: 10px;
|
|
1396
|
+
max-height: 300px;
|
|
1397
|
+
overflow-y: auto;
|
|
1398
|
+
animation: fadeIn 0.2s ease;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
.card-preview-area.open {
|
|
1402
|
+
display: block;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
@keyframes fadeIn {
|
|
1406
|
+
from { opacity: 0; transform: translateY(-4px); }
|
|
1407
|
+
to { opacity: 1; transform: translateY(0); }
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
.preview-msg {
|
|
1411
|
+
font-size: 12px;
|
|
1412
|
+
line-height: 1.5;
|
|
1413
|
+
padding: 4px 8px;
|
|
1414
|
+
margin-bottom: 4px;
|
|
1415
|
+
border-radius: 6px;
|
|
1416
|
+
word-break: break-word;
|
|
1417
|
+
white-space: pre-wrap;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
.preview-user {
|
|
1421
|
+
background: rgba(96, 165, 250, 0.08);
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
.preview-assistant {
|
|
1425
|
+
background: rgba(74, 222, 128, 0.06);
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
.preview-role {
|
|
1429
|
+
font-weight: 600;
|
|
1430
|
+
font-size: 10px;
|
|
1431
|
+
text-transform: uppercase;
|
|
1432
|
+
letter-spacing: 0.3px;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
.preview-user .preview-role { color: var(--accent-blue); }
|
|
1436
|
+
.preview-assistant .preview-role { color: var(--accent-green); }
|
|
1437
|
+
|
|
1438
|
+
.preview-empty {
|
|
1439
|
+
color: var(--text-muted);
|
|
1440
|
+
font-size: 12px;
|
|
1441
|
+
padding: 8px 0;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
/* ── Hover tooltip ──────────────────────────────────────────── */
|
|
1445
|
+
|
|
1446
|
+
.hover-tooltip {
|
|
1447
|
+
position: fixed;
|
|
1448
|
+
background: var(--bg-secondary);
|
|
1449
|
+
border: 1px solid var(--border);
|
|
1450
|
+
border-radius: 10px;
|
|
1451
|
+
padding: 12px;
|
|
1452
|
+
z-index: 150;
|
|
1453
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
|
|
1454
|
+
max-height: 280px;
|
|
1455
|
+
overflow-y: auto;
|
|
1456
|
+
opacity: 0;
|
|
1457
|
+
transform: translateY(4px);
|
|
1458
|
+
transition: opacity 0.15s, transform 0.15s;
|
|
1459
|
+
pointer-events: none;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
.hover-tooltip.visible {
|
|
1463
|
+
opacity: 1;
|
|
1464
|
+
transform: translateY(0);
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
[data-theme="light"] .hover-tooltip {
|
|
1468
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.12);
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1373
1471
|
/* ── Update banner ──────────────────────────────────────────── */
|
|
1374
1472
|
|
|
1375
1473
|
.update-banner {
|
package/src/server.js
CHANGED
|
@@ -3,7 +3,7 @@ const http = require('http');
|
|
|
3
3
|
const https = require('https');
|
|
4
4
|
const { URL } = require('url');
|
|
5
5
|
const { exec } = require('child_process');
|
|
6
|
-
const { loadSessions, loadSessionDetail, deleteSession, getGitCommits, exportSessionMarkdown } = require('./data');
|
|
6
|
+
const { loadSessions, loadSessionDetail, deleteSession, getGitCommits, exportSessionMarkdown, getSessionPreview, searchFullText } = require('./data');
|
|
7
7
|
const { detectTerminals, openInTerminal } = require('./terminals');
|
|
8
8
|
const { getHTML } = require('./html');
|
|
9
9
|
|
|
@@ -104,6 +104,23 @@ function startServer(port, openBrowser = true) {
|
|
|
104
104
|
json(res, commits);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
// ── Session preview ─────────────────────
|
|
108
|
+
else if (req.method === 'GET' && pathname.startsWith('/api/preview/')) {
|
|
109
|
+
const sessionId = pathname.split('/').pop();
|
|
110
|
+
const project = parsed.searchParams.get('project') || '';
|
|
111
|
+
const limit = parseInt(parsed.searchParams.get('limit') || '10');
|
|
112
|
+
const messages = getSessionPreview(sessionId, project, limit);
|
|
113
|
+
json(res, messages);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── Full-text search ──────────────────────
|
|
117
|
+
else if (req.method === 'GET' && pathname === '/api/search') {
|
|
118
|
+
const q = parsed.searchParams.get('q') || '';
|
|
119
|
+
const sessions = loadSessions();
|
|
120
|
+
const results = searchFullText(q, sessions);
|
|
121
|
+
json(res, results);
|
|
122
|
+
}
|
|
123
|
+
|
|
107
124
|
// ── Version check ────────────────────────
|
|
108
125
|
else if (req.method === 'GET' && pathname === '/api/version') {
|
|
109
126
|
const pkg = require('../package.json');
|