nothumanallowed 13.2.56 → 13.2.59
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/commands/ui.mjs +158 -10
- package/src/config.mjs +3 -0
- package/src/constants.mjs +1 -1
- package/src/services/github.mjs +19 -0
- package/src/services/google-calendar.mjs +6 -0
- package/src/services/mail-router.mjs +13 -0
- package/src/services/microsoft-calendar.mjs +7 -0
- package/src/services/web-ui.mjs +228 -50
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "13.2.
|
|
3
|
+
"version": "13.2.59",
|
|
4
4
|
"description": "NotHumanAllowed — 38 AI agents, 80 tools, Studio (visual agentic workflows). Email, calendar, browser automation, screen capture, canvas, cron/heartbeat, Alexandria E2E messaging, GitHub, Notion, Slack, voice chat, free AI (Liara), 28 languages. Zero-dependency CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/commands/ui.mjs
CHANGED
|
@@ -18,7 +18,7 @@ import { loadConfig } from '../config.mjs';
|
|
|
18
18
|
import { detectMailProvider, hasMailProvider, getProviderStatus } from '../services/mail-router.mjs';
|
|
19
19
|
import { callLLM, callLLMStream, callAgent, parseAgentFile } from '../services/llm.mjs';
|
|
20
20
|
import { getUnreadImportant, getMessage, listMessages, sendEmail, createDraft } from '../services/mail-router.mjs';
|
|
21
|
-
import { getTodayEvents, getUpcomingEvents, createEvent, updateEvent, getEventsForDate } from '../services/mail-router.mjs';
|
|
21
|
+
import { getTodayEvents, getUpcomingEvents, createEvent, updateEvent, deleteEvent, getEventsForDate } from '../services/mail-router.mjs';
|
|
22
22
|
import {
|
|
23
23
|
getTasks,
|
|
24
24
|
addTask,
|
|
@@ -1027,6 +1027,70 @@ export async function cmdUI(args) {
|
|
|
1027
1027
|
return;
|
|
1028
1028
|
}
|
|
1029
1029
|
|
|
1030
|
+
// POST /api/calendar — create event
|
|
1031
|
+
if (method === 'POST' && pathname === '/api/calendar') {
|
|
1032
|
+
try {
|
|
1033
|
+
const body = await parseBody(req);
|
|
1034
|
+
if (!body.summary) { sendJSON(res, 400, { error: 'summary required' }); logRequest(method, pathname, 400, Date.now() - start); return; }
|
|
1035
|
+
const calendarId = body.calendarId || 'primary';
|
|
1036
|
+
const event = { summary: body.summary };
|
|
1037
|
+
if (body.description) event.description = body.description;
|
|
1038
|
+
if (body.location) event.location = body.location;
|
|
1039
|
+
if (body.allDay && body.date) {
|
|
1040
|
+
event.start = { date: body.date };
|
|
1041
|
+
event.end = { date: body.date };
|
|
1042
|
+
} else {
|
|
1043
|
+
const startDT = body.start || (body.date ? body.date + 'T09:00:00' : new Date().toISOString());
|
|
1044
|
+
const endDT = body.end || (body.date ? body.date + 'T10:00:00' : new Date(Date.now() + 3600000).toISOString());
|
|
1045
|
+
event.start = { dateTime: startDT };
|
|
1046
|
+
event.end = { dateTime: endDT };
|
|
1047
|
+
}
|
|
1048
|
+
const created = await createEvent(config, event, calendarId);
|
|
1049
|
+
sendJSON(res, 201, { event: created });
|
|
1050
|
+
} catch (e) {
|
|
1051
|
+
sendJSON(res, 500, { error: e.message });
|
|
1052
|
+
}
|
|
1053
|
+
logRequest(method, pathname, 201, Date.now() - start);
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// PATCH /api/calendar/:calId/:eventId — update event
|
|
1058
|
+
const calPatchMatch = pathname.match(/^\/api\/calendar\/([^/]+)\/([^/]+)$/);
|
|
1059
|
+
if (method === 'PATCH' && calPatchMatch) {
|
|
1060
|
+
try {
|
|
1061
|
+
const calendarId = decodeURIComponent(calPatchMatch[1]);
|
|
1062
|
+
const eventId = decodeURIComponent(calPatchMatch[2]);
|
|
1063
|
+
const body = await parseBody(req);
|
|
1064
|
+
const patch = {};
|
|
1065
|
+
if (body.summary !== undefined) patch.summary = body.summary;
|
|
1066
|
+
if (body.description !== undefined) patch.description = body.description;
|
|
1067
|
+
if (body.location !== undefined) patch.location = body.location;
|
|
1068
|
+
if (body.start !== undefined) patch.start = { dateTime: body.start };
|
|
1069
|
+
if (body.end !== undefined) patch.end = { dateTime: body.end };
|
|
1070
|
+
const updated = await updateEvent(config, calendarId, eventId, patch);
|
|
1071
|
+
sendJSON(res, 200, { event: updated });
|
|
1072
|
+
} catch (e) {
|
|
1073
|
+
sendJSON(res, 500, { error: e.message });
|
|
1074
|
+
}
|
|
1075
|
+
logRequest(method, pathname, 200, Date.now() - start);
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// DELETE /api/calendar/:calId/:eventId — delete event
|
|
1080
|
+
const calDeleteMatch = pathname.match(/^\/api\/calendar\/([^/]+)\/([^/]+)$/);
|
|
1081
|
+
if (method === 'DELETE' && calDeleteMatch) {
|
|
1082
|
+
try {
|
|
1083
|
+
const calendarId = decodeURIComponent(calDeleteMatch[1]);
|
|
1084
|
+
const eventId = decodeURIComponent(calDeleteMatch[2]);
|
|
1085
|
+
await deleteEvent(config, calendarId, eventId);
|
|
1086
|
+
sendJSON(res, 200, { ok: true });
|
|
1087
|
+
} catch (e) {
|
|
1088
|
+
sendJSON(res, 500, { error: e.message });
|
|
1089
|
+
}
|
|
1090
|
+
logRequest(method, pathname, 200, Date.now() - start);
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1030
1094
|
// GET /api/tasks
|
|
1031
1095
|
if (method === 'GET' && pathname === '/api/tasks') {
|
|
1032
1096
|
const tasks = getTasks();
|
|
@@ -2390,11 +2454,26 @@ export async function cmdUI(args) {
|
|
|
2390
2454
|
}
|
|
2391
2455
|
|
|
2392
2456
|
// ── GitHub ───────────────────────────────────────────────────────
|
|
2457
|
+
if (method === 'GET' && pathname === '/api/github/repos') {
|
|
2458
|
+
try {
|
|
2459
|
+
const gh = await import('../services/github.mjs');
|
|
2460
|
+
const data = await gh.listUserRepos(config, 30);
|
|
2461
|
+
sendJSON(res, 200, data);
|
|
2462
|
+
} catch (e) {
|
|
2463
|
+
sendJSON(res, 200, { error: e.message, repos: [] });
|
|
2464
|
+
}
|
|
2465
|
+
logRequest(method, pathname, 200, Date.now() - start);
|
|
2466
|
+
return;
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2393
2469
|
if (method === 'GET' && pathname === '/api/github') {
|
|
2394
2470
|
try {
|
|
2395
2471
|
const gh = await import('../services/github.mjs');
|
|
2396
|
-
const
|
|
2397
|
-
|
|
2472
|
+
const [notifData, repoData] = await Promise.all([
|
|
2473
|
+
gh.listNotificationsRaw(config, 15),
|
|
2474
|
+
gh.listUserRepos(config, 30).catch(() => null),
|
|
2475
|
+
]);
|
|
2476
|
+
sendJSON(res, 200, { notifications: notifData, user: repoData });
|
|
2398
2477
|
} catch (e) {
|
|
2399
2478
|
sendJSON(res, 200, { error: e.message, notifications: [] });
|
|
2400
2479
|
}
|
|
@@ -2525,7 +2604,7 @@ export async function cmdUI(args) {
|
|
|
2525
2604
|
const daysUntil = Math.ceil((thisYear - today) / 86400000);
|
|
2526
2605
|
if (daysUntil <= 90) {
|
|
2527
2606
|
const dateStr = thisYear.toLocaleDateString('en-US', { month: 'long', day: 'numeric' });
|
|
2528
|
-
upcoming.push({ name: c.name, date: dateStr, daysUntil });
|
|
2607
|
+
upcoming.push({ name: c.name, date: dateStr, rawDate: c.birthday, daysUntil, contactId: c.resourceName });
|
|
2529
2608
|
}
|
|
2530
2609
|
}
|
|
2531
2610
|
upcoming.sort((a, b) => a.daysUntil - b.daysUntil);
|
|
@@ -2537,6 +2616,42 @@ export async function cmdUI(args) {
|
|
|
2537
2616
|
return;
|
|
2538
2617
|
}
|
|
2539
2618
|
|
|
2619
|
+
// POST /api/birthdays — create or update birthday on a contact
|
|
2620
|
+
if (method === 'POST' && pathname === '/api/birthdays') {
|
|
2621
|
+
try {
|
|
2622
|
+
const body = await parseBody(req);
|
|
2623
|
+
const gc = await import('../services/google-contacts.mjs');
|
|
2624
|
+
if (body.contactId) {
|
|
2625
|
+
// Update existing contact's birthday
|
|
2626
|
+
await gc.updateContact(config, body.contactId, { birthday: body.date });
|
|
2627
|
+
} else {
|
|
2628
|
+
// Create new contact with just name + birthday
|
|
2629
|
+
const created = await gc.createContact(config, { name: body.name });
|
|
2630
|
+
await gc.updateContact(config, created.resourceName, { birthday: body.date });
|
|
2631
|
+
}
|
|
2632
|
+
sendJSON(res, 200, { ok: true });
|
|
2633
|
+
} catch (e) {
|
|
2634
|
+
sendJSON(res, 500, { error: e.message });
|
|
2635
|
+
}
|
|
2636
|
+
logRequest(method, pathname, 200, Date.now() - start);
|
|
2637
|
+
return;
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
// POST /api/birthdays/delete — clear birthday from a contact
|
|
2641
|
+
if (method === 'POST' && pathname === '/api/birthdays/delete') {
|
|
2642
|
+
try {
|
|
2643
|
+
const body = await parseBody(req);
|
|
2644
|
+
const gc = await import('../services/google-contacts.mjs');
|
|
2645
|
+
// Clear birthday by setting it to empty (remove field)
|
|
2646
|
+
await gc.updateContact(config, body.contactId, { birthday: '' });
|
|
2647
|
+
sendJSON(res, 200, { ok: true });
|
|
2648
|
+
} catch (e) {
|
|
2649
|
+
sendJSON(res, 500, { error: e.message });
|
|
2650
|
+
}
|
|
2651
|
+
logRequest(method, pathname, 200, Date.now() - start);
|
|
2652
|
+
return;
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2540
2655
|
// ── Studio: plan workflow ────────────────────────────────────────
|
|
2541
2656
|
if (pathname === '/api/studio/plan' && method === 'POST') {
|
|
2542
2657
|
const body = await parseBody(req);
|
|
@@ -2762,24 +2877,57 @@ export async function cmdUI(args) {
|
|
|
2762
2877
|
sendToken('[Reading GitHub...] ');
|
|
2763
2878
|
try {
|
|
2764
2879
|
const gh = await import('../services/github.mjs');
|
|
2765
|
-
|
|
2766
|
-
|
|
2880
|
+
if (!config.github?.token) {
|
|
2881
|
+
toolData = 'GitHub token not configured. Run: nha config set github-token YOUR_PAT';
|
|
2882
|
+
} else {
|
|
2883
|
+
const parts = [];
|
|
2884
|
+
// Notifications (always available)
|
|
2885
|
+
try {
|
|
2886
|
+
const notifs = await withTimeout(gh.listNotifications(config, 15), 'GitHubAgent-notifs');
|
|
2887
|
+
if (notifs) parts.push('## GitHub Notifications\n' + notifs);
|
|
2888
|
+
} catch (e) { /* skip */ }
|
|
2889
|
+
// Issues/PRs on configured repo if available
|
|
2890
|
+
const repo = config.github?.defaultRepo || '';
|
|
2891
|
+
if (repo) {
|
|
2892
|
+
try {
|
|
2893
|
+
const issues = await withTimeout(gh.listIssues(config, repo, 'open', 10), 'GitHubAgent-issues');
|
|
2894
|
+
if (issues) parts.push('## Open Issues (' + repo + ')\n' + issues);
|
|
2895
|
+
} catch (e) { /* skip */ }
|
|
2896
|
+
try {
|
|
2897
|
+
const prs = await withTimeout(gh.listPRs(config, repo, 'open', 10), 'GitHubAgent-prs');
|
|
2898
|
+
if (prs) parts.push('## Open PRs (' + repo + ')\n' + prs);
|
|
2899
|
+
} catch (e) { /* skip */ }
|
|
2900
|
+
}
|
|
2901
|
+
toolData = parts.length > 0 ? parts.join('\n\n') : 'No GitHub data available.';
|
|
2902
|
+
}
|
|
2767
2903
|
} catch (e) { toolData = `GitHub read failed: ${e.message}`; }
|
|
2768
2904
|
|
|
2769
2905
|
} else if (agent === 'NotionAgent') {
|
|
2770
2906
|
sendToken('[Searching Notion...] ');
|
|
2771
2907
|
try {
|
|
2772
2908
|
const nt = await import('../services/notion.mjs');
|
|
2773
|
-
|
|
2774
|
-
|
|
2909
|
+
if (!config.notion?.token) {
|
|
2910
|
+
toolData = 'Notion token not configured. Run: nha config set notion-token YOUR_TOKEN';
|
|
2911
|
+
} else {
|
|
2912
|
+
const results = await withTimeout(nt.search(config, stepPrompt, 10), 'NotionAgent');
|
|
2913
|
+
toolData = typeof results === 'string' ? results : JSON.stringify(results);
|
|
2914
|
+
}
|
|
2775
2915
|
} catch (e) { toolData = `Notion search failed: ${e.message}`; }
|
|
2776
2916
|
|
|
2777
2917
|
} else if (agent === 'SlackAgent') {
|
|
2778
2918
|
sendToken('[Reading Slack...] ');
|
|
2779
2919
|
try {
|
|
2780
2920
|
const sl = await import('../services/slack.mjs');
|
|
2781
|
-
|
|
2782
|
-
|
|
2921
|
+
if (!config.slack?.token) {
|
|
2922
|
+
toolData = 'Slack token not configured. Run: nha config set slack-token xoxb-YOUR_TOKEN';
|
|
2923
|
+
} else {
|
|
2924
|
+
const parts = [];
|
|
2925
|
+
try {
|
|
2926
|
+
const channels = await withTimeout(sl.listChannels(config, 10), 'SlackAgent-channels');
|
|
2927
|
+
if (channels) parts.push('## Slack Channels\n' + (typeof channels === 'string' ? channels : JSON.stringify(channels)));
|
|
2928
|
+
} catch (e) { /* skip */ }
|
|
2929
|
+
toolData = parts.length > 0 ? parts.join('\n\n') : 'No Slack data available.';
|
|
2930
|
+
}
|
|
2783
2931
|
} catch (e) { toolData = `Slack read failed: ${e.message}`; }
|
|
2784
2932
|
|
|
2785
2933
|
} else if (agent === 'DriveAgent') {
|
package/src/config.mjs
CHANGED
|
@@ -102,6 +102,7 @@ const DEFAULT_CONFIG = {
|
|
|
102
102
|
},
|
|
103
103
|
github: {
|
|
104
104
|
token: '',
|
|
105
|
+
defaultRepo: '',
|
|
105
106
|
},
|
|
106
107
|
notion: {
|
|
107
108
|
token: '',
|
|
@@ -264,6 +265,8 @@ export function setConfigValue(key, value) {
|
|
|
264
265
|
'proactive-deadlines': 'ops.proactive.deadlines',
|
|
265
266
|
'github-token': 'github.token',
|
|
266
267
|
'gh-token': 'github.token',
|
|
268
|
+
'github-repo': 'github.defaultRepo',
|
|
269
|
+
'gh-repo': 'github.defaultRepo',
|
|
267
270
|
'notion-token': 'notion.token',
|
|
268
271
|
'slack-token': 'slack.token',
|
|
269
272
|
'name': 'profile.name',
|
package/src/constants.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'url';
|
|
|
5
5
|
const __filename = fileURLToPath(import.meta.url);
|
|
6
6
|
const __dirname = path.dirname(__filename);
|
|
7
7
|
|
|
8
|
-
export const VERSION = '13.2.
|
|
8
|
+
export const VERSION = '13.2.59';
|
|
9
9
|
export const BASE_URL = 'https://nothumanallowed.com/cli';
|
|
10
10
|
export const API_BASE = 'https://nothumanallowed.com/api/v1';
|
|
11
11
|
|
package/src/services/github.mjs
CHANGED
|
@@ -166,6 +166,25 @@ export async function listPRsRaw(config, repo, state = 'open', maxResults = 15)
|
|
|
166
166
|
/**
|
|
167
167
|
* Mark all notifications as read.
|
|
168
168
|
*/
|
|
169
|
+
export async function listUserRepos(config, maxResults = 30) {
|
|
170
|
+
const data = await ghFetch(config, `/user/repos?sort=pushed&direction=desc&per_page=${maxResults}&affiliation=owner,collaborator`);
|
|
171
|
+
const user = await ghFetch(config, '/user');
|
|
172
|
+
return {
|
|
173
|
+
login: user.login,
|
|
174
|
+
name: user.name,
|
|
175
|
+
avatar: user.avatar_url,
|
|
176
|
+
repos: (Array.isArray(data) ? data : []).map(r => ({
|
|
177
|
+
full_name: r.full_name,
|
|
178
|
+
description: r.description || '',
|
|
179
|
+
language: r.language || '',
|
|
180
|
+
stars: r.stargazers_count || 0,
|
|
181
|
+
open_issues: r.open_issues_count || 0,
|
|
182
|
+
pushed: r.pushed_at ? r.pushed_at.slice(0, 10) : '',
|
|
183
|
+
private: r.private,
|
|
184
|
+
})),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
169
188
|
export async function markNotificationsRead(config) {
|
|
170
189
|
await ghFetch(config, '/notifications', {
|
|
171
190
|
method: 'PUT',
|
|
@@ -174,6 +174,12 @@ export async function updateEvent(config, calendarId, eventId, patch) {
|
|
|
174
174
|
});
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
export async function deleteEvent(config, calendarId, eventId) {
|
|
178
|
+
await calFetch(config, `/calendars/${encodeURIComponent(calendarId)}/events/${eventId}`, {
|
|
179
|
+
method: 'DELETE',
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
177
183
|
// ── Event Parser ───────────────────────────────────────────────────────────
|
|
178
184
|
|
|
179
185
|
function parseEvent(raw) {
|
|
@@ -281,6 +281,19 @@ export async function updateEvent(config, calendarId, eventId, patch) {
|
|
|
281
281
|
return gc.updateEvent(config, calendarId, eventId, patch);
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
+
export async function deleteEvent(config, calendarId, eventId) {
|
|
285
|
+
const provider = detectMailProvider(config);
|
|
286
|
+
if (!provider) throw new Error('No mail provider authenticated.');
|
|
287
|
+
|
|
288
|
+
if (provider === 'microsoft') {
|
|
289
|
+
const ms = await getMicrosoftCalendar();
|
|
290
|
+
return ms.deleteEvent(config, calendarId, eventId);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const gc = await getGoogleCalendar();
|
|
294
|
+
return gc.deleteEvent(config, calendarId, eventId);
|
|
295
|
+
}
|
|
296
|
+
|
|
284
297
|
/**
|
|
285
298
|
* List events for a date range.
|
|
286
299
|
*/
|
|
@@ -228,6 +228,13 @@ export async function updateEvent(config, calendarId, eventId, patch) {
|
|
|
228
228
|
});
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
export async function deleteEvent(config, calendarId, eventId) {
|
|
232
|
+
const calPath = calendarId === 'primary'
|
|
233
|
+
? `/calendar/events/${eventId}`
|
|
234
|
+
: `/calendars/${calendarId}/events/${eventId}`;
|
|
235
|
+
await graphFetch(config, calPath, { method: 'DELETE' });
|
|
236
|
+
}
|
|
237
|
+
|
|
231
238
|
// ── Event Parser ───────────────────────────────────────────────────────────
|
|
232
239
|
|
|
233
240
|
/**
|
package/src/services/web-ui.mjs
CHANGED
|
@@ -242,7 +242,7 @@ setInterval(updateClock,1000);updateClock();
|
|
|
242
242
|
|
|
243
243
|
// ---- API ----
|
|
244
244
|
function apiGet(p){return fetch(API+p).then(function(r){return r.ok?r.json():null}).catch(function(){return null})}
|
|
245
|
-
function apiPost(p,b){return fetch(API+p,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b)}).then(function(r){
|
|
245
|
+
function apiPost(p,b,m){return fetch(API+p,{method:m||'POST',headers:{'Content-Type':'application/json'},body:b!=null?JSON.stringify(b):undefined}).then(function(r){if(!r.ok)return r.text().then(function(t){throw new Error(t||r.status)});return r.text().then(function(t){try{return JSON.parse(t)}catch(e){return null}})})}
|
|
246
246
|
function apiPatch(p){return fetch(API+p,{method:'PATCH'}).then(function(r){return r.ok?r.json():null}).catch(function(){return null})}
|
|
247
247
|
|
|
248
248
|
// ---- LOAD DATA ----
|
|
@@ -1184,75 +1184,180 @@ function openDayDetail(dateStr){
|
|
|
1184
1184
|
var evts=calEventsCache[dateStr]||[];
|
|
1185
1185
|
var dayLabel=new Date(dateStr+'T12:00:00').toLocaleDateString('en',{weekday:'long',month:'long',day:'numeric',year:'numeric'});
|
|
1186
1186
|
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
h+='<
|
|
1192
|
-
|
|
1193
|
-
evts.
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
h+='<div style="color:var(--
|
|
1202
|
-
x.
|
|
1203
|
-
|
|
1204
|
-
h+='<div style="
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
h+='<div style="
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1187
|
+
function buildDayHtml(){
|
|
1188
|
+
var h='<h2 style="color:var(--green);margin-bottom:4px">'+esc(dayLabel)+'</h2>';
|
|
1189
|
+
h+='<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px">';
|
|
1190
|
+
h+='<span style="color:var(--dim);font-size:11px">'+dateStr+'</span>';
|
|
1191
|
+
h+='<button onclick="openEventForm(null,'+JSON.stringify(dateStr)+')" style="margin-left:auto;background:var(--green3);color:var(--bg);padding:5px 12px;border-radius:var(--r);font-size:12px;font-weight:700">+ Add Event</button>';
|
|
1192
|
+
h+='</div>';
|
|
1193
|
+
if(evts.length===0){
|
|
1194
|
+
h+='<div style="color:var(--dim);padding:20px;text-align:center">No events on this day</div>';
|
|
1195
|
+
} else {
|
|
1196
|
+
evts.forEach(function(x,idx){
|
|
1197
|
+
var timeStr=x.isAllDay?'All day':fmtTime(x.start)+' - '+fmtTime(x.end);
|
|
1198
|
+
var calId=x.calendarId||'primary';
|
|
1199
|
+
h+='<div style="border:1px solid var(--border);border-radius:6px;padding:12px;margin-bottom:10px;background:var(--bg3)">';
|
|
1200
|
+
h+='<div style="display:flex;align-items:flex-start;gap:6px;margin-bottom:4px">';
|
|
1201
|
+
h+='<div style="flex:1"><div style="color:var(--amber);font-weight:700;font-size:13px;margin-bottom:4px">'+esc(timeStr)+'</div>';
|
|
1202
|
+
h+='<div style="color:var(--bright);font-size:15px;font-weight:700;margin-bottom:6px">'+esc(x.summary)+'</div></div>';
|
|
1203
|
+
if(x.id){
|
|
1204
|
+
h+='<div style="display:flex;gap:4px;flex-shrink:0">';
|
|
1205
|
+
h+='<button onclick="openEventForm('+JSON.stringify({id:x.id,calId:calId,summary:x.summary,description:x.description||'',location:x.location||'',start:x.start,end:x.end,isAllDay:x.isAllDay})+','+JSON.stringify(dateStr)+')" style="background:var(--bg2);border:1px solid var(--border);color:var(--text);padding:3px 8px;border-radius:4px;font-size:11px">Edit</button>';
|
|
1206
|
+
h+='<button onclick="deleteCalEvent('+JSON.stringify(calId)+','+JSON.stringify(x.id)+','+JSON.stringify(dateStr)+')" style="background:var(--bg2);border:1px solid var(--red);color:var(--red);padding:3px 8px;border-radius:4px;font-size:11px">Delete</button>';
|
|
1207
|
+
h+='</div>';
|
|
1208
|
+
}
|
|
1209
|
+
h+='</div>';
|
|
1210
|
+
if(x.location)h+='<div style="color:var(--cyan);font-size:12px;margin-bottom:4px">Location: '+esc(x.location)+'</div>';
|
|
1211
|
+
if(x.organizer)h+='<div style="color:var(--dim);font-size:11px;margin-bottom:4px">Organizer: '+esc(x.organizer)+'</div>';
|
|
1212
|
+
if(x.attendees&&x.attendees.length>0){
|
|
1213
|
+
h+='<div style="color:var(--dim);font-size:11px;margin-bottom:4px">Attendees:</div>';
|
|
1214
|
+
x.attendees.forEach(function(a){
|
|
1215
|
+
var status=a.responseStatus==='accepted'?'var(--green)':a.responseStatus==='declined'?'var(--red)':'var(--dim)';
|
|
1216
|
+
h+='<div style="font-size:11px;color:'+status+';padding-left:8px">'+esc(a.name||a.email)+' ('+esc(a.responseStatus)+')</div>';
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
if(x.description){
|
|
1220
|
+
h+='<div style="border-top:1px solid var(--border);margin-top:8px;padding-top:8px;color:var(--text);font-size:12px;white-space:pre-wrap;word-wrap:break-word">'+esc(x.description)+'</div>';
|
|
1221
|
+
}
|
|
1222
|
+
if(x.hangoutLink){
|
|
1223
|
+
h+='<div style="margin-top:8px"><a href="'+esc(x.hangoutLink)+'" target="_blank" style="color:var(--cyan);font-size:12px;font-weight:700">Join Video Call</a></div>';
|
|
1224
|
+
}
|
|
1225
|
+
if(x.htmlLink){
|
|
1226
|
+
h+='<div style="margin-top:4px"><a href="'+esc(x.htmlLink)+'" target="_blank" style="color:var(--dim);font-size:10px">Open in Google Calendar</a></div>';
|
|
1227
|
+
}
|
|
1228
|
+
h+='</div>';
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
return h;
|
|
1218
1232
|
}
|
|
1219
1233
|
|
|
1220
|
-
// Use the agent modal for day detail (read-only mode)
|
|
1221
1234
|
selectedAgent=null;
|
|
1222
1235
|
agentChatHistory=[];
|
|
1223
1236
|
document.getElementById('modalName').textContent=dayLabel;
|
|
1224
1237
|
document.getElementById('modalAgentDesc').textContent='';
|
|
1225
|
-
// Show the day events in the messages area
|
|
1226
1238
|
var msgEl=document.getElementById('agentMessages');
|
|
1227
|
-
if(msgEl){msgEl.innerHTML='<div class="agent-chat__bubble agent-chat__bubble--agent md-body" style="width:100%;max-width:100%;box-sizing:border-box">'+
|
|
1228
|
-
// Hide input footer in read-only mode
|
|
1239
|
+
if(msgEl){msgEl.innerHTML='<div id="dayDetailBody" class="agent-chat__bubble agent-chat__bubble--agent md-body" style="width:100%;max-width:100%;box-sizing:border-box">'+buildDayHtml()+'</div>';}
|
|
1229
1240
|
var footer=document.querySelector('.agent-chat__footer');
|
|
1230
1241
|
if(footer)footer.style.display='none';
|
|
1231
1242
|
document.getElementById('agentModal').classList.add('modal-overlay--open');
|
|
1232
1243
|
}
|
|
1233
1244
|
|
|
1245
|
+
function refreshDayDetail(dateStr){
|
|
1246
|
+
delete calEventsCache[dateStr];
|
|
1247
|
+
apiGet('/api/calendar?date='+dateStr).then(function(r){
|
|
1248
|
+
calEventsCache[dateStr]=(r&&r.events)||[];
|
|
1249
|
+
openDayDetail(dateStr);
|
|
1250
|
+
renderCalendar(document.getElementById('content'));
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
function deleteCalEvent(calId,eventId,dateStr){
|
|
1255
|
+
if(!confirm('Delete this event?'))return;
|
|
1256
|
+
apiPost('/api/calendar/'+encodeURIComponent(calId)+'/'+encodeURIComponent(eventId),null,'DELETE').then(function(){
|
|
1257
|
+
refreshDayDetail(dateStr);
|
|
1258
|
+
}).catch(function(e){alert('Error: '+e.message);});
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
function openEventForm(evt,dateStr){
|
|
1262
|
+
var isEdit=evt&&evt.id;
|
|
1263
|
+
var defDate=dateStr||new Date().toISOString().split('T')[0];
|
|
1264
|
+
var defStart=evt&&evt.start?evt.start:defDate+'T09:00';
|
|
1265
|
+
var defEnd=evt&&evt.end?evt.end:defDate+'T10:00';
|
|
1266
|
+
if(defStart.length>16)defStart=defStart.slice(0,16);
|
|
1267
|
+
if(defEnd.length>16)defEnd=defEnd.slice(0,16);
|
|
1268
|
+
|
|
1269
|
+
var overlay=document.createElement('div');
|
|
1270
|
+
overlay.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:10000;display:flex;align-items:center;justify-content:center';
|
|
1271
|
+
var card=document.createElement('div');
|
|
1272
|
+
card.style.cssText='background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:24px;width:420px;max-width:95vw;max-height:90vh;overflow-y:auto';
|
|
1273
|
+
card.innerHTML='<div style="font-size:16px;font-weight:700;color:var(--bright);margin-bottom:16px">'+(isEdit?'Edit Event':'New Event')+'</div>'+
|
|
1274
|
+
'<label style="font-size:12px;color:var(--dim);display:block;margin-bottom:4px">Title *</label>'+
|
|
1275
|
+
'<input id="evtTitle" type="text" value="'+esc(evt&&evt.summary||'')+'" placeholder="Event title" style="width:100%;box-sizing:border-box;padding:8px 10px;margin-bottom:12px;font-size:13px">'+
|
|
1276
|
+
'<label style="font-size:12px;color:var(--dim);display:block;margin-bottom:4px">Start</label>'+
|
|
1277
|
+
'<input id="evtStart" type="datetime-local" value="'+esc(defStart)+'" style="width:100%;box-sizing:border-box;padding:8px 10px;margin-bottom:12px;font-size:13px">'+
|
|
1278
|
+
'<label style="font-size:12px;color:var(--dim);display:block;margin-bottom:4px">End</label>'+
|
|
1279
|
+
'<input id="evtEnd" type="datetime-local" value="'+esc(defEnd)+'" style="width:100%;box-sizing:border-box;padding:8px 10px;margin-bottom:12px;font-size:13px">'+
|
|
1280
|
+
'<label style="font-size:12px;color:var(--dim);display:block;margin-bottom:4px">Location</label>'+
|
|
1281
|
+
'<input id="evtLoc" type="text" value="'+esc(evt&&evt.location||'')+'" placeholder="Optional" style="width:100%;box-sizing:border-box;padding:8px 10px;margin-bottom:12px;font-size:13px">'+
|
|
1282
|
+
'<label style="font-size:12px;color:var(--dim);display:block;margin-bottom:4px">Description</label>'+
|
|
1283
|
+
'<textarea id="evtDesc" style="width:100%;box-sizing:border-box;padding:8px 10px;margin-bottom:16px;font-size:13px;height:70px;resize:vertical">'+esc(evt&&evt.description||'')+'</textarea>'+
|
|
1284
|
+
'<div style="display:flex;gap:8px;justify-content:flex-end">'+
|
|
1285
|
+
'<button id="evtCancelBtn" style="background:var(--bg3);border:1px solid var(--border);color:var(--text);padding:8px 18px;border-radius:var(--r);font-size:13px">Cancel</button>'+
|
|
1286
|
+
'<button id="evtSaveBtn" style="background:var(--green3);color:var(--bg);padding:8px 18px;border-radius:var(--r);font-size:13px;font-weight:700">'+(isEdit?'Save Changes':'Create Event')+'</button>'+
|
|
1287
|
+
'</div><div id="evtErr" style="color:var(--red);font-size:12px;margin-top:8px"></div>';
|
|
1288
|
+
overlay.appendChild(card);
|
|
1289
|
+
document.body.appendChild(overlay);
|
|
1290
|
+
card.querySelector('#evtCancelBtn').onclick=function(){document.body.removeChild(overlay);};
|
|
1291
|
+
overlay.onclick=function(e){if(e.target===overlay)document.body.removeChild(overlay);};
|
|
1292
|
+
card.querySelector('#evtSaveBtn').onclick=function(){
|
|
1293
|
+
var title=card.querySelector('#evtTitle').value.trim();
|
|
1294
|
+
if(!title){card.querySelector('#evtErr').textContent='Title is required';return;}
|
|
1295
|
+
var startVal=card.querySelector('#evtStart').value;
|
|
1296
|
+
var endVal=card.querySelector('#evtEnd').value;
|
|
1297
|
+
var loc=card.querySelector('#evtLoc').value.trim();
|
|
1298
|
+
var desc=card.querySelector('#evtDesc').value.trim();
|
|
1299
|
+
var btn=card.querySelector('#evtSaveBtn');
|
|
1300
|
+
btn.textContent='Saving...';btn.disabled=true;
|
|
1301
|
+
var promise;
|
|
1302
|
+
if(isEdit){
|
|
1303
|
+
var calId=evt.calId||'primary';
|
|
1304
|
+
var patch={summary:title,start:startVal,end:endVal};
|
|
1305
|
+
if(loc)patch.location=loc;
|
|
1306
|
+
if(desc)patch.description=desc;
|
|
1307
|
+
promise=apiPost('/api/calendar/'+encodeURIComponent(calId)+'/'+encodeURIComponent(evt.id),patch,'PATCH');
|
|
1308
|
+
} else {
|
|
1309
|
+
promise=apiPost('/api/calendar',{summary:title,start:startVal,end:endVal,location:loc,description:desc,date:dateStr});
|
|
1310
|
+
}
|
|
1311
|
+
promise.then(function(){
|
|
1312
|
+
document.body.removeChild(overlay);
|
|
1313
|
+
refreshDayDetail(dateStr);
|
|
1314
|
+
}).catch(function(e){
|
|
1315
|
+
card.querySelector('#evtErr').textContent='Error: '+(e.message||'Unknown error');
|
|
1316
|
+
btn.textContent=isEdit?'Save Changes':'Create Event';btn.disabled=false;
|
|
1317
|
+
});
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1234
1321
|
// ---- GITHUB ----
|
|
1235
1322
|
var ghData=null;var ghRepo='';
|
|
1236
1323
|
function renderGitHub(el){
|
|
1237
1324
|
function renderGhData(r){
|
|
1238
|
-
var
|
|
1325
|
+
var user=r.user||null;
|
|
1326
|
+
// Header: user profile + repo input
|
|
1327
|
+
var userHtml='';
|
|
1328
|
+
if(user&&user.login){
|
|
1329
|
+
userHtml='<div style="display:flex;align-items:center;gap:10px;margin-bottom:14px;padding:10px 14px;background:var(--bg2);border-radius:var(--r);border:1px solid var(--border)">'+(user.avatar?'<img src="'+esc(user.avatar)+'" style="width:36px;height:36px;border-radius:50%;object-fit:cover" alt="">':'')+'<div style="flex:1"><div style="font-weight:700;font-size:13px;color:var(--green)">@'+esc(user.login)+'</div>'+(user.name?'<div style="font-size:11px;color:var(--dim)">'+esc(user.name)+'</div>':'')+'</div><button onclick="disconnectService(\\x27github-token\\x27,function(el){ghData=null;renderGitHub(el)})" style="background:var(--bg3);border:1px solid var(--red);color:var(--red);padding:4px 10px;border-radius:var(--r);font-size:11px">Disconnect</button></div>';
|
|
1330
|
+
}
|
|
1331
|
+
var h=userHtml+'<div style="display:flex;gap:8px;margin-bottom:8px;flex-wrap:wrap"><input type="text" id="ghRepo" placeholder="owner/repo" value="'+esc(ghRepo)+'" style="flex:1;min-width:180px;font-size:13px;padding:10px 14px" onkeydown="if(event.key===\\x27Enter\\x27)loadGhIssues()"><button onclick="loadGhIssues()" style="background:var(--green3);color:var(--bg);padding:8px 16px;border-radius:var(--r);font-weight:700;font-size:12px">Issues</button><button onclick="loadGhPRs()" style="background:var(--cyan);color:var(--bg);padding:8px 16px;border-radius:var(--r);font-weight:700;font-size:12px">PRs</button></div>';
|
|
1332
|
+
// My repos as clickable pills
|
|
1333
|
+
if(user&&user.repos&&user.repos.length>0){
|
|
1334
|
+
h+='<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px">';
|
|
1335
|
+
user.repos.slice(0,12).forEach(function(repo){
|
|
1336
|
+
h+='<button onclick="ghRepo=\\x27'+esc(repo.full_name)+'\\x27;document.getElementById(\\x27ghRepo\\x27).value=\\x27'+esc(repo.full_name)+'\\x27;loadGhIssues()" style="background:var(--bg3);border:1px solid var(--border);border-radius:20px;padding:4px 10px;font-size:11px;color:var(--fg);cursor:pointer;white-space:nowrap" title="'+esc(repo.description)+(repo.open_issues?' | '+repo.open_issues+' open issues':'')+'">'+(repo.private?'🔒 ':'')+esc(repo.full_name)+(repo.open_issues?'<span style="color:var(--amber);margin-left:4px">'+repo.open_issues+'</span>':'')+'</button>';
|
|
1337
|
+
});
|
|
1338
|
+
h+='</div>';
|
|
1339
|
+
}
|
|
1340
|
+
// Notifications
|
|
1239
1341
|
var notifs=r.notifications||[];
|
|
1240
1342
|
if(notifs.length>0){
|
|
1241
1343
|
h+='<div style="display:flex;align-items:center;justify-content:space-between"><div class="section-title">Notifications ('+notifs.length+')</div><button onclick="ghMarkRead()" style="background:var(--bg3);color:var(--dim);border:1px solid var(--border);padding:4px 10px;border-radius:var(--r);font-size:10px;cursor:pointer">Mark all read</button></div>';
|
|
1242
1344
|
notifs.forEach(function(n){h+='<div class="card" style="padding:10px 14px;cursor:pointer" onclick="window.open(\\x27'+esc(n.url)+'\\x27,\\x27_blank\\x27)"><span style="color:var(--cyan);font-size:11px">'+esc(n.repo)+'</span> <span style="color:var(--dim);font-size:10px">['+esc(n.type)+']</span><div style="font-size:13px;margin-top:2px">'+esc(n.title)+'</div><div style="font-size:10px;color:var(--dim)">'+esc(n.reason)+' · '+esc(n.updated)+'</div></div>'});
|
|
1243
1345
|
}
|
|
1346
|
+
// Issues
|
|
1244
1347
|
if(r.issues&&r.issues.length>0){
|
|
1245
1348
|
h+='<div class="section-title">Issues — '+esc(r.repo||ghRepo)+'</div>';
|
|
1246
1349
|
r.issues.forEach(function(i){h+='<div class="card" style="padding:10px 14px;cursor:pointer" onclick="window.open(\\x27'+esc(i.url)+'\\x27,\\x27_blank\\x27)"><span style="color:var(--green);font-weight:700">#'+i.number+'</span> '+esc(i.title)+(i.assignee?' <span style="font-size:10px;color:var(--cyan)">→ '+esc(i.assignee)+'</span>':'')+(i.labels?'<span style="font-size:9px;color:var(--amber);margin-left:6px">['+esc(i.labels)+']</span>':'')+'<div style="font-size:10px;color:var(--dim)">'+esc(i.updated)+'</div></div>'});
|
|
1247
1350
|
}
|
|
1351
|
+
// PRs
|
|
1248
1352
|
if(r.prs&&r.prs.length>0){
|
|
1249
1353
|
h+='<div class="section-title">Pull Requests — '+esc(r.repo||ghRepo)+'</div>';
|
|
1250
1354
|
r.prs.forEach(function(p){h+='<div class="card" style="padding:10px 14px;cursor:pointer" onclick="window.open(\\x27'+esc(p.url)+'\\x27,\\x27_blank\\x27)"><span style="color:var(--cyan);font-weight:700">#'+p.number+'</span> '+esc(p.title)+' <span style="font-size:10px;color:var(--dim)">by '+esc(p.author)+'</span>'+(p.draft?'<span style="font-size:9px;color:var(--amber)"> DRAFT</span>':'')+'<div style="font-size:10px;color:var(--dim)">'+esc(p.updated)+'</div></div>'});
|
|
1251
1355
|
}
|
|
1252
|
-
if(!notifs.length&&!(r.issues&&r.issues.length)&&!(r.prs&&r.prs.length)){
|
|
1356
|
+
if(!notifs.length&&!(r.issues&&r.issues.length)&&!(r.prs&&r.prs.length)){
|
|
1357
|
+
h+='<div class="card" style="text-align:center;color:var(--dim);padding:20px">Click a repo above or type owner/repo and click Issues or PRs.</div>';
|
|
1358
|
+
}
|
|
1253
1359
|
el.innerHTML=h;
|
|
1254
1360
|
}
|
|
1255
|
-
// If ghData already loaded (e.g. after loadGhIssues/loadGhPRs), render directly without re-fetching
|
|
1256
1361
|
if(ghData){renderGhData(ghData);return;}
|
|
1257
1362
|
el.innerHTML='<div style="text-align:center;padding:40px"><div class="spinner"></div><div style="color:var(--dim)">Loading GitHub...</div></div>';
|
|
1258
1363
|
apiGet('/api/github').then(function(r){
|
|
@@ -1261,13 +1366,20 @@ function renderGitHub(el){
|
|
|
1261
1366
|
renderGhData(r);
|
|
1262
1367
|
});
|
|
1263
1368
|
}
|
|
1264
|
-
function loadGhIssues(){var inp=document.getElementById('ghRepo');if(!inp||!inp.value.trim())return;ghRepo=inp.value.trim();var el=document.getElementById('content');el.innerHTML='<div style="text-align:center;padding:40px"><div class="spinner"></div></div>';apiGet('/api/github/issues?repo='+encodeURIComponent(ghRepo)).then(function(r){if(ghData){ghData.issues=r.issues||[];ghData.prs=ghData.prs||[];ghData.repo=r.repo}else{ghData={issues:r.issues||[],prs:[],notifications:[]}}renderGitHub(document.getElementById('content'))})}
|
|
1369
|
+
function loadGhIssues(){var inp=document.getElementById('ghRepo');if(!inp||!inp.value.trim())return;ghRepo=inp.value.trim();apiPost('/api/config',{key:'github-repo',value:ghRepo}).catch(function(){});var el=document.getElementById('content');el.innerHTML='<div style="text-align:center;padding:40px"><div class="spinner"></div></div>';apiGet('/api/github/issues?repo='+encodeURIComponent(ghRepo)).then(function(r){if(ghData){ghData.issues=r.issues||[];ghData.prs=ghData.prs||[];ghData.repo=r.repo}else{ghData={issues:r.issues||[],prs:[],notifications:[]}}renderGitHub(document.getElementById('content'))})}
|
|
1265
1370
|
function loadGhPRs(){var inp=document.getElementById('ghRepo');if(!inp||!inp.value.trim())return;ghRepo=inp.value.trim();var el=document.getElementById('content');el.innerHTML='<div style="text-align:center;padding:40px"><div class="spinner"></div></div>';apiGet('/api/github/prs?repo='+encodeURIComponent(ghRepo)).then(function(r){if(ghData){ghData.prs=r.prs||[];ghData.issues=ghData.issues||[];ghData.repo=r.repo}else{ghData={prs:r.prs||[],issues:[],notifications:[]}}renderGitHub(document.getElementById('content'))})}
|
|
1266
1371
|
function ghMarkRead(){apiPost('/api/github/mark-read',{}).then(function(){if(ghData)ghData.notifications=[];renderGitHub(document.getElementById('content'))})}
|
|
1267
1372
|
|
|
1268
1373
|
// ---- NOTION ----
|
|
1269
1374
|
function renderNotion(el){
|
|
1270
|
-
|
|
1375
|
+
apiGet('/api/notion/search?q=').then(function(r){
|
|
1376
|
+
var isOk=!(r&&r.error);
|
|
1377
|
+
var banner=isOk?'':''+setupBanner('Notion','nha config set notion-token YOUR_INTEGRATION_TOKEN')+'<div style="color:var(--dim);font-size:12px;padding:8px 0">Get an Integration Token from notion.so/my-integrations → New integration → Internal → copy Secret</div>';
|
|
1378
|
+
var disconnectBtn=isOk?'<button onclick="disconnectService(\\x27notion-token\\x27,renderNotion)" style="background:var(--bg3);border:1px solid var(--red);color:var(--red);padding:4px 10px;border-radius:var(--r);font-size:11px;margin-bottom:12px">Disconnect Notion</button>':'';
|
|
1379
|
+
el.innerHTML=banner+disconnectBtn+'<div style="display:flex;gap:8px;margin-bottom:16px"><input type="text" id="notionQuery" placeholder="Search Notion pages..." style="flex:1;font-size:13px;padding:10px 14px" onkeydown="if(event.key===\\x27Enter\\x27)searchNotion()">'+(isOk?'<button onclick="searchNotion()" style="background:var(--green3);color:var(--bg);padding:8px 16px;border-radius:var(--r);font-weight:700;font-size:12px">Search</button>':'<button style="background:var(--bg3);color:var(--dim);padding:8px 16px;border-radius:var(--r);font-size:12px" disabled>Search</button>')+'</div><div id="notionResults"></div>';
|
|
1380
|
+
}).catch(function(){
|
|
1381
|
+
el.innerHTML=setupBanner('Notion','nha config set notion-token YOUR_INTEGRATION_TOKEN')+'<div style="display:flex;gap:8px;margin-bottom:16px"><input type="text" id="notionQuery" placeholder="Search Notion pages..." style="flex:1;font-size:13px;padding:10px 14px"><button style="background:var(--bg3);color:var(--dim);padding:8px 16px;border-radius:var(--r);font-size:12px" disabled>Search</button></div>';
|
|
1382
|
+
});
|
|
1271
1383
|
}
|
|
1272
1384
|
function searchNotion(){
|
|
1273
1385
|
var q=document.getElementById('notionQuery');if(!q||!q.value.trim())return;
|
|
@@ -1289,14 +1401,22 @@ function loadNotionPage(id){
|
|
|
1289
1401
|
}
|
|
1290
1402
|
|
|
1291
1403
|
// ---- SLACK ----
|
|
1404
|
+
function disconnectService(configKey,renderFn){
|
|
1405
|
+
if(!confirm('Remove this connection? You can reconnect anytime.'))return;
|
|
1406
|
+
apiPost('/api/config',{key:configKey,value:''}).then(function(){
|
|
1407
|
+
var el=document.getElementById('content');
|
|
1408
|
+
if(el&&renderFn)renderFn(el);
|
|
1409
|
+
}).catch(function(){});
|
|
1410
|
+
}
|
|
1411
|
+
function setupBanner(service,cmd){return '<div style="display:flex;align-items:center;gap:12px;padding:12px 16px;background:var(--bg2);border:1px solid var(--border);border-left:3px solid var(--amber);border-radius:var(--r);margin-bottom:14px;font-size:12px"><span style="font-size:20px">🔒</span><div><div style="color:var(--fg);font-weight:600;margin-bottom:2px">'+esc(service)+' not configured</div><div style="color:var(--dim);font-family:var(--mono);font-size:11px">'+esc(cmd)+'</div></div></div>';}
|
|
1292
1412
|
var slackData=null;
|
|
1293
1413
|
function renderSlack(el){
|
|
1294
1414
|
el.innerHTML='<div style="text-align:center;padding:40px"><div class="spinner"></div><div style="color:var(--dim)">Loading Slack channels...</div></div>';
|
|
1295
1415
|
apiGet('/api/slack/channels').then(function(r){
|
|
1296
|
-
if(r&&r.error){el.innerHTML='
|
|
1416
|
+
if(r&&r.error){el.innerHTML=setupBanner('Slack','nha config set slack-token xoxb-YOUR_TOKEN')+'<div style="color:var(--dim);font-size:12px;padding:8px 0">Get a Bot Token from api.slack.com/apps → OAuth & Permissions → Bot Token Scopes: channels:read, channels:history, users:read</div>';return}
|
|
1297
1417
|
slackData=r;
|
|
1298
1418
|
var channels=r.channels||[];
|
|
1299
|
-
var h='<div class="section-title">Channels ('+channels.length+')</div>';
|
|
1419
|
+
var h='<div style="display:flex;align-items:center;margin-bottom:8px"><div class="section-title" style="margin:0;flex:1">Channels ('+channels.length+')</div><button onclick="disconnectService(\\x27slack-token\\x27,renderSlack)" style="background:var(--bg3);border:1px solid var(--red);color:var(--red);padding:4px 10px;border-radius:var(--r);font-size:11px">Disconnect</button></div>';
|
|
1300
1420
|
if(channels.length===0){h+='<div class="card" style="text-align:center;color:var(--dim);padding:20px">No channels found</div>'}
|
|
1301
1421
|
channels.forEach(function(c){h+='<div class="card" style="padding:10px 14px;cursor:pointer" onclick="loadSlackChannel(\\x27'+esc(c.id)+'\\x27,\\x27'+esc(c.name)+'\\x27)"><span style="color:var(--green);font-weight:700">#'+esc(c.name)+'</span> <span style="font-size:10px;color:var(--dim)">'+c.members+' members</span>'+(c.purpose?'<div style="font-size:11px;color:var(--dim);margin-top:2px">'+esc(c.purpose)+'</div>':'')+'</div>'});
|
|
1302
1422
|
h+='<div id="slackMessages"></div>';
|
|
@@ -1322,17 +1442,75 @@ function renderBirthdays(el){
|
|
|
1322
1442
|
apiGet('/api/birthdays').then(function(r){
|
|
1323
1443
|
if(r&&r.error){el.innerHTML='<div class="card" style="text-align:center;padding:30px"><div style="color:var(--dim)">'+esc(r.error)+'</div></div>';return}
|
|
1324
1444
|
var bdays=r.birthdays||[];
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
h+='<div class="card" style="padding:
|
|
1331
|
-
}
|
|
1445
|
+
var h='<div style="display:flex;align-items:center;margin-bottom:12px">';
|
|
1446
|
+
h+='<div class="section-title" style="margin:0;flex:1">Upcoming Birthdays</div>';
|
|
1447
|
+
h+='<button onclick="openBirthdayForm(null)" style="background:var(--green3);color:var(--bg);padding:5px 14px;border-radius:var(--r);font-size:12px;font-weight:700">+ Add Birthday</button>';
|
|
1448
|
+
h+='</div>';
|
|
1449
|
+
if(bdays.length===0){
|
|
1450
|
+
h+='<div class="card" style="text-align:center;padding:30px;color:var(--dim)">No upcoming birthdays found.<br><span style="font-size:11px">Add one above, or add birthdays to your Google Contacts.</span></div>';
|
|
1451
|
+
} else {
|
|
1452
|
+
bdays.forEach(function(b){
|
|
1453
|
+
var isToday=b.daysUntil===0;
|
|
1454
|
+
var label=isToday?'<span style="color:var(--red);font-weight:700">TODAY!</span>':b.daysUntil===1?'<span style="color:var(--amber)">Tomorrow</span>':'<span style="color:var(--dim)">in '+b.daysUntil+' days</span>';
|
|
1455
|
+
h+='<div class="card" style="padding:10px 14px;display:flex;align-items:center;gap:8px'+(isToday?';border-color:var(--red)':'')+'"><span style="font-size:18px">🎂</span><div style="flex:1"><div style="font-weight:700;font-size:13px">'+esc(b.name)+'</div><div style="font-size:11px;color:var(--dim)">'+esc(b.date)+'</div></div><div style="margin-right:8px">'+label+'</div>';
|
|
1456
|
+
if(b.contactId){
|
|
1457
|
+
h+='<button onclick="openBirthdayForm('+JSON.stringify({contactId:b.contactId,name:b.name,date:b.date})+')" style="background:var(--bg3);border:1px solid var(--border);color:var(--text);padding:3px 8px;border-radius:4px;font-size:11px">Edit</button>';
|
|
1458
|
+
h+='<button onclick="deleteBirthday('+JSON.stringify(b.contactId)+','+JSON.stringify(b.name)+')" style="background:var(--bg3);border:1px solid var(--red);color:var(--red);padding:3px 8px;border-radius:4px;font-size:11px;margin-left:4px">Delete</button>';
|
|
1459
|
+
}
|
|
1460
|
+
h+='</div>';
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1332
1463
|
el.innerHTML=h;
|
|
1333
1464
|
});
|
|
1334
1465
|
}
|
|
1335
1466
|
|
|
1467
|
+
function openBirthdayForm(b){
|
|
1468
|
+
var isEdit=b&&b.contactId;
|
|
1469
|
+
var overlay=document.createElement('div');
|
|
1470
|
+
overlay.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:10000;display:flex;align-items:center;justify-content:center';
|
|
1471
|
+
var card=document.createElement('div');
|
|
1472
|
+
card.style.cssText='background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:24px;width:360px;max-width:95vw';
|
|
1473
|
+
card.innerHTML='<div style="font-size:16px;font-weight:700;color:var(--bright);margin-bottom:16px">'+(isEdit?'Edit Birthday':'Add Birthday')+'</div>'+
|
|
1474
|
+
'<label style="font-size:12px;color:var(--dim);display:block;margin-bottom:4px">Name *</label>'+
|
|
1475
|
+
'<input id="bdayName" type="text" value="'+esc(b&&b.name||'')+'" placeholder="Contact name" style="width:100%;box-sizing:border-box;padding:8px 10px;margin-bottom:12px;font-size:13px">'+
|
|
1476
|
+
'<label style="font-size:12px;color:var(--dim);display:block;margin-bottom:4px">Birthday (MM-DD or YYYY-MM-DD)</label>'+
|
|
1477
|
+
'<input id="bdayDate" type="text" value="'+esc(b&&b.date||'')+'" placeholder="e.g. 03-15 or 1990-03-15" style="width:100%;box-sizing:border-box;padding:8px 10px;margin-bottom:16px;font-size:13px">'+
|
|
1478
|
+
'<div style="font-size:11px;color:var(--dim);margin-bottom:16px">Birthday will be saved as a Google Calendar event on the specified date.</div>'+
|
|
1479
|
+
'<div style="display:flex;gap:8px;justify-content:flex-end">'+
|
|
1480
|
+
'<button id="bdayCancelBtn" style="background:var(--bg3);border:1px solid var(--border);color:var(--text);padding:8px 18px;border-radius:var(--r);font-size:13px">Cancel</button>'+
|
|
1481
|
+
'<button id="bdaySaveBtn" style="background:var(--green3);color:var(--bg);padding:8px 18px;border-radius:var(--r);font-size:13px;font-weight:700">'+(isEdit?'Save':'Add')+'</button>'+
|
|
1482
|
+
'</div><div id="bdayErr" style="color:var(--red);font-size:12px;margin-top:8px"></div>';
|
|
1483
|
+
overlay.appendChild(card);
|
|
1484
|
+
document.body.appendChild(overlay);
|
|
1485
|
+
card.querySelector('#bdayCancelBtn').onclick=function(){document.body.removeChild(overlay);};
|
|
1486
|
+
overlay.onclick=function(e){if(e.target===overlay)document.body.removeChild(overlay);};
|
|
1487
|
+
card.querySelector('#bdaySaveBtn').onclick=function(){
|
|
1488
|
+
var name=card.querySelector('#bdayName').value.trim();
|
|
1489
|
+
var date=card.querySelector('#bdayDate').value.trim();
|
|
1490
|
+
if(!name){card.querySelector('#bdayErr').textContent='Name is required';return;}
|
|
1491
|
+
if(!date){card.querySelector('#bdayErr').textContent='Date is required';return;}
|
|
1492
|
+
var btn=card.querySelector('#bdaySaveBtn');
|
|
1493
|
+
btn.textContent='Saving...';btn.disabled=true;
|
|
1494
|
+
// Parse date into a full date for the calendar event
|
|
1495
|
+
var fullDate=date;
|
|
1496
|
+
if(/^\d{2}-\d{2}$/.test(date))fullDate=new Date().getFullYear()+'-'+date;
|
|
1497
|
+
apiPost('/api/birthdays',{name:name,date:fullDate,contactId:isEdit?b.contactId:null,edit:isEdit}).then(function(){
|
|
1498
|
+
document.body.removeChild(overlay);
|
|
1499
|
+
renderBirthdays(document.getElementById('content'));
|
|
1500
|
+
}).catch(function(e){
|
|
1501
|
+
card.querySelector('#bdayErr').textContent='Error: '+(e.message||'Unknown error');
|
|
1502
|
+
btn.textContent=isEdit?'Save':'Add';btn.disabled=false;
|
|
1503
|
+
});
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
function deleteBirthday(contactId,name){
|
|
1508
|
+
if(!confirm('Remove birthday for '+name+'?'))return;
|
|
1509
|
+
apiPost('/api/birthdays/delete',{contactId:contactId}).then(function(){
|
|
1510
|
+
renderBirthdays(document.getElementById('content'));
|
|
1511
|
+
}).catch(function(e){alert('Error: '+e.message);});
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1336
1514
|
// ---- AGENTS ----
|
|
1337
1515
|
var AGENT_DESCRIPTIONS = {
|
|
1338
1516
|
saber:'Security audits, OWASP Top 10, threat modeling, pentest planning',
|