nothumanallowed 13.5.199 → 14.0.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.
@@ -0,0 +1,44 @@
1
+ /**
2
+ * WebSocket handler — broadcasts daemon events to the React UI.
3
+ * Uses the `ws` package already bundled in nha-cli.
4
+ */
5
+
6
+ import { createRequire } from 'module';
7
+ import path from 'path';
8
+ import { fileURLToPath } from 'url';
9
+
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
+ // Resolve `ws` from nha-cli's node_modules
12
+ const require = createRequire(import.meta.url);
13
+ // __dirname = packages/nha-cli/src/server/ → ../../ = packages/nha-cli/
14
+ const { WebSocketServer } = require(
15
+ path.resolve(__dirname, '../../node_modules/ws/index.js')
16
+ );
17
+
18
+ let wss = null;
19
+
20
+ export function setupWebSocket(server) {
21
+ wss = new WebSocketServer({ server, path: '/api' });
22
+
23
+ wss.on('connection', (ws) => {
24
+ ws.send(JSON.stringify({ type: 'connected', ts: Date.now() }));
25
+
26
+ ws.on('message', (data) => {
27
+ try {
28
+ const msg = JSON.parse(data.toString());
29
+ // Echo back for now — route-specific handlers can call broadcast()
30
+ broadcast({ type: 'echo', payload: msg });
31
+ } catch { /* ignore malformed */ }
32
+ });
33
+
34
+ ws.on('error', () => {});
35
+ });
36
+ }
37
+
38
+ export function broadcast(msg) {
39
+ if (!wss) return;
40
+ const data = JSON.stringify(msg);
41
+ for (const client of wss.clients) {
42
+ if (client.readyState === 1) client.send(data);
43
+ }
44
+ }
@@ -424,18 +424,31 @@ export function deleteLabel(id) {
424
424
  getDb().prepare('DELETE FROM email_labels WHERE id = ? AND is_system = 0').run(id);
425
425
  }
426
426
 
427
- export function updateLabelCounts(db, labelId) {
428
- db.prepare(`
429
- UPDATE email_labels SET
430
- total_count = (SELECT COUNT(*) FROM email_message_labels eml
431
- JOIN email_messages m ON m.id = eml.message_id
432
- WHERE eml.label_id = ? AND m.permanently_deleted = 0),
433
- unread_count = (SELECT COUNT(*) FROM email_message_labels eml
434
- JOIN email_messages m ON m.id = eml.message_id
435
- LEFT JOIN email_message_state s ON s.message_id = m.id
436
- WHERE eml.label_id = ? AND m.permanently_deleted = 0 AND (s.is_read IS NULL OR s.is_read = 0))
437
- WHERE id = ?
438
- `).run(labelId, labelId, labelId);
427
+ const UPDATE_LABEL_SQL = `
428
+ UPDATE email_labels SET
429
+ total_count = (SELECT COUNT(*) FROM email_message_labels eml
430
+ JOIN email_messages m ON m.id = eml.message_id
431
+ WHERE eml.label_id = ? AND m.permanently_deleted = 0),
432
+ unread_count = (SELECT COUNT(*) FROM email_message_labels eml
433
+ JOIN email_messages m ON m.id = eml.message_id
434
+ LEFT JOIN email_message_state s ON s.message_id = m.id
435
+ WHERE eml.label_id = ? AND m.permanently_deleted = 0 AND (s.is_read IS NULL OR s.is_read = 0))
436
+ WHERE id = ?
437
+ `;
438
+
439
+ // Called from email-imap.mjs after a folder sync — updates all labels for the account
440
+ export function updateLabelCounts(accountId) {
441
+ const db = getDb();
442
+ const labels = db.prepare('SELECT id FROM email_labels WHERE account_id = ?').all(accountId);
443
+ const stmt = db.prepare(UPDATE_LABEL_SQL);
444
+ for (const lbl of labels) {
445
+ stmt.run(lbl.id, lbl.id, lbl.id);
446
+ }
447
+ }
448
+
449
+ // Called internally after adding/removing a single message from a label
450
+ function updateSingleLabelCount(db, labelId) {
451
+ db.prepare(UPDATE_LABEL_SQL).run(labelId, labelId, labelId);
439
452
  }
440
453
 
441
454
  // ── MESSAGE CRUD ───────────────────────────────────────────────────────────
@@ -494,13 +507,13 @@ export function insertAttachments(messageId, attachments) {
494
507
  export function addMessageToLabel(messageId, labelId) {
495
508
  const db = getDb();
496
509
  db.prepare('INSERT OR IGNORE INTO email_message_labels (message_id, label_id) VALUES (?, ?)').run(messageId, labelId);
497
- updateLabelCounts(db, labelId);
510
+ updateSingleLabelCount(db, labelId);
498
511
  }
499
512
 
500
513
  export function removeMessageFromLabel(messageId, labelId) {
501
514
  const db = getDb();
502
515
  db.prepare('DELETE FROM email_message_labels WHERE message_id = ? AND label_id = ?').run(messageId, labelId);
503
- updateLabelCounts(db, labelId);
516
+ updateSingleLabelCount(db, labelId);
504
517
  }
505
518
 
506
519
  export function getMessageLabels(messageId) {
@@ -578,9 +591,13 @@ export function getThread(threadId, accountId) {
578
591
  // ── STATE MUTATIONS (local only — never touches IMAP) ─────────────────────
579
592
 
580
593
  export function markRead(messageId, isRead) {
581
- getDb().prepare(`INSERT INTO email_message_state (message_id, is_read, is_starred) VALUES (?, ?, 0)
594
+ const db = getDb();
595
+ db.prepare(`INSERT INTO email_message_state (message_id, is_read, is_starred) VALUES (?, ?, 0)
582
596
  ON CONFLICT(message_id) DO UPDATE SET is_read = excluded.is_read, updated_at = datetime('now')`)
583
597
  .run(messageId, isRead ? 1 : 0);
598
+ // Refresh label unread counts for this message's account
599
+ const msg = db.prepare('SELECT account_id FROM email_messages WHERE id = ?').get(messageId);
600
+ if (msg?.account_id) updateLabelCounts(msg.account_id);
584
601
  }
585
602
 
586
603
  export function markStarred(messageId, isStarred) {