watercooler 0.0.7 → 0.0.9
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/public/app.js +165 -43
- package/public/index.html +56 -1
- package/server.ts +22 -22
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -9,7 +9,7 @@ let config = { user: '', mailbox: '', avatar: null };
|
|
|
9
9
|
let messages = []; // Messages TO user (for main panel)
|
|
10
10
|
let allMessages = []; // All messages involving user (for desk dialogs)
|
|
11
11
|
let recipients = [];
|
|
12
|
-
let
|
|
12
|
+
let statusStates = {}; // Map of name -> {tool_name, timestamp}
|
|
13
13
|
let scene, camera, renderer, controls;
|
|
14
14
|
let agentMeshes = new Map();
|
|
15
15
|
let connectionLines = [];
|
|
@@ -980,14 +980,9 @@ function updateVillage() {
|
|
|
980
980
|
clearConnections();
|
|
981
981
|
|
|
982
982
|
// Use recipients (from coworkers.db) as the authoritative list of agents
|
|
983
|
+
// Only show people in the coworker list, not random message senders
|
|
983
984
|
const allAgents = new Set([config.user.toLowerCase(), ...recipients.map(r => r.toLowerCase())]);
|
|
984
985
|
|
|
985
|
-
// Also add message participants
|
|
986
|
-
messages.forEach(m => {
|
|
987
|
-
allAgents.add(m.sender.toLowerCase());
|
|
988
|
-
allAgents.add(m.recipient.toLowerCase());
|
|
989
|
-
});
|
|
990
|
-
|
|
991
986
|
// Arrange agents in a circle on the platform
|
|
992
987
|
const agents = Array.from(allAgents);
|
|
993
988
|
const radius = Math.min(20, Math.max(10, agents.length * 3));
|
|
@@ -998,8 +993,8 @@ function updateVillage() {
|
|
|
998
993
|
const z = Math.sin(angle) * radius;
|
|
999
994
|
const position = new THREE.Vector3(x, 0, z);
|
|
1000
995
|
|
|
1001
|
-
const
|
|
1002
|
-
const toolName =
|
|
996
|
+
const statusState = statusStates[agent.toLowerCase()];
|
|
997
|
+
const toolName = statusState?.tool_name || null;
|
|
1003
998
|
|
|
1004
999
|
if (!agentMeshes.has(agent)) {
|
|
1005
1000
|
const group = createAgentDesk(agent, position, toolName);
|
|
@@ -1163,14 +1158,14 @@ async function loadData() {
|
|
|
1163
1158
|
recipients = Array.isArray(recipientsData) ? recipientsData : [];
|
|
1164
1159
|
allMessages = Array.isArray(allMessagesData) ? allMessagesData : [];
|
|
1165
1160
|
|
|
1166
|
-
// Load
|
|
1167
|
-
if (config.
|
|
1161
|
+
// Load status states if status DB is configured
|
|
1162
|
+
if (config.status) {
|
|
1168
1163
|
try {
|
|
1169
|
-
const
|
|
1170
|
-
|
|
1164
|
+
const statusRes = await fetch('/api/status');
|
|
1165
|
+
statusStates = await statusRes.json();
|
|
1171
1166
|
} catch (err) {
|
|
1172
|
-
console.error('Error loading
|
|
1173
|
-
|
|
1167
|
+
console.error('Error loading status states:', err);
|
|
1168
|
+
statusStates = {};
|
|
1174
1169
|
}
|
|
1175
1170
|
}
|
|
1176
1171
|
|
|
@@ -1212,7 +1207,9 @@ function updateUI() {
|
|
|
1212
1207
|
// Update recipient select (send panel) - only from coworkers.db
|
|
1213
1208
|
const select = document.getElementById('recipient-select');
|
|
1214
1209
|
const currentVal = select.value;
|
|
1210
|
+
const everyoneOption = recipients.length > 0 ? '<option value="@everyone" style="font-weight: bold; color: #5EEAD4;">@everyone (broadcast to all)</option>' : '';
|
|
1215
1211
|
select.innerHTML = '<option value="">Coworker...</option>' +
|
|
1212
|
+
everyoneOption +
|
|
1216
1213
|
recipients.sort().map(r =>
|
|
1217
1214
|
`<option value="${r}" ${r === currentVal ? 'selected' : ''}>${r}</option>`
|
|
1218
1215
|
).join('');
|
|
@@ -1227,15 +1224,7 @@ function updateUI() {
|
|
|
1227
1224
|
</div>
|
|
1228
1225
|
`;
|
|
1229
1226
|
} else {
|
|
1230
|
-
messagesDiv.innerHTML = messages.slice(0, 20).map(msg =>
|
|
1231
|
-
<div class="message-card ${msg.read ? '' : 'unread'}" data-id="${msg.id}" data-sender="${msg.sender}" data-recipient="${msg.recipient}">
|
|
1232
|
-
<div class="message-header">
|
|
1233
|
-
<span class="message-sender">${msg.sender} → ${msg.recipient}</span>
|
|
1234
|
-
<span class="message-time">${new Date(msg.timestamp).toLocaleString()}</span>
|
|
1235
|
-
</div>
|
|
1236
|
-
<div class="message-text">${marked.parse(msg.message)}</div>
|
|
1237
|
-
</div>
|
|
1238
|
-
`).join('');
|
|
1227
|
+
messagesDiv.innerHTML = messages.slice(0, 20).map(msg => renderMessageCard(msg, true)).join('');
|
|
1239
1228
|
|
|
1240
1229
|
// Add click handlers for all messages (clicking marks as read and sets recipient for reply)
|
|
1241
1230
|
messagesDiv.querySelectorAll('.message-card').forEach(el => {
|
|
@@ -1311,25 +1300,49 @@ async function sendMessage() {
|
|
|
1311
1300
|
}
|
|
1312
1301
|
|
|
1313
1302
|
try {
|
|
1314
|
-
|
|
1315
|
-
method: 'POST',
|
|
1316
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1317
|
-
body: JSON.stringify({ to, from: config.user, message })
|
|
1318
|
-
});
|
|
1303
|
+
let sendPromises;
|
|
1319
1304
|
|
|
1320
|
-
if (
|
|
1305
|
+
if (to === '@everyone') {
|
|
1306
|
+
// Send to all recipients individually
|
|
1307
|
+
sendPromises = recipients.map(recipient =>
|
|
1308
|
+
fetch('/api/send', {
|
|
1309
|
+
method: 'POST',
|
|
1310
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1311
|
+
body: JSON.stringify({ to: recipient, from: config.user, message })
|
|
1312
|
+
})
|
|
1313
|
+
);
|
|
1314
|
+
} else {
|
|
1315
|
+
// Send to single recipient
|
|
1316
|
+
sendPromises = [fetch('/api/send', {
|
|
1317
|
+
method: 'POST',
|
|
1318
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1319
|
+
body: JSON.stringify({ to, from: config.user, message })
|
|
1320
|
+
})];
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
const responses = await Promise.all(sendPromises);
|
|
1324
|
+
const allSuccessful = responses.every(r => r.ok);
|
|
1325
|
+
|
|
1326
|
+
if (allSuccessful) {
|
|
1321
1327
|
// Clear input
|
|
1322
1328
|
document.getElementById('message-input').value = '';
|
|
1323
1329
|
|
|
1324
1330
|
// Show toast
|
|
1325
1331
|
const toast = document.getElementById('toast');
|
|
1332
|
+
const toastMsg = document.getElementById('toast-message');
|
|
1333
|
+
if (toastMsg && to === '@everyone') {
|
|
1334
|
+
toastMsg.textContent = `Message broadcast to ${recipients.length} coworkers!`;
|
|
1335
|
+
}
|
|
1326
1336
|
toast.classList.add('show');
|
|
1327
|
-
setTimeout(() =>
|
|
1337
|
+
setTimeout(() => {
|
|
1338
|
+
toast.classList.remove('show');
|
|
1339
|
+
if (toastMsg) toastMsg.textContent = 'Message sent!';
|
|
1340
|
+
}, 3000);
|
|
1328
1341
|
|
|
1329
1342
|
// Reload data
|
|
1330
1343
|
loadData();
|
|
1331
1344
|
} else {
|
|
1332
|
-
alert('Failed to send message');
|
|
1345
|
+
alert('Failed to send message to some recipients');
|
|
1333
1346
|
}
|
|
1334
1347
|
} catch (err) {
|
|
1335
1348
|
console.error('Error sending:', err);
|
|
@@ -1496,15 +1509,7 @@ function updateDeskDialogContent() {
|
|
|
1496
1509
|
</div>
|
|
1497
1510
|
`;
|
|
1498
1511
|
} else {
|
|
1499
|
-
content.innerHTML = filteredMessages.map(msg =>
|
|
1500
|
-
<div class="message-card ${msg.read ? '' : 'unread'}" style="margin-bottom: 12px;">
|
|
1501
|
-
<div class="message-header">
|
|
1502
|
-
<span class="message-sender">${msg.sender} → ${msg.recipient}</span>
|
|
1503
|
-
<span class="message-time">${new Date(msg.timestamp).toLocaleString()}</span>
|
|
1504
|
-
</div>
|
|
1505
|
-
<div class="message-text">${marked.parse(msg.message)}</div>
|
|
1506
|
-
</div>
|
|
1507
|
-
`).join('');
|
|
1512
|
+
content.innerHTML = filteredMessages.map(msg => renderMessageCard(msg, true)).join('');
|
|
1508
1513
|
}
|
|
1509
1514
|
}
|
|
1510
1515
|
|
|
@@ -1527,8 +1532,8 @@ function updateDeskLabels() {
|
|
|
1527
1532
|
!m.read
|
|
1528
1533
|
).length;
|
|
1529
1534
|
|
|
1530
|
-
const
|
|
1531
|
-
const toolName =
|
|
1535
|
+
const statusState = statusStates[name.toLowerCase()];
|
|
1536
|
+
const toolName = statusState?.tool_name || null;
|
|
1532
1537
|
|
|
1533
1538
|
const sprite = group.children.find(c => c.isSprite);
|
|
1534
1539
|
if (sprite) {
|
|
@@ -1591,6 +1596,123 @@ function updateDeskLabels() {
|
|
|
1591
1596
|
});
|
|
1592
1597
|
}
|
|
1593
1598
|
|
|
1599
|
+
// Parse markdown frontmatter from message text
|
|
1600
|
+
function parseFrontmatter(text) {
|
|
1601
|
+
const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n?/;
|
|
1602
|
+
const match = text.match(frontmatterRegex);
|
|
1603
|
+
|
|
1604
|
+
if (!match) {
|
|
1605
|
+
return { content: text, frontmatter: null };
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
const frontmatterText = match[1];
|
|
1609
|
+
const content = text.slice(match[0].length).trim();
|
|
1610
|
+
|
|
1611
|
+
// Simple YAML-like parsing
|
|
1612
|
+
const frontmatter = {};
|
|
1613
|
+
const lines = frontmatterText.split('\n');
|
|
1614
|
+
|
|
1615
|
+
for (const line of lines) {
|
|
1616
|
+
const colonIndex = line.indexOf(':');
|
|
1617
|
+
if (colonIndex > 0) {
|
|
1618
|
+
const key = line.slice(0, colonIndex).trim();
|
|
1619
|
+
let value = line.slice(colonIndex + 1).trim();
|
|
1620
|
+
|
|
1621
|
+
// Handle arrays: choices: ["option1", "option2"]
|
|
1622
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
1623
|
+
try {
|
|
1624
|
+
value = JSON.parse(value.replace(/'/g, '"'));
|
|
1625
|
+
} catch {
|
|
1626
|
+
// Fallback: parse as comma-separated
|
|
1627
|
+
value = value.slice(1, -1).split(',').map(s => s.trim().replace(/^["']|["']$/g, ''));
|
|
1628
|
+
}
|
|
1629
|
+
} else if (value.startsWith('- ')) {
|
|
1630
|
+
// YAML array format with dashes - collect all consecutive dash items
|
|
1631
|
+
// This is handled below by checking the whole frontmatter
|
|
1632
|
+
} else {
|
|
1633
|
+
// Remove quotes if present
|
|
1634
|
+
value = value.replace(/^["']|["']$/g, '');
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
frontmatter[key] = value;
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
// Handle YAML array format: choices:\n - option1\n - option2
|
|
1642
|
+
const choicesMatch = frontmatterText.match(/choices:\s*\n((?:\s*-\s*[^\n]+\n?)+)/);
|
|
1643
|
+
if (choicesMatch) {
|
|
1644
|
+
const choicesLines = choicesMatch[1].trim().split('\n');
|
|
1645
|
+
frontmatter.choices = choicesLines
|
|
1646
|
+
.map(line => line.replace(/^\s*-\s*/, '').trim())
|
|
1647
|
+
.filter(line => line)
|
|
1648
|
+
.map(choice => choice.replace(/^["']|["']$/g, ''));
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
return { content, frontmatter };
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
// Send a quick response from a choice button
|
|
1655
|
+
window.sendQuickResponse = async function(to, message, messageId) {
|
|
1656
|
+
try {
|
|
1657
|
+
const response = await fetch('/api/send', {
|
|
1658
|
+
method: 'POST',
|
|
1659
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1660
|
+
body: JSON.stringify({ to, from: config.user, message })
|
|
1661
|
+
});
|
|
1662
|
+
|
|
1663
|
+
if (response.ok) {
|
|
1664
|
+
// Mark the original message as read if messageId is provided
|
|
1665
|
+
if (messageId) {
|
|
1666
|
+
await fetch(`/api/messages/${messageId}/read`, { method: 'POST' });
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
// Show toast
|
|
1670
|
+
const toast = document.getElementById('toast');
|
|
1671
|
+
const toastMsg = document.getElementById('toast-message');
|
|
1672
|
+
if (toastMsg) toastMsg.textContent = 'Quick reply sent!';
|
|
1673
|
+
toast.classList.add('show');
|
|
1674
|
+
setTimeout(() => {
|
|
1675
|
+
toast.classList.remove('show');
|
|
1676
|
+
if (toastMsg) toastMsg.textContent = 'Message sent!';
|
|
1677
|
+
}, 2000);
|
|
1678
|
+
|
|
1679
|
+
// Reload data
|
|
1680
|
+
loadData();
|
|
1681
|
+
} else {
|
|
1682
|
+
console.error('Failed to send quick response');
|
|
1683
|
+
}
|
|
1684
|
+
} catch (err) {
|
|
1685
|
+
console.error('Error sending quick response:', err);
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
// Render a message card HTML with optional choice buttons
|
|
1690
|
+
function renderMessageCard(msg, showChoices = true) {
|
|
1691
|
+
const { content, frontmatter } = parseFrontmatter(msg.message);
|
|
1692
|
+
const choices = frontmatter?.choices;
|
|
1693
|
+
const showChoicesButtons = showChoices && Array.isArray(choices) && choices.length > 0;
|
|
1694
|
+
const replyTo = msg.recipient.toLowerCase() === config.user.toLowerCase() ? msg.sender : msg.recipient;
|
|
1695
|
+
|
|
1696
|
+
return `
|
|
1697
|
+
<div class="message-card ${msg.read ? '' : 'unread'}" data-id="${msg.id}" data-sender="${msg.sender}" data-recipient="${msg.recipient}">
|
|
1698
|
+
<div class="message-header">
|
|
1699
|
+
<span class="message-sender">${msg.sender} → ${msg.recipient}</span>
|
|
1700
|
+
<span class="message-time">${new Date(msg.timestamp).toLocaleString()}</span>
|
|
1701
|
+
</div>
|
|
1702
|
+
<div class="message-text">${marked.parse(content)}</div>
|
|
1703
|
+
${showChoicesButtons ? `
|
|
1704
|
+
<div class="message-choices">
|
|
1705
|
+
${choices.map((choice) => `
|
|
1706
|
+
<button class="choice-btn" onclick="sendQuickResponse('${replyTo}', '${choice.replace(/'/g, "\\'")}', ${msg.id})">
|
|
1707
|
+
${choice}
|
|
1708
|
+
</button>
|
|
1709
|
+
`).join('')}
|
|
1710
|
+
</div>
|
|
1711
|
+
` : ''}
|
|
1712
|
+
</div>
|
|
1713
|
+
`;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1594
1716
|
// Event listeners
|
|
1595
1717
|
document.getElementById('send-btn').addEventListener('click', sendMessage);
|
|
1596
1718
|
|
package/public/index.html
CHANGED
|
@@ -592,6 +592,18 @@
|
|
|
592
592
|
.empty-state p {
|
|
593
593
|
font-size: 1rem;
|
|
594
594
|
}
|
|
595
|
+
|
|
596
|
+
/* Choice buttons on mobile */
|
|
597
|
+
.message-choices {
|
|
598
|
+
gap: 6px;
|
|
599
|
+
margin-top: 10px;
|
|
600
|
+
padding-top: 10px;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
.choice-btn {
|
|
604
|
+
padding: 10px 14px;
|
|
605
|
+
font-size: 0.9rem;
|
|
606
|
+
}
|
|
595
607
|
}
|
|
596
608
|
|
|
597
609
|
/* Extra small screens */
|
|
@@ -776,6 +788,49 @@
|
|
|
776
788
|
.tab-badge[style*="display: none"] {
|
|
777
789
|
display: none;
|
|
778
790
|
}
|
|
791
|
+
|
|
792
|
+
/* Message choice buttons */
|
|
793
|
+
.message-choices {
|
|
794
|
+
display: flex;
|
|
795
|
+
flex-wrap: wrap;
|
|
796
|
+
gap: 8px;
|
|
797
|
+
margin-top: 12px;
|
|
798
|
+
padding-top: 12px;
|
|
799
|
+
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
.choice-btn {
|
|
803
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
804
|
+
color: white;
|
|
805
|
+
border: none;
|
|
806
|
+
border-radius: 20px;
|
|
807
|
+
padding: 8px 16px;
|
|
808
|
+
font-size: 0.85rem;
|
|
809
|
+
font-weight: 500;
|
|
810
|
+
cursor: pointer;
|
|
811
|
+
transition: all 0.2s ease;
|
|
812
|
+
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
.choice-btn:hover {
|
|
816
|
+
transform: translateY(-2px);
|
|
817
|
+
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.5);
|
|
818
|
+
background: linear-gradient(135deg, #7b8ff0 0%, #8b5eb5 100%);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
.choice-btn:active {
|
|
822
|
+
transform: translateY(0);
|
|
823
|
+
box-shadow: 0 2px 6px rgba(102, 126, 234, 0.3);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/* Darker variant for unread messages */
|
|
827
|
+
.message-card.unread .choice-btn {
|
|
828
|
+
background: linear-gradient(135deg, #4fd1c5 0%, #667eea 100%);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
.message-card.unread .choice-btn:hover {
|
|
832
|
+
background: linear-gradient(135deg, #5ee4d8 0%, #7b8ff0 100%);
|
|
833
|
+
}
|
|
779
834
|
</style>
|
|
780
835
|
<!-- Markdown parser -->
|
|
781
836
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
@@ -841,7 +896,7 @@
|
|
|
841
896
|
</div>
|
|
842
897
|
</div>
|
|
843
898
|
|
|
844
|
-
<div class="toast" id="toast">Message sent!</div>
|
|
899
|
+
<div class="toast" id="toast"><span id="toast-message">Message sent!</span></div>
|
|
845
900
|
|
|
846
901
|
<script type="importmap">
|
|
847
902
|
{
|
package/server.ts
CHANGED
|
@@ -13,7 +13,7 @@ const args = process.argv.slice(2);
|
|
|
13
13
|
let user: string | null = null;
|
|
14
14
|
let mailboxPath: string | null = null;
|
|
15
15
|
let coworkerPath: string | null = null;
|
|
16
|
-
let
|
|
16
|
+
let statusPath: string | null = null;
|
|
17
17
|
let port: number = parseInt(process.env.PORT || '3000', 10);
|
|
18
18
|
let host: string = process.env.HOST || '0.0.0.0';
|
|
19
19
|
|
|
@@ -24,8 +24,8 @@ for (let i = 0; i < args.length; i++) {
|
|
|
24
24
|
mailboxPath = args[++i];
|
|
25
25
|
} else if (args[i] === '--coworkers' || args[i] === '-c') {
|
|
26
26
|
coworkerPath = args[++i];
|
|
27
|
-
} else if (args[i] === '--
|
|
28
|
-
|
|
27
|
+
} else if (args[i] === '--status' || args[i] === '-s') {
|
|
28
|
+
statusPath = args[++i];
|
|
29
29
|
} else if (args[i] === '--port' || args[i] === '-p') {
|
|
30
30
|
const p = parseInt(args[++i], 10);
|
|
31
31
|
if (!isNaN(p)) port = p;
|
|
@@ -35,7 +35,7 @@ for (let i = 0; i < args.length; i++) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
if (!user || !mailboxPath) {
|
|
38
|
-
console.error('Usage: watercooler --user <name> --mailbox <path> [--coworkers <path>] [--
|
|
38
|
+
console.error('Usage: watercooler --user <name> --mailbox <path> [--coworkers <path>] [--status <path>] [--port <number>] [--host <address>]');
|
|
39
39
|
process.exit(1);
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -44,15 +44,15 @@ console.log(` Mailbox: ${mailboxPath}`);
|
|
|
44
44
|
if (coworkerPath) {
|
|
45
45
|
console.log(` Coworker DB: ${coworkerPath}`);
|
|
46
46
|
}
|
|
47
|
-
if (
|
|
48
|
-
console.log(`
|
|
47
|
+
if (statusPath) {
|
|
48
|
+
console.log(` Status DB: ${statusPath}`);
|
|
49
49
|
}
|
|
50
50
|
console.log(` URL: http://${host}:${port}`);
|
|
51
51
|
|
|
52
52
|
// Databases
|
|
53
53
|
let db: Database.Database | null = null;
|
|
54
54
|
let coworkerDb: Database.Database | null = null;
|
|
55
|
-
let
|
|
55
|
+
let statusDb: Database.Database | null = null;
|
|
56
56
|
|
|
57
57
|
try {
|
|
58
58
|
db = new Database(mailboxPath);
|
|
@@ -71,12 +71,12 @@ if (coworkerPath) {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
if (
|
|
74
|
+
if (statusPath) {
|
|
75
75
|
try {
|
|
76
|
-
|
|
77
|
-
console.log('
|
|
76
|
+
statusDb = new Database(statusPath);
|
|
77
|
+
console.log(' Status DB: connected');
|
|
78
78
|
} catch (err: any) {
|
|
79
|
-
console.warn('
|
|
79
|
+
console.warn(' Status DB error:', err.message);
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
|
|
@@ -247,16 +247,16 @@ app.post('/api/messages/:id/read', (req, res) => {
|
|
|
247
247
|
}
|
|
248
248
|
});
|
|
249
249
|
|
|
250
|
-
// API: Get
|
|
251
|
-
app.get('/api/
|
|
250
|
+
// API: Get status states (latest tool usage per coworker)
|
|
251
|
+
app.get('/api/status', (req, res) => {
|
|
252
252
|
try {
|
|
253
|
-
if (!
|
|
253
|
+
if (!statusDb) {
|
|
254
254
|
res.json({});
|
|
255
255
|
return;
|
|
256
256
|
}
|
|
257
257
|
|
|
258
258
|
// Check if latest_tool_usage table exists
|
|
259
|
-
const tableCheck =
|
|
259
|
+
const tableCheck = statusDb.prepare(`
|
|
260
260
|
SELECT name FROM sqlite_master
|
|
261
261
|
WHERE type='table' AND name='latest_tool_usage'
|
|
262
262
|
`).get();
|
|
@@ -267,7 +267,7 @@ app.get('/api/avatars', (req, res) => {
|
|
|
267
267
|
}
|
|
268
268
|
|
|
269
269
|
// Get latest tool usage per name
|
|
270
|
-
const stmt =
|
|
270
|
+
const stmt = statusDb.prepare(`
|
|
271
271
|
SELECT name, tool_name, timestamp
|
|
272
272
|
FROM latest_tool_usage
|
|
273
273
|
ORDER BY timestamp DESC
|
|
@@ -276,26 +276,26 @@ app.get('/api/avatars', (req, res) => {
|
|
|
276
276
|
const rows = stmt.all() as Array<{name: string; tool_name: string; timestamp: number}>;
|
|
277
277
|
|
|
278
278
|
// Build map of name -> latest tool (first occurrence is latest due to ORDER BY)
|
|
279
|
-
const
|
|
279
|
+
const statusStates: Record<string, {tool_name: string; timestamp: number}> = {};
|
|
280
280
|
for (const row of rows) {
|
|
281
|
-
if (!
|
|
282
|
-
|
|
281
|
+
if (!statusStates[row.name]) {
|
|
282
|
+
statusStates[row.name] = {
|
|
283
283
|
tool_name: row.tool_name,
|
|
284
284
|
timestamp: row.timestamp
|
|
285
285
|
};
|
|
286
286
|
}
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
-
res.json(
|
|
289
|
+
res.json(statusStates);
|
|
290
290
|
} catch (err: any) {
|
|
291
|
-
console.error('Error in /api/
|
|
291
|
+
console.error('Error in /api/status:', err.message);
|
|
292
292
|
res.status(500).json({ error: err.message });
|
|
293
293
|
}
|
|
294
294
|
});
|
|
295
295
|
|
|
296
296
|
// Config endpoint
|
|
297
297
|
app.get('/api/config', (req, res) => {
|
|
298
|
-
res.json({ user, mailbox: mailboxPath, coworker: coworkerPath,
|
|
298
|
+
res.json({ user, mailbox: mailboxPath, coworker: coworkerPath, status: statusPath });
|
|
299
299
|
});
|
|
300
300
|
|
|
301
301
|
app.listen(port, host, () => {
|