@jhizzard/termdeck 0.3.0 → 0.3.1
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/packages/cli/src/index.js +8 -0
- package/packages/client/public/app.js +471 -0
- package/packages/client/public/style.css +337 -0
- package/packages/server/src/index.js +102 -3
- package/packages/server/src/mnestra-bridge/index.js +1 -1
- package/packages/server/src/preflight.js +373 -0
- package/packages/server/src/rag.js +40 -0
- package/packages/server/src/session.js +8 -1
- package/packages/server/src/transcripts.js +290 -0
|
@@ -1432,6 +1432,343 @@
|
|
|
1432
1432
|
border-color: var(--tg-accent);
|
|
1433
1433
|
}
|
|
1434
1434
|
|
|
1435
|
+
/* ===== HEALTH BADGE (Sprint 6 T4) ===== */
|
|
1436
|
+
.health-badge {
|
|
1437
|
+
display: inline-flex;
|
|
1438
|
+
align-items: center;
|
|
1439
|
+
gap: 5px;
|
|
1440
|
+
font-family: var(--tg-mono);
|
|
1441
|
+
font-size: 11px;
|
|
1442
|
+
padding: 2px 8px;
|
|
1443
|
+
border-radius: 10px;
|
|
1444
|
+
border: 1px solid var(--tg-border);
|
|
1445
|
+
background: var(--tg-surface);
|
|
1446
|
+
color: var(--tg-text-dim);
|
|
1447
|
+
cursor: pointer;
|
|
1448
|
+
transition: all 0.15s;
|
|
1449
|
+
}
|
|
1450
|
+
.health-badge:hover {
|
|
1451
|
+
color: var(--tg-text);
|
|
1452
|
+
border-color: var(--tg-border-active);
|
|
1453
|
+
}
|
|
1454
|
+
.health-badge .hb-icon { font-size: 12px; line-height: 1; }
|
|
1455
|
+
.health-badge.hb-green {
|
|
1456
|
+
color: var(--tg-green);
|
|
1457
|
+
border-color: rgba(158, 206, 106, 0.3);
|
|
1458
|
+
}
|
|
1459
|
+
.health-badge.hb-amber {
|
|
1460
|
+
color: var(--tg-amber);
|
|
1461
|
+
border-color: var(--tg-amber);
|
|
1462
|
+
background: rgba(224, 175, 104, 0.08);
|
|
1463
|
+
animation: health-pulse 2s ease-in-out infinite;
|
|
1464
|
+
}
|
|
1465
|
+
.health-badge.hb-red {
|
|
1466
|
+
color: var(--tg-red);
|
|
1467
|
+
border-color: var(--tg-red);
|
|
1468
|
+
background: rgba(247, 118, 142, 0.08);
|
|
1469
|
+
animation: health-pulse 2s ease-in-out infinite;
|
|
1470
|
+
}
|
|
1471
|
+
@keyframes health-pulse {
|
|
1472
|
+
0%, 100% { opacity: 1; }
|
|
1473
|
+
50% { opacity: 0.6; }
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
/* Health dropdown */
|
|
1477
|
+
.health-dropdown {
|
|
1478
|
+
display: none;
|
|
1479
|
+
position: fixed;
|
|
1480
|
+
z-index: 3100;
|
|
1481
|
+
background: var(--tg-surface);
|
|
1482
|
+
border: 1px solid var(--tg-border);
|
|
1483
|
+
border-radius: var(--tg-radius);
|
|
1484
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.45);
|
|
1485
|
+
padding: 10px 14px;
|
|
1486
|
+
min-width: 300px;
|
|
1487
|
+
max-width: 420px;
|
|
1488
|
+
font-family: var(--tg-mono);
|
|
1489
|
+
font-size: 11px;
|
|
1490
|
+
}
|
|
1491
|
+
.health-dropdown.open { display: block; }
|
|
1492
|
+
.hd-check {
|
|
1493
|
+
display: grid;
|
|
1494
|
+
grid-template-columns: 16px 1fr auto auto;
|
|
1495
|
+
gap: 6px;
|
|
1496
|
+
align-items: baseline;
|
|
1497
|
+
padding: 5px 0;
|
|
1498
|
+
border-bottom: 1px solid var(--tg-border);
|
|
1499
|
+
}
|
|
1500
|
+
.hd-check:last-child { border-bottom: none; }
|
|
1501
|
+
.hd-icon { text-align: center; font-weight: 700; }
|
|
1502
|
+
.hd-ok .hd-icon { color: var(--tg-green); }
|
|
1503
|
+
.hd-fail .hd-icon { color: var(--tg-red); }
|
|
1504
|
+
.hd-name { color: var(--tg-text); }
|
|
1505
|
+
.hd-dots {
|
|
1506
|
+
border-bottom: 1px dotted var(--tg-border);
|
|
1507
|
+
min-width: 20px;
|
|
1508
|
+
align-self: end;
|
|
1509
|
+
margin-bottom: 3px;
|
|
1510
|
+
}
|
|
1511
|
+
.hd-status { font-weight: 600; }
|
|
1512
|
+
.hd-ok .hd-status { color: var(--tg-green); }
|
|
1513
|
+
.hd-fail .hd-status { color: var(--tg-red); }
|
|
1514
|
+
.hd-detail {
|
|
1515
|
+
grid-column: 2 / -1;
|
|
1516
|
+
color: var(--tg-text-dim);
|
|
1517
|
+
font-size: 10px;
|
|
1518
|
+
}
|
|
1519
|
+
.hd-detail:empty { display: none; }
|
|
1520
|
+
.hd-remediation {
|
|
1521
|
+
grid-column: 2 / -1;
|
|
1522
|
+
color: var(--tg-amber);
|
|
1523
|
+
font-size: 10px;
|
|
1524
|
+
padding: 2px 6px;
|
|
1525
|
+
background: rgba(224, 175, 104, 0.08);
|
|
1526
|
+
border-radius: 3px;
|
|
1527
|
+
margin-top: 2px;
|
|
1528
|
+
}
|
|
1529
|
+
.hd-loading, .hd-empty {
|
|
1530
|
+
color: var(--tg-text-dim);
|
|
1531
|
+
padding: 8px 0;
|
|
1532
|
+
text-align: center;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
/* ===== TRANSCRIPT MODAL (Sprint 6 T4) ===== */
|
|
1536
|
+
.transcript-modal {
|
|
1537
|
+
display: none;
|
|
1538
|
+
position: fixed;
|
|
1539
|
+
inset: 0;
|
|
1540
|
+
z-index: 3000;
|
|
1541
|
+
align-items: center;
|
|
1542
|
+
justify-content: center;
|
|
1543
|
+
}
|
|
1544
|
+
.transcript-modal.open { display: flex; }
|
|
1545
|
+
.transcript-backdrop {
|
|
1546
|
+
position: absolute;
|
|
1547
|
+
inset: 0;
|
|
1548
|
+
background: rgba(0, 0, 0, 0.72);
|
|
1549
|
+
}
|
|
1550
|
+
.transcript-card {
|
|
1551
|
+
position: relative;
|
|
1552
|
+
background: var(--tg-surface);
|
|
1553
|
+
border: 1px solid var(--tg-accent-dim);
|
|
1554
|
+
border-radius: 10px;
|
|
1555
|
+
width: 800px;
|
|
1556
|
+
max-width: calc(100vw - 40px);
|
|
1557
|
+
max-height: calc(100vh - 80px);
|
|
1558
|
+
display: flex;
|
|
1559
|
+
flex-direction: column;
|
|
1560
|
+
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.55);
|
|
1561
|
+
font-family: var(--tg-sans);
|
|
1562
|
+
color: var(--tg-text);
|
|
1563
|
+
}
|
|
1564
|
+
.transcript-card header {
|
|
1565
|
+
padding: 18px 22px 10px;
|
|
1566
|
+
border-bottom: 1px solid var(--tg-border);
|
|
1567
|
+
display: flex;
|
|
1568
|
+
align-items: center;
|
|
1569
|
+
justify-content: space-between;
|
|
1570
|
+
}
|
|
1571
|
+
.transcript-card header h3 {
|
|
1572
|
+
margin: 0;
|
|
1573
|
+
font-size: 15px;
|
|
1574
|
+
color: var(--tg-accent);
|
|
1575
|
+
}
|
|
1576
|
+
.transcript-tabs {
|
|
1577
|
+
display: flex;
|
|
1578
|
+
gap: 4px;
|
|
1579
|
+
}
|
|
1580
|
+
.transcript-tab {
|
|
1581
|
+
background: none;
|
|
1582
|
+
border: 1px solid var(--tg-border);
|
|
1583
|
+
color: var(--tg-text-dim);
|
|
1584
|
+
font-size: 11px;
|
|
1585
|
+
padding: 4px 12px;
|
|
1586
|
+
border-radius: 3px;
|
|
1587
|
+
cursor: pointer;
|
|
1588
|
+
font-family: var(--tg-sans);
|
|
1589
|
+
transition: all 0.1s;
|
|
1590
|
+
}
|
|
1591
|
+
.transcript-tab:hover { color: var(--tg-text); border-color: var(--tg-border-active); }
|
|
1592
|
+
.transcript-tab.active {
|
|
1593
|
+
color: var(--tg-accent);
|
|
1594
|
+
border-color: var(--tg-accent-dim);
|
|
1595
|
+
background: var(--tg-bg);
|
|
1596
|
+
}
|
|
1597
|
+
.transcript-search-bar {
|
|
1598
|
+
padding: 10px 22px;
|
|
1599
|
+
border-bottom: 1px solid var(--tg-border);
|
|
1600
|
+
}
|
|
1601
|
+
.transcript-search-bar .ctrl-input {
|
|
1602
|
+
width: 100%;
|
|
1603
|
+
}
|
|
1604
|
+
.transcript-body {
|
|
1605
|
+
flex: 1;
|
|
1606
|
+
overflow-y: auto;
|
|
1607
|
+
padding: 12px 22px;
|
|
1608
|
+
min-height: 200px;
|
|
1609
|
+
max-height: 60vh;
|
|
1610
|
+
}
|
|
1611
|
+
.transcript-loading, .transcript-empty {
|
|
1612
|
+
color: var(--tg-text-dim);
|
|
1613
|
+
font-size: 12px;
|
|
1614
|
+
text-align: center;
|
|
1615
|
+
padding: 40px 0;
|
|
1616
|
+
font-family: var(--tg-mono);
|
|
1617
|
+
}
|
|
1618
|
+
.transcript-card footer {
|
|
1619
|
+
padding: 10px 22px 14px;
|
|
1620
|
+
border-top: 1px solid var(--tg-border);
|
|
1621
|
+
display: flex;
|
|
1622
|
+
justify-content: space-between;
|
|
1623
|
+
align-items: center;
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
/* Transcript session cards (recent view) */
|
|
1627
|
+
.transcript-session {
|
|
1628
|
+
padding: 10px 12px;
|
|
1629
|
+
border: 1px solid var(--tg-border);
|
|
1630
|
+
border-radius: var(--tg-radius-sm);
|
|
1631
|
+
margin-bottom: 8px;
|
|
1632
|
+
cursor: pointer;
|
|
1633
|
+
transition: border-color 0.15s;
|
|
1634
|
+
}
|
|
1635
|
+
.transcript-session:hover { border-color: var(--tg-accent-dim); }
|
|
1636
|
+
.ts-header {
|
|
1637
|
+
display: flex;
|
|
1638
|
+
align-items: center;
|
|
1639
|
+
gap: 8px;
|
|
1640
|
+
margin-bottom: 6px;
|
|
1641
|
+
font-size: 11px;
|
|
1642
|
+
}
|
|
1643
|
+
.ts-id {
|
|
1644
|
+
font-family: var(--tg-mono);
|
|
1645
|
+
color: var(--tg-accent);
|
|
1646
|
+
font-weight: 600;
|
|
1647
|
+
}
|
|
1648
|
+
.ts-type {
|
|
1649
|
+
color: var(--tg-text-dim);
|
|
1650
|
+
font-family: var(--tg-mono);
|
|
1651
|
+
}
|
|
1652
|
+
.ts-project {
|
|
1653
|
+
padding: 1px 6px;
|
|
1654
|
+
border-radius: 3px;
|
|
1655
|
+
background: var(--tg-bg);
|
|
1656
|
+
border: 1px solid var(--tg-accent-dim);
|
|
1657
|
+
color: var(--tg-accent);
|
|
1658
|
+
font-size: 10px;
|
|
1659
|
+
}
|
|
1660
|
+
.ts-lines {
|
|
1661
|
+
color: var(--tg-text-dim);
|
|
1662
|
+
font-size: 10px;
|
|
1663
|
+
margin-left: auto;
|
|
1664
|
+
}
|
|
1665
|
+
.ts-preview {
|
|
1666
|
+
font-family: var(--tg-mono);
|
|
1667
|
+
font-size: 11px;
|
|
1668
|
+
color: var(--tg-text-dim);
|
|
1669
|
+
background: var(--tg-bg);
|
|
1670
|
+
padding: 6px 8px;
|
|
1671
|
+
border-radius: 3px;
|
|
1672
|
+
margin: 0;
|
|
1673
|
+
max-height: 80px;
|
|
1674
|
+
overflow: hidden;
|
|
1675
|
+
white-space: pre-wrap;
|
|
1676
|
+
word-break: break-word;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
/* Transcript search results */
|
|
1680
|
+
.transcript-result {
|
|
1681
|
+
padding: 8px 10px;
|
|
1682
|
+
border: 1px solid var(--tg-border);
|
|
1683
|
+
border-radius: var(--tg-radius-sm);
|
|
1684
|
+
margin-bottom: 6px;
|
|
1685
|
+
cursor: pointer;
|
|
1686
|
+
transition: border-color 0.15s;
|
|
1687
|
+
}
|
|
1688
|
+
.transcript-result:hover { border-color: var(--tg-accent-dim); }
|
|
1689
|
+
.tr-meta {
|
|
1690
|
+
display: flex;
|
|
1691
|
+
gap: 8px;
|
|
1692
|
+
font-size: 10px;
|
|
1693
|
+
color: var(--tg-text-dim);
|
|
1694
|
+
margin-bottom: 4px;
|
|
1695
|
+
}
|
|
1696
|
+
.tr-session { font-family: var(--tg-mono); color: var(--tg-accent); }
|
|
1697
|
+
.tr-time { font-family: var(--tg-mono); }
|
|
1698
|
+
.tr-line {
|
|
1699
|
+
font-family: var(--tg-mono);
|
|
1700
|
+
font-size: 11px;
|
|
1701
|
+
color: var(--tg-text);
|
|
1702
|
+
margin: 0;
|
|
1703
|
+
white-space: pre-wrap;
|
|
1704
|
+
word-break: break-word;
|
|
1705
|
+
}
|
|
1706
|
+
mark.tr-highlight {
|
|
1707
|
+
background: rgba(224, 175, 104, 0.25);
|
|
1708
|
+
color: var(--tg-amber);
|
|
1709
|
+
border-radius: 2px;
|
|
1710
|
+
padding: 0 1px;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
/* Transcript replay view */
|
|
1714
|
+
.transcript-replay-header {
|
|
1715
|
+
display: flex;
|
|
1716
|
+
align-items: center;
|
|
1717
|
+
justify-content: space-between;
|
|
1718
|
+
margin-bottom: 10px;
|
|
1719
|
+
}
|
|
1720
|
+
.tr-replay-id {
|
|
1721
|
+
font-family: var(--tg-mono);
|
|
1722
|
+
font-size: 12px;
|
|
1723
|
+
color: var(--tg-accent);
|
|
1724
|
+
}
|
|
1725
|
+
.transcript-copy {
|
|
1726
|
+
background: none;
|
|
1727
|
+
border: 1px solid var(--tg-border);
|
|
1728
|
+
color: var(--tg-text-dim);
|
|
1729
|
+
font-family: var(--tg-mono);
|
|
1730
|
+
font-size: 11px;
|
|
1731
|
+
padding: 4px 10px;
|
|
1732
|
+
border-radius: 3px;
|
|
1733
|
+
cursor: pointer;
|
|
1734
|
+
transition: all 0.15s;
|
|
1735
|
+
}
|
|
1736
|
+
.transcript-copy:hover {
|
|
1737
|
+
color: var(--tg-text);
|
|
1738
|
+
border-color: var(--tg-border-active);
|
|
1739
|
+
}
|
|
1740
|
+
.transcript-copy.copied {
|
|
1741
|
+
color: var(--tg-green);
|
|
1742
|
+
border-color: var(--tg-green);
|
|
1743
|
+
}
|
|
1744
|
+
.transcript-replay-content {
|
|
1745
|
+
font-family: var(--tg-mono);
|
|
1746
|
+
font-size: 11px;
|
|
1747
|
+
color: var(--tg-text);
|
|
1748
|
+
background: var(--tg-bg);
|
|
1749
|
+
padding: 12px 14px;
|
|
1750
|
+
border-radius: var(--tg-radius-sm);
|
|
1751
|
+
margin: 0;
|
|
1752
|
+
white-space: pre-wrap;
|
|
1753
|
+
word-break: break-word;
|
|
1754
|
+
max-height: 50vh;
|
|
1755
|
+
overflow-y: auto;
|
|
1756
|
+
}
|
|
1757
|
+
.transcript-back {
|
|
1758
|
+
background: none;
|
|
1759
|
+
border: 1px solid var(--tg-border);
|
|
1760
|
+
color: var(--tg-text-dim);
|
|
1761
|
+
font-family: var(--tg-sans);
|
|
1762
|
+
font-size: 11px;
|
|
1763
|
+
padding: 5px 14px;
|
|
1764
|
+
border-radius: 3px;
|
|
1765
|
+
cursor: pointer;
|
|
1766
|
+
}
|
|
1767
|
+
.transcript-back:hover {
|
|
1768
|
+
color: var(--tg-text);
|
|
1769
|
+
border-color: var(--tg-border-active);
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1435
1772
|
/* ===== SCROLLBAR ===== */
|
|
1436
1773
|
::-webkit-scrollbar { width: 6px; }
|
|
1437
1774
|
::-webkit-scrollbar-track { background: transparent; }
|
|
@@ -47,6 +47,8 @@ const { initDatabase, logCommand, getSessionHistory, getProjectSessions } = requ
|
|
|
47
47
|
const { RAGIntegration } = require('./rag');
|
|
48
48
|
const { createBridge } = require('./mnestra-bridge');
|
|
49
49
|
const { writeSessionLog } = require('./session-logger');
|
|
50
|
+
const { TranscriptWriter } = require('./transcripts');
|
|
51
|
+
const { createHealthHandler } = require('./preflight');
|
|
50
52
|
const { themes, statusColors } = require('./themes');
|
|
51
53
|
const { loadConfig, addProject } = require('./config');
|
|
52
54
|
|
|
@@ -87,12 +89,33 @@ function createServer(config) {
|
|
|
87
89
|
const mnestraBridge = createBridge(config);
|
|
88
90
|
console.log(`[mnestra-bridge] mode=${mnestraBridge.mode}`);
|
|
89
91
|
|
|
92
|
+
// Initialize transcript writer (Session Transcripts — Sprint 6)
|
|
93
|
+
const transcriptConfig = config.transcripts || {};
|
|
94
|
+
const transcriptEnabled = transcriptConfig.enabled !== undefined
|
|
95
|
+
? transcriptConfig.enabled
|
|
96
|
+
: !!process.env.DATABASE_URL;
|
|
97
|
+
let transcriptWriter = null;
|
|
98
|
+
if (transcriptEnabled && process.env.DATABASE_URL) {
|
|
99
|
+
transcriptWriter = new TranscriptWriter(process.env.DATABASE_URL, {
|
|
100
|
+
batchSize: transcriptConfig.batchSize || 50,
|
|
101
|
+
flushIntervalMs: transcriptConfig.flushIntervalMs || 2000,
|
|
102
|
+
enabled: true
|
|
103
|
+
});
|
|
104
|
+
console.log('[transcript] Writer initialized (flush every %dms, batch %d)',
|
|
105
|
+
transcriptConfig.flushIntervalMs || 2000, transcriptConfig.batchSize || 50);
|
|
106
|
+
} else {
|
|
107
|
+
console.log('[transcript] Writer disabled (no DATABASE_URL or transcripts.enabled=false)');
|
|
108
|
+
}
|
|
109
|
+
|
|
90
110
|
// Wire RAG to session events
|
|
91
111
|
sessions.on('session:created', (s) => rag.onSessionCreated(s));
|
|
92
112
|
sessions.on('session:removed', (s) => rag.onSessionEnded(s));
|
|
93
113
|
|
|
94
114
|
// ==================== REST API ====================
|
|
95
115
|
|
|
116
|
+
// GET /api/health - preflight health checks (Sprint 6 T1, wired by T3)
|
|
117
|
+
app.get('/api/health', createHealthHandler(config));
|
|
118
|
+
|
|
96
119
|
// GET /api/sessions - list all active sessions
|
|
97
120
|
app.get('/api/sessions', (req, res) => {
|
|
98
121
|
res.json(sessions.getAll());
|
|
@@ -157,7 +180,7 @@ function createServer(config) {
|
|
|
157
180
|
session.pid = term.pid;
|
|
158
181
|
session.meta.status = 'active';
|
|
159
182
|
|
|
160
|
-
// PTY output → analyze + broadcast to WebSocket
|
|
183
|
+
// PTY output → analyze + broadcast to WebSocket + transcript archive
|
|
161
184
|
term.onData((data) => {
|
|
162
185
|
session.analyzeOutput(data);
|
|
163
186
|
|
|
@@ -165,6 +188,15 @@ function createServer(config) {
|
|
|
165
188
|
if (session.ws && session.ws.readyState === 1) {
|
|
166
189
|
session.ws.send(JSON.stringify({ type: 'output', data }));
|
|
167
190
|
}
|
|
191
|
+
|
|
192
|
+
// Archive to transcript writer (non-blocking, failure-safe)
|
|
193
|
+
if (transcriptWriter) {
|
|
194
|
+
try {
|
|
195
|
+
transcriptWriter.append(session.id, data, Buffer.byteLength(data, 'utf8'));
|
|
196
|
+
} catch (err) {
|
|
197
|
+
// Never let transcript failures disrupt the PTY data path
|
|
198
|
+
}
|
|
199
|
+
}
|
|
168
200
|
});
|
|
169
201
|
|
|
170
202
|
term.onExit(({ exitCode, signal }) => {
|
|
@@ -447,6 +479,53 @@ function createServer(config) {
|
|
|
447
479
|
});
|
|
448
480
|
});
|
|
449
481
|
|
|
482
|
+
// ==================== Transcript endpoints (Sprint 6 T3) ====================
|
|
483
|
+
|
|
484
|
+
// GET /api/transcripts/search - FTS across all sessions
|
|
485
|
+
// (Must be registered before :sessionId to avoid route collision)
|
|
486
|
+
app.get('/api/transcripts/search', async (req, res) => {
|
|
487
|
+
if (!transcriptWriter) return res.json([]);
|
|
488
|
+
const q = req.query.q;
|
|
489
|
+
if (!q) return res.status(400).json({ error: 'Missing q parameter' });
|
|
490
|
+
const since = req.query.since || null;
|
|
491
|
+
const limit = Math.min(Math.max(parseInt(req.query.limit) || 50, 1), 200);
|
|
492
|
+
try {
|
|
493
|
+
const results = await transcriptWriter.search(q, { since, limit });
|
|
494
|
+
res.json(results);
|
|
495
|
+
} catch (err) {
|
|
496
|
+
console.error('[transcript] search endpoint error:', err.message);
|
|
497
|
+
res.status(500).json({ error: 'Transcript search failed' });
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
// GET /api/transcripts/recent - time-windowed crash recovery
|
|
502
|
+
app.get('/api/transcripts/recent', async (req, res) => {
|
|
503
|
+
if (!transcriptWriter) return res.json([]);
|
|
504
|
+
const minutes = Math.min(Math.max(parseInt(req.query.minutes) || 60, 1), 1440);
|
|
505
|
+
const limit = Math.min(Math.max(parseInt(req.query.limit) || 500, 1), 2000);
|
|
506
|
+
try {
|
|
507
|
+
const results = await transcriptWriter.getRecent(minutes, limit);
|
|
508
|
+
res.json(results);
|
|
509
|
+
} catch (err) {
|
|
510
|
+
console.error('[transcript] recent endpoint error:', err.message);
|
|
511
|
+
res.status(500).json({ error: 'Transcript recent query failed' });
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
// GET /api/transcripts/:sessionId - ordered chunks for a session
|
|
516
|
+
app.get('/api/transcripts/:sessionId', async (req, res) => {
|
|
517
|
+
if (!transcriptWriter) return res.json([]);
|
|
518
|
+
const limit = req.query.limit ? Math.min(Math.max(parseInt(req.query.limit), 1), 5000) : undefined;
|
|
519
|
+
const since = req.query.since || undefined;
|
|
520
|
+
try {
|
|
521
|
+
const chunks = await transcriptWriter.getSessionTranscript(req.params.sessionId, { limit, since });
|
|
522
|
+
res.json(chunks);
|
|
523
|
+
} catch (err) {
|
|
524
|
+
console.error('[transcript] session transcript endpoint error:', err.message);
|
|
525
|
+
res.status(500).json({ error: 'Transcript retrieval failed' });
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
|
|
450
529
|
// ==================== Rumen insights (Sprint 4 T2) ====================
|
|
451
530
|
// Read-only access to rumen_insights + rumen_jobs in the petvetbid Postgres
|
|
452
531
|
// instance. Contract frozen in docs/sprint-4-rumen-integration/API-CONTRACT.md.
|
|
@@ -717,7 +796,7 @@ function createServer(config) {
|
|
|
717
796
|
res.sendFile(path.join(clientDir, 'index.html'));
|
|
718
797
|
});
|
|
719
798
|
|
|
720
|
-
return { app, server, wss, sessions, rag, db };
|
|
799
|
+
return { app, server, wss, sessions, rag, db, transcriptWriter };
|
|
721
800
|
}
|
|
722
801
|
|
|
723
802
|
// Start server
|
|
@@ -733,10 +812,29 @@ if (require.main === module) {
|
|
|
733
812
|
config.sessionLogs = { ...(config.sessionLogs || {}), enabled: true };
|
|
734
813
|
}
|
|
735
814
|
|
|
736
|
-
const { server } = createServer(config);
|
|
815
|
+
const { server, transcriptWriter } = createServer(config);
|
|
737
816
|
const port = config.port || 3000;
|
|
738
817
|
const host = config.host || '127.0.0.1';
|
|
739
818
|
|
|
819
|
+
// Graceful shutdown — flush transcript buffer before exit
|
|
820
|
+
let shutdownInProgress = false;
|
|
821
|
+
async function handleShutdown(signal) {
|
|
822
|
+
if (shutdownInProgress) return;
|
|
823
|
+
shutdownInProgress = true;
|
|
824
|
+
console.log(`\n[server] ${signal} received, shutting down...`);
|
|
825
|
+
if (transcriptWriter) {
|
|
826
|
+
console.log('[transcript] Flushing buffer before exit...');
|
|
827
|
+
try { await transcriptWriter.close(); } catch (err) {
|
|
828
|
+
console.error('[transcript] Shutdown flush failed:', err.message);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
server.close(() => process.exit(0));
|
|
832
|
+
// Force exit after 5s if server.close hangs
|
|
833
|
+
setTimeout(() => process.exit(1), 5000).unref();
|
|
834
|
+
}
|
|
835
|
+
process.on('SIGINT', () => handleShutdown('SIGINT'));
|
|
836
|
+
process.on('SIGTERM', () => handleShutdown('SIGTERM'));
|
|
837
|
+
|
|
740
838
|
server.listen(port, host, () => {
|
|
741
839
|
console.log(`\n TermDeck running at http://${host}:${port}\n`);
|
|
742
840
|
console.log(` Terminals: 0 active`);
|
|
@@ -744,6 +842,7 @@ if (require.main === module) {
|
|
|
744
842
|
console.log(` PTY: ${pty ? 'node-pty OK' : 'unavailable (install node-pty)'}`);
|
|
745
843
|
console.log(` RAG: ${config.rag?.supabaseUrl ? 'configured' : 'not configured'}`);
|
|
746
844
|
console.log(` Session logs: ${config.sessionLogs?.enabled ? '~/.termdeck/sessions/ (on exit)' : 'off'}`);
|
|
845
|
+
console.log(` Transcripts: ${transcriptWriter ? 'streaming to Supabase' : 'off (no DATABASE_URL)'}`);
|
|
747
846
|
console.log(`\n WARNING: TermDeck binds to ${host} only.`);
|
|
748
847
|
console.log(` Do NOT expose this to the network without authentication.`);
|
|
749
848
|
console.log(` Terminal sessions have full shell access.\n`);
|
|
@@ -207,7 +207,7 @@ function createBridge(config) {
|
|
|
207
207
|
} catch (err) {
|
|
208
208
|
// Kill child so it respawns next call
|
|
209
209
|
if (state.mcpChild) {
|
|
210
|
-
try { state.mcpChild.kill(); } catch {}
|
|
210
|
+
try { state.mcpChild.kill(); } catch (err) { /* process may already be dead */ }
|
|
211
211
|
state.mcpChild = null;
|
|
212
212
|
}
|
|
213
213
|
throw err;
|