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.
- package/package.json +4 -2
- package/src/commands/ui.mjs +13 -6732
- package/src/constants.mjs +1 -1
- package/src/server/index.mjs +299 -0
- package/src/server/routes/agents.mjs +109 -0
- package/src/server/routes/calendar.mjs +115 -0
- package/src/server/routes/chat.mjs +350 -0
- package/src/server/routes/collab.mjs +97 -0
- package/src/server/routes/config.mjs +179 -0
- package/src/server/routes/drive.mjs +184 -0
- package/src/server/routes/email.mjs +428 -0
- package/src/server/routes/google-auth.mjs +58 -0
- package/src/server/routes/integrations.mjs +437 -0
- package/src/server/routes/studio.mjs +287 -0
- package/src/server/routes/tasks.mjs +116 -0
- package/src/server/routes/webcraft.mjs +1092 -0
- package/src/server/ws.mjs +44 -0
- package/src/services/email-db.mjs +32 -15
- package/src/services/tool-executor.mjs +702 -2
- package/src/ui-dist/3rdArmLogo.jpeg +0 -0
- package/src/ui-dist/NHALogo.jpeg +0 -0
- package/src/ui-dist/apple-touch-icon.png +0 -0
- package/src/ui-dist/assets/index-DBDRtTmR.js +423 -0
- package/src/ui-dist/assets/index-DyNVopKq.css +1 -0
- package/src/ui-dist/favicon-16x16.png +0 -0
- package/src/ui-dist/favicon-32x32.png +0 -0
- package/src/ui-dist/favicon.ico +0 -0
- package/src/ui-dist/favicon.svg +6 -0
- package/src/ui-dist/icon-192x192.png +0 -0
- package/src/ui-dist/icons.svg +24 -0
- package/src/ui-dist/index.html +17 -0
- package/src/services/web-ui.mjs +0 -10651
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Third-party integrations — GitHub, Notion, Slack, Contacts, Birthdays, Reminders, Maps, Cron
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { sendJSON, sendError, parseBody } from '../index.mjs';
|
|
6
|
+
import { loadConfig } from '../../config.mjs';
|
|
7
|
+
import { NHA_DIR } from '../../constants.mjs';
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
|
|
11
|
+
export function register(router) {
|
|
12
|
+
// ── GitHub ────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
router.get('/api/github/repos', async (_req, res) => {
|
|
15
|
+
try {
|
|
16
|
+
const { listRepos } = await import('../../services/github.mjs');
|
|
17
|
+
const config = loadConfig();
|
|
18
|
+
sendJSON(res, 200, { repos: await listRepos(config) });
|
|
19
|
+
} catch (e) { sendError(res, 500, e.message); }
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
router.get('/api/github', async (req, res) => {
|
|
23
|
+
try {
|
|
24
|
+
const { getNotifications } = await import('../../services/github.mjs');
|
|
25
|
+
const config = loadConfig();
|
|
26
|
+
sendJSON(res, 200, { notifications: await getNotifications(config) });
|
|
27
|
+
} catch (e) { sendError(res, 500, e.message); }
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
router.get('/api/github/issues', async (req, res) => {
|
|
31
|
+
try {
|
|
32
|
+
const { listIssues } = await import('../../services/github.mjs');
|
|
33
|
+
const config = loadConfig();
|
|
34
|
+
const url = new URL(req.url, 'http://localhost');
|
|
35
|
+
sendJSON(res, 200, { issues: await listIssues(config, url.searchParams.get('repo')) });
|
|
36
|
+
} catch (e) { sendError(res, 500, e.message); }
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
router.get('/api/github/prs', async (req, res) => {
|
|
40
|
+
try {
|
|
41
|
+
const { listPRs } = await import('../../services/github.mjs');
|
|
42
|
+
const config = loadConfig();
|
|
43
|
+
const url = new URL(req.url, 'http://localhost');
|
|
44
|
+
sendJSON(res, 200, { prs: await listPRs(config, url.searchParams.get('repo')) });
|
|
45
|
+
} catch (e) { sendError(res, 500, e.message); }
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
router.post('/api/github/mark-read', async (req, res) => {
|
|
49
|
+
try {
|
|
50
|
+
const { markNotificationRead } = await import('../../services/github.mjs');
|
|
51
|
+
const body = await parseBody(req);
|
|
52
|
+
const config = loadConfig();
|
|
53
|
+
await markNotificationRead(config, body.id);
|
|
54
|
+
sendJSON(res, 200, { ok: true });
|
|
55
|
+
} catch (e) { sendError(res, 500, e.message); }
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// ── Notion ────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
router.get('/api/notion/search', async (req, res) => {
|
|
61
|
+
try {
|
|
62
|
+
const { searchNotion } = await import('../../services/notion.mjs');
|
|
63
|
+
const config = loadConfig();
|
|
64
|
+
const url = new URL(req.url, 'http://localhost');
|
|
65
|
+
sendJSON(res, 200, { results: await searchNotion(config, url.searchParams.get('q') || '') });
|
|
66
|
+
} catch (e) { sendError(res, 500, e.message); }
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
router.get('/api/notion/page', async (req, res) => {
|
|
70
|
+
try {
|
|
71
|
+
const { getNotionPage } = await import('../../services/notion.mjs');
|
|
72
|
+
const config = loadConfig();
|
|
73
|
+
const url = new URL(req.url, 'http://localhost');
|
|
74
|
+
sendJSON(res, 200, { page: await getNotionPage(config, url.searchParams.get('id')) });
|
|
75
|
+
} catch (e) { sendError(res, 500, e.message); }
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ── Slack ─────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
router.get('/api/slack/channels', async (_req, res) => {
|
|
81
|
+
try {
|
|
82
|
+
const { listChannels } = await import('../../services/slack.mjs');
|
|
83
|
+
const config = loadConfig();
|
|
84
|
+
sendJSON(res, 200, { channels: await listChannels(config) });
|
|
85
|
+
} catch (e) { sendError(res, 500, e.message); }
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
router.get('/api/slack/messages', async (req, res) => {
|
|
89
|
+
try {
|
|
90
|
+
const { getChannelMessages } = await import('../../services/slack.mjs');
|
|
91
|
+
const config = loadConfig();
|
|
92
|
+
const url = new URL(req.url, 'http://localhost');
|
|
93
|
+
sendJSON(res, 200, { messages: await getChannelMessages(config, url.searchParams.get('channel')) });
|
|
94
|
+
} catch (e) { sendError(res, 500, e.message); }
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ── Contacts ──────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
router.get('/api/contacts', async (req, res) => {
|
|
100
|
+
try {
|
|
101
|
+
const url = new URL(req.url, 'http://localhost');
|
|
102
|
+
const q = url.searchParams.get('q') || '';
|
|
103
|
+
const limit = parseInt(url.searchParams.get('limit') || '100', 10);
|
|
104
|
+
const config = loadConfig();
|
|
105
|
+
if (q) {
|
|
106
|
+
const { searchContacts } = await import('../../services/google-contacts.mjs');
|
|
107
|
+
sendJSON(res, 200, { contacts: await searchContacts(config, q, limit) });
|
|
108
|
+
} else {
|
|
109
|
+
const { listContacts } = await import('../../services/google-contacts.mjs');
|
|
110
|
+
sendJSON(res, 200, { contacts: await listContacts(config, limit) });
|
|
111
|
+
}
|
|
112
|
+
} catch (e) { sendError(res, 500, e.message); }
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
router.post('/api/contacts', async (req, res) => {
|
|
116
|
+
try {
|
|
117
|
+
const { createContact } = await import('../../services/google-contacts.mjs');
|
|
118
|
+
const body = await parseBody(req);
|
|
119
|
+
const config = loadConfig();
|
|
120
|
+
sendJSON(res, 201, { contact: await createContact(config, body) });
|
|
121
|
+
} catch (e) { sendError(res, 500, e.message); }
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
router.post('/api/contacts/update', async (req, res) => {
|
|
125
|
+
try {
|
|
126
|
+
const { updateContact } = await import('../../services/google-contacts.mjs');
|
|
127
|
+
const body = await parseBody(req);
|
|
128
|
+
const config = loadConfig();
|
|
129
|
+
sendJSON(res, 200, { contact: await updateContact(config, body.resourceName, body) });
|
|
130
|
+
} catch (e) { sendError(res, 500, e.message); }
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
router.post('/api/contacts/delete', async (req, res) => {
|
|
134
|
+
try {
|
|
135
|
+
const { deleteContact } = await import('../../services/google-contacts.mjs');
|
|
136
|
+
const body = await parseBody(req);
|
|
137
|
+
const config = loadConfig();
|
|
138
|
+
await deleteContact(config, body.resourceName);
|
|
139
|
+
sendJSON(res, 200, { ok: true });
|
|
140
|
+
} catch (e) { sendError(res, 500, e.message); }
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// ── Birthdays ─────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
const birthdaysFile = path.join(NHA_DIR, 'birthdays.json');
|
|
146
|
+
const loadBirthdays = () => { try { return JSON.parse(fs.readFileSync(birthdaysFile, 'utf-8')); } catch { return []; } };
|
|
147
|
+
const saveBirthdays = (b) => { fs.mkdirSync(NHA_DIR, { recursive: true }); fs.writeFileSync(birthdaysFile, JSON.stringify(b, null, 2)); };
|
|
148
|
+
|
|
149
|
+
router.get('/api/birthdays', (_req, res) => {
|
|
150
|
+
sendJSON(res, 200, { birthdays: loadBirthdays() });
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
router.post('/api/birthdays', async (req, res) => {
|
|
154
|
+
try {
|
|
155
|
+
const body = await parseBody(req);
|
|
156
|
+
const list = loadBirthdays();
|
|
157
|
+
const id = Date.now().toString(36);
|
|
158
|
+
list.push({ id, ...body });
|
|
159
|
+
saveBirthdays(list);
|
|
160
|
+
sendJSON(res, 201, { birthday: { id, ...body } });
|
|
161
|
+
} catch (e) { sendError(res, 500, e.message); }
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
router.post('/api/birthdays/delete', async (req, res) => {
|
|
165
|
+
try {
|
|
166
|
+
const body = await parseBody(req);
|
|
167
|
+
const list = loadBirthdays().filter(b => b.id !== body.id);
|
|
168
|
+
saveBirthdays(list);
|
|
169
|
+
sendJSON(res, 200, { ok: true });
|
|
170
|
+
} catch (e) { sendError(res, 500, e.message); }
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// ── Cron jobs ─────────────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
router.get('/api/cron', async (_req, res) => {
|
|
176
|
+
try {
|
|
177
|
+
const { listCronJobs } = await import('../../services/ops-daemon.mjs');
|
|
178
|
+
sendJSON(res, 200, { jobs: listCronJobs() });
|
|
179
|
+
} catch (e) { sendError(res, 500, e.message); }
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
router.post('/api/cron', async (req, res) => {
|
|
183
|
+
try {
|
|
184
|
+
const body = await parseBody(req);
|
|
185
|
+
const { addCronJob, removeCronJob } = await import('../../services/ops-daemon.mjs');
|
|
186
|
+
if (body.action === 'remove') { removeCronJob(body.id); return sendJSON(res, 200, { ok: true }); }
|
|
187
|
+
const job = addCronJob(body);
|
|
188
|
+
sendJSON(res, 201, { job });
|
|
189
|
+
} catch (e) { sendError(res, 500, e.message); }
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// ── Reminders ─────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
const remindersFile = path.join(NHA_DIR, 'reminders.json');
|
|
195
|
+
const loadReminders = () => { try { return JSON.parse(fs.readFileSync(remindersFile, 'utf-8')); } catch { return []; } };
|
|
196
|
+
const saveReminders = (r) => { fs.mkdirSync(NHA_DIR, { recursive: true }); fs.writeFileSync(remindersFile, JSON.stringify(r, null, 2)); };
|
|
197
|
+
|
|
198
|
+
router.get('/api/reminders', (_req, res) => {
|
|
199
|
+
const now = Date.now();
|
|
200
|
+
const reminders = loadReminders().map(r => ({ ...r, triggered: r.at && new Date(r.at).getTime() <= now }));
|
|
201
|
+
sendJSON(res, 200, { reminders });
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
router.post('/api/reminders', async (req, res) => {
|
|
205
|
+
try {
|
|
206
|
+
const body = await parseBody(req);
|
|
207
|
+
if (body.action === 'delete') {
|
|
208
|
+
saveReminders(loadReminders().filter(r => r.id !== body.id));
|
|
209
|
+
return sendJSON(res, 200, { ok: true });
|
|
210
|
+
}
|
|
211
|
+
if (body.action === 'snooze') {
|
|
212
|
+
const mins = body.minutes || 10;
|
|
213
|
+
const list = loadReminders().map(r => r.id === body.id ? { ...r, at: new Date(Date.now() + mins * 60_000).toISOString() } : r);
|
|
214
|
+
saveReminders(list);
|
|
215
|
+
return sendJSON(res, 200, { ok: true });
|
|
216
|
+
}
|
|
217
|
+
if (body.action === 'dismiss') {
|
|
218
|
+
const list = loadReminders().map(r => r.id === body.id ? { ...r, dismissed: true } : r);
|
|
219
|
+
saveReminders(list);
|
|
220
|
+
return sendJSON(res, 200, { ok: true });
|
|
221
|
+
}
|
|
222
|
+
const id = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
223
|
+
const reminder = { id, text: body.text, at: body.at || null, repeat: body.repeat || null, dismissed: false, createdAt: new Date().toISOString() };
|
|
224
|
+
const list = loadReminders();
|
|
225
|
+
list.push(reminder);
|
|
226
|
+
saveReminders(list);
|
|
227
|
+
sendJSON(res, 201, { reminder });
|
|
228
|
+
} catch (e) { sendError(res, 500, e.message); }
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// ── Maps / Places ──────────────────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
router.get('/api/maps', async (req, res) => {
|
|
234
|
+
try {
|
|
235
|
+
const url = new URL(req.url, 'http://localhost');
|
|
236
|
+
const q = url.searchParams.get('q') || '';
|
|
237
|
+
const lat = url.searchParams.get('lat');
|
|
238
|
+
const lon = url.searchParams.get('lon');
|
|
239
|
+
if (!q) return sendError(res, 400, 'Missing query parameter q');
|
|
240
|
+
|
|
241
|
+
// Use Nominatim (free, no key required) for geocoding/search
|
|
242
|
+
const base = 'https://nominatim.openstreetmap.org';
|
|
243
|
+
const headers = { 'User-Agent': 'NHA-UI/1.0 (nothumanallowed.com)' };
|
|
244
|
+
|
|
245
|
+
if (lat && lon) {
|
|
246
|
+
// Nearby search: geocode nearby named places using overpass API approach via Nominatim
|
|
247
|
+
const r = await fetch(`${base}/search?q=${encodeURIComponent(q)}&lat=${lat}&lon=${lon}&format=json&limit=10&addressdetails=1`, { headers });
|
|
248
|
+
const places = await r.json();
|
|
249
|
+
return sendJSON(res, 200, { places: places.map(p => ({ id: p.place_id, name: p.display_name.split(',')[0], address: p.display_name, lat: parseFloat(p.lat), lon: parseFloat(p.lon), type: p.type, category: p.class })) });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const r = await fetch(`${base}/search?q=${encodeURIComponent(q)}&format=json&limit=10&addressdetails=1`, { headers });
|
|
253
|
+
const places = await r.json();
|
|
254
|
+
sendJSON(res, 200, { places: places.map(p => ({ id: p.place_id, name: p.display_name.split(',')[0], address: p.display_name, lat: parseFloat(p.lat), lon: parseFloat(p.lon), type: p.type, category: p.class })) });
|
|
255
|
+
} catch (e) { sendError(res, 500, e.message); }
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// ── Screen Capture ─────────────────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
router.post('/api/screen/capture', async (_req, res) => {
|
|
261
|
+
try {
|
|
262
|
+
const { captureScreen } = await import('../../services/screen-capture.mjs');
|
|
263
|
+
const result = captureScreen({ base64: true });
|
|
264
|
+
if (!result.ok) return sendError(res, 500, result.error || 'Screenshot failed');
|
|
265
|
+
sendJSON(res, 200, { ok: true, base64: result.base64, width: result.width, height: result.height });
|
|
266
|
+
} catch (e) { sendError(res, 500, e.message); }
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
router.get('/api/screen/status', async (_req, res) => {
|
|
270
|
+
try {
|
|
271
|
+
const { isScreenCaptureAvailable } = await import('../../services/screen-capture.mjs');
|
|
272
|
+
sendJSON(res, 200, { available: isScreenCaptureAvailable(), platform: process.platform });
|
|
273
|
+
} catch (e) { sendError(res, 500, e.message); }
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// ── Slack send ────────────────────────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
router.post('/api/slack/send', async (req, res) => {
|
|
279
|
+
try {
|
|
280
|
+
const { sendMessage } = await import('../../services/slack.mjs');
|
|
281
|
+
const body = await parseBody(req);
|
|
282
|
+
const config = loadConfig();
|
|
283
|
+
if (!body.channel || !body.text) return sendError(res, 400, 'channel and text required');
|
|
284
|
+
await sendMessage(config, body.channel, body.text);
|
|
285
|
+
sendJSON(res, 200, { ok: true });
|
|
286
|
+
} catch (e) { sendError(res, 500, e.message); }
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// ── Notes full CRUD ───────────────────────────────────────────────────
|
|
290
|
+
|
|
291
|
+
router.get('/api/notes/search', async (req, res) => {
|
|
292
|
+
try {
|
|
293
|
+
const { searchNotes } = await import('../../services/notes.mjs');
|
|
294
|
+
const url = new URL(req.url, 'http://localhost');
|
|
295
|
+
sendJSON(res, 200, { notes: searchNotes(url.searchParams.get('q') || '') });
|
|
296
|
+
} catch (e) { sendError(res, 500, e.message); }
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
router.get(/^\/api\/notes\/(?<id>[^/?]+)$/, async (req, res) => {
|
|
300
|
+
try {
|
|
301
|
+
const { getNote } = await import('../../services/notes.mjs');
|
|
302
|
+
const note = getNote(req.params.id);
|
|
303
|
+
if (!note) return sendError(res, 404, 'Note not found');
|
|
304
|
+
sendJSON(res, 200, { note });
|
|
305
|
+
} catch (e) { sendError(res, 500, e.message); }
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
router.post('/api/notes/create', async (req, res) => {
|
|
309
|
+
try {
|
|
310
|
+
const { createNote } = await import('../../services/notes.mjs');
|
|
311
|
+
const body = await parseBody(req);
|
|
312
|
+
if (!body.title) return sendError(res, 400, 'title required');
|
|
313
|
+
const note = createNote(body.title, body.content || '', body.tags || []);
|
|
314
|
+
sendJSON(res, 201, { note });
|
|
315
|
+
} catch (e) { sendError(res, 500, e.message); }
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
router.post('/api/notes/update', async (req, res) => {
|
|
319
|
+
try {
|
|
320
|
+
const { updateNote } = await import('../../services/notes.mjs');
|
|
321
|
+
const body = await parseBody(req);
|
|
322
|
+
if (!body.id) return sendError(res, 400, 'id required');
|
|
323
|
+
updateNote(body.id, body.title, body.content, body.tags);
|
|
324
|
+
sendJSON(res, 200, { ok: true });
|
|
325
|
+
} catch (e) { sendError(res, 500, e.message); }
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
router.post('/api/notes/delete', async (req, res) => {
|
|
329
|
+
try {
|
|
330
|
+
const { deleteNote } = await import('../../services/notes.mjs');
|
|
331
|
+
const body = await parseBody(req);
|
|
332
|
+
if (!body.id) return sendError(res, 400, 'id required');
|
|
333
|
+
deleteNote(body.id);
|
|
334
|
+
sendJSON(res, 200, { ok: true });
|
|
335
|
+
} catch (e) { sendError(res, 500, e.message); }
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// ── Contacts — Google Birthdays ───────────────────────────────────────
|
|
339
|
+
|
|
340
|
+
router.get('/api/contacts/birthdays', async (_req, res) => {
|
|
341
|
+
try {
|
|
342
|
+
const { getBirthdays } = await import('../../services/google-contacts.mjs');
|
|
343
|
+
const config = loadConfig();
|
|
344
|
+
sendJSON(res, 200, { birthdays: await getBirthdays(config) });
|
|
345
|
+
} catch (e) {
|
|
346
|
+
if (e.message?.includes('token') || e.message?.includes('auth')) {
|
|
347
|
+
return sendJSON(res, 200, { birthdays: [], authRequired: true });
|
|
348
|
+
}
|
|
349
|
+
sendError(res, 500, e.message);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// ── Connectors — workflow persistence ────────────────────────────────
|
|
354
|
+
|
|
355
|
+
const workflowsFile = path.join(NHA_DIR, 'workflows.json');
|
|
356
|
+
const loadWorkflows = () => { try { return JSON.parse(fs.readFileSync(workflowsFile, 'utf-8')); } catch { return []; } };
|
|
357
|
+
const saveWorkflows = (w) => { fs.mkdirSync(NHA_DIR, { recursive: true }); fs.writeFileSync(workflowsFile, JSON.stringify(w, null, 2)); };
|
|
358
|
+
|
|
359
|
+
router.get('/api/connectors/workflows', (_req, res) => {
|
|
360
|
+
sendJSON(res, 200, { workflows: loadWorkflows() });
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
router.post('/api/connectors/workflows', async (req, res) => {
|
|
364
|
+
try {
|
|
365
|
+
const body = await parseBody(req);
|
|
366
|
+
const { action } = body;
|
|
367
|
+
if (action === 'delete') {
|
|
368
|
+
saveWorkflows(loadWorkflows().filter(w => w.id !== body.id));
|
|
369
|
+
return sendJSON(res, 200, { ok: true });
|
|
370
|
+
}
|
|
371
|
+
if (action === 'toggle') {
|
|
372
|
+
const updated = loadWorkflows().map(w => w.id === body.id ? { ...w, enabled: !w.enabled } : w);
|
|
373
|
+
saveWorkflows(updated);
|
|
374
|
+
return sendJSON(res, 200, { ok: true, workflows: updated });
|
|
375
|
+
}
|
|
376
|
+
// Upsert workflow
|
|
377
|
+
const list = loadWorkflows();
|
|
378
|
+
const idx = list.findIndex(w => w.id === body.id);
|
|
379
|
+
if (idx >= 0) list[idx] = body;
|
|
380
|
+
else list.push(body);
|
|
381
|
+
saveWorkflows(list);
|
|
382
|
+
sendJSON(res, 200, { ok: true, workflow: body });
|
|
383
|
+
} catch (e) { sendError(res, 500, e.message); }
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// ── Unread counts — sidebar badges ────────────────────────────────────
|
|
387
|
+
|
|
388
|
+
router.get('/api/unread-counts', async (_req, res) => {
|
|
389
|
+
try {
|
|
390
|
+
const config = loadConfig();
|
|
391
|
+
let gmailUnread = 0;
|
|
392
|
+
let imapUnread = 0;
|
|
393
|
+
let todayEvents = 0;
|
|
394
|
+
|
|
395
|
+
// Gmail unread
|
|
396
|
+
try {
|
|
397
|
+
const { listMessages } = await import('../../services/mail-router.mjs');
|
|
398
|
+
const emails = await listMessages(config, { folder: 'inbox' });
|
|
399
|
+
gmailUnread = (Array.isArray(emails) ? emails : []).filter(e => e.isUnread).length;
|
|
400
|
+
} catch { /* not connected */ }
|
|
401
|
+
|
|
402
|
+
// IMAP unread
|
|
403
|
+
try {
|
|
404
|
+
const { listAccounts, getSystemLabel } = await import('../../services/email-db.mjs');
|
|
405
|
+
const accounts = listAccounts();
|
|
406
|
+
for (const acc of accounts) {
|
|
407
|
+
const inbox = getSystemLabel(acc.id, 'inbox');
|
|
408
|
+
imapUnread += inbox?.unreadCount ?? 0;
|
|
409
|
+
}
|
|
410
|
+
} catch { /* not configured */ }
|
|
411
|
+
|
|
412
|
+
// Today events count
|
|
413
|
+
try {
|
|
414
|
+
const { getTodayEvents } = await import('../../services/mail-router.mjs');
|
|
415
|
+
const events = await getTodayEvents(config);
|
|
416
|
+
todayEvents = Array.isArray(events) ? events.length : 0;
|
|
417
|
+
} catch { /* not connected */ }
|
|
418
|
+
|
|
419
|
+
sendJSON(res, 200, { gmailUnread, imapUnread, emailUnread: gmailUnread + imapUnread, todayEvents });
|
|
420
|
+
} catch (e) { sendError(res, 500, e.message); }
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
// ── Screenshots ───────────────────────────────────────────────────────
|
|
424
|
+
|
|
425
|
+
router.get(/^\/api\/screenshots\/(.+)$/, (req, res) => {
|
|
426
|
+
const name = req.url.match(/^\/api\/screenshots\/(.+)$/)?.[1];
|
|
427
|
+
if (!name || name.includes('..') || !name.match(/\.(jpg|png|webp)$/)) {
|
|
428
|
+
return sendError(res, 404, 'Not found');
|
|
429
|
+
}
|
|
430
|
+
const p = path.join(NHA_DIR, 'screenshots', name);
|
|
431
|
+
if (!fs.existsSync(p)) return sendError(res, 404, 'Not found');
|
|
432
|
+
const ext = path.extname(name);
|
|
433
|
+
const mime = { '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.webp': 'image/webp' }[ext] || 'image/jpeg';
|
|
434
|
+
res.writeHead(200, { 'Content-Type': mime, 'Cache-Control': 'public, max-age=86400' });
|
|
435
|
+
res.end(fs.readFileSync(p));
|
|
436
|
+
});
|
|
437
|
+
}
|