nothumanallowed 2.1.0 → 3.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,788 @@
1
+ /**
2
+ * Web UI — Single HTML page served from memory.
3
+ * Terminal/hacker aesthetic matching NHA's green-on-black style.
4
+ * Zero dependencies. All CSS + JS inline.
5
+ */
6
+
7
+ export function getHTML(port) {
8
+ return `<!DOCTYPE html>
9
+ <html lang="en">
10
+ <head>
11
+ <meta charset="utf-8">
12
+ <meta name="viewport" content="width=device-width,initial-scale=1">
13
+ <title>NHA — Local Operations Console</title>
14
+ <style>
15
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
16
+ :root{
17
+ --bg:#0a0a0a;--bg2:#111;--bg3:#1a1a1a;--bg4:#222;
18
+ --green:#00ff41;--green2:#00cc33;--green3:#00aa28;--greendim:#0a3a12;
19
+ --cyan:#00e5ff;--amber:#ffb300;--red:#ff1744;--magenta:#e040fb;
20
+ --text:#c8c8c8;--textdim:#666;--textbright:#fff;
21
+ --border:#1e1e1e;--borderbright:#333;
22
+ --mono:'JetBrains Mono','Fira Code','SF Mono','Cascadia Code','Consolas',monospace;
23
+ --radius:6px;
24
+ }
25
+ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--mono);font-size:13px;line-height:1.5;overflow:hidden}
26
+ a{color:var(--cyan);text-decoration:none}
27
+ a:hover{text-decoration:underline}
28
+ button{font-family:var(--mono);cursor:pointer;border:none;outline:none}
29
+ input,textarea{font-family:var(--mono);background:var(--bg2);color:var(--text);border:1px solid var(--border);padding:8px 12px;border-radius:var(--radius);outline:none;font-size:13px}
30
+ input:focus,textarea:focus{border-color:var(--green3)}
31
+ ::-webkit-scrollbar{width:6px;height:6px}
32
+ ::-webkit-scrollbar-track{background:var(--bg)}
33
+ ::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
34
+ ::-webkit-scrollbar-thumb:hover{background:var(--borderbright)}
35
+
36
+ /* Layout */
37
+ #app{display:flex;height:100vh;width:100vw}
38
+ #sidebar{width:220px;min-width:220px;background:var(--bg2);border-right:1px solid var(--border);display:flex;flex-direction:column;padding:0;overflow-y:auto}
39
+ #main{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0}
40
+ #header{padding:12px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:12px;background:var(--bg)}
41
+ #content{flex:1;overflow-y:auto;padding:20px}
42
+
43
+ /* Sidebar */
44
+ .sidebar-brand{padding:16px;border-bottom:1px solid var(--border)}
45
+ .sidebar-brand h1{font-size:16px;color:var(--green);font-weight:700;letter-spacing:2px}
46
+ .sidebar-brand p{font-size:10px;color:var(--textdim);margin-top:2px}
47
+ .nav-section{padding:12px 0}
48
+ .nav-section-label{padding:0 16px;font-size:10px;text-transform:uppercase;letter-spacing:1.5px;color:var(--textdim);margin-bottom:4px}
49
+ .nav-item{display:flex;align-items:center;gap:10px;padding:8px 16px;color:var(--textdim);cursor:pointer;transition:all .15s;font-size:12px;border-left:2px solid transparent}
50
+ .nav-item:hover{color:var(--text);background:var(--bg3)}
51
+ .nav-item.active{color:var(--green);background:var(--greendim);border-left-color:var(--green)}
52
+ .nav-item .icon{width:16px;text-align:center;font-size:14px}
53
+ .nav-item .badge{margin-left:auto;background:var(--green3);color:var(--bg);padding:1px 6px;border-radius:8px;font-size:10px;font-weight:700}
54
+
55
+ /* Cards */
56
+ .card{background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius);padding:16px;margin-bottom:12px}
57
+ .card-title{font-size:11px;text-transform:uppercase;letter-spacing:1px;color:var(--textdim);margin-bottom:8px}
58
+ .card-value{font-size:24px;font-weight:700;color:var(--green)}
59
+ .card-sub{font-size:11px;color:var(--textdim);margin-top:4px}
60
+
61
+ /* Dashboard grid */
62
+ .dash-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px;margin-bottom:20px}
63
+ .dash-section{margin-bottom:24px}
64
+ .dash-section h2{font-size:13px;color:var(--cyan);margin-bottom:12px;text-transform:uppercase;letter-spacing:1px}
65
+
66
+ /* Chat */
67
+ #chat-view{display:flex;flex-direction:column;height:100%;overflow:hidden}
68
+ #chat-messages{flex:1;overflow-y:auto;padding:0 0 12px 0}
69
+ .msg{margin-bottom:12px;display:flex;gap:10px}
70
+ .msg-user .msg-bubble{background:var(--bg3);border:1px solid var(--borderbright);border-radius:8px 8px 2px 8px;padding:10px 14px;max-width:80%;margin-left:auto;color:var(--textbright)}
71
+ .msg-assistant .msg-bubble{background:var(--greendim);border:1px solid var(--green3);border-radius:8px 8px 8px 2px;padding:10px 14px;max-width:85%;color:var(--text);white-space:pre-wrap;word-wrap:break-word}
72
+ .msg-label{font-size:10px;color:var(--textdim);margin-bottom:2px}
73
+ .msg-thinking{color:var(--textdim);font-style:italic;animation:pulse 1.5s infinite}
74
+ @keyframes pulse{0%,100%{opacity:.4}50%{opacity:1}}
75
+ #chat-input-bar{display:flex;gap:8px;padding:12px 0 0 0;border-top:1px solid var(--border)}
76
+ #chat-input{flex:1;resize:none;min-height:40px;max-height:120px;padding:10px 14px}
77
+ #chat-send{background:var(--green3);color:var(--bg);padding:10px 20px;border-radius:var(--radius);font-weight:700;font-size:12px;transition:background .15s}
78
+ #chat-send:hover{background:var(--green2)}
79
+ #chat-send:disabled{opacity:.4;cursor:not-allowed}
80
+
81
+ /* Tasks */
82
+ .task-item{display:flex;align-items:center;gap:10px;padding:10px 12px;border-bottom:1px solid var(--border);transition:background .1s}
83
+ .task-item:hover{background:var(--bg3)}
84
+ .task-check{width:18px;height:18px;border:2px solid var(--borderbright);border-radius:4px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .15s;flex-shrink:0}
85
+ .task-check:hover{border-color:var(--green)}
86
+ .task-check.done{background:var(--green3);border-color:var(--green)}
87
+ .task-check.done::after{content:'\\2713';color:var(--bg);font-size:12px;font-weight:700}
88
+ .task-desc{flex:1;min-width:0}
89
+ .task-desc.done{text-decoration:line-through;color:var(--textdim)}
90
+ .task-priority{font-size:10px;padding:2px 6px;border-radius:4px;text-transform:uppercase;font-weight:700}
91
+ .task-priority.critical{background:var(--red);color:#fff}
92
+ .task-priority.high{background:var(--amber);color:#000}
93
+ .task-priority.medium{background:var(--bg4);color:var(--text)}
94
+ .task-priority.low{background:var(--bg3);color:var(--textdim)}
95
+ .task-add-bar{display:flex;gap:8px;margin-bottom:16px}
96
+ .task-add-bar input{flex:1}
97
+ .task-add-bar select{background:var(--bg2);color:var(--text);border:1px solid var(--border);padding:8px;border-radius:var(--radius);font-size:12px}
98
+ .task-add-bar button{background:var(--green3);color:var(--bg);padding:8px 16px;border-radius:var(--radius);font-weight:700;font-size:12px}
99
+
100
+ /* Emails */
101
+ .email-item{padding:12px;border-bottom:1px solid var(--border);cursor:default}
102
+ .email-item:hover{background:var(--bg3)}
103
+ .email-from{color:var(--cyan);font-size:12px;font-weight:600}
104
+ .email-subject{color:var(--textbright);margin-top:2px}
105
+ .email-snippet{color:var(--textdim);font-size:11px;margin-top:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
106
+ .email-date{color:var(--textdim);font-size:10px;margin-top:4px}
107
+
108
+ /* Calendar */
109
+ .event-item{display:flex;gap:12px;padding:12px;border-bottom:1px solid var(--border)}
110
+ .event-time{color:var(--amber);font-weight:600;min-width:110px;white-space:nowrap}
111
+ .event-title{color:var(--textbright)}
112
+ .event-location{color:var(--textdim);font-size:11px;margin-top:2px}
113
+ .event-cal{color:var(--textdim);font-size:10px}
114
+
115
+ /* Plan */
116
+ .plan-section{margin-bottom:20px}
117
+ .plan-section h3{font-size:12px;color:var(--amber);text-transform:uppercase;letter-spacing:1px;margin-bottom:8px;padding-bottom:4px;border-bottom:1px solid var(--border)}
118
+ .plan-summary{color:var(--textbright);font-size:14px;line-height:1.7;padding:12px 0}
119
+ .plan-schedule-item{display:flex;gap:12px;padding:8px 0;border-bottom:1px solid var(--border)}
120
+ .plan-schedule-time{color:var(--cyan);min-width:100px;font-weight:600}
121
+ .plan-schedule-type{font-size:10px;padding:2px 6px;border-radius:4px;text-transform:uppercase;font-weight:700;min-width:60px;text-align:center}
122
+ .plan-schedule-type.meeting{background:#003d5c;color:var(--cyan)}
123
+ .plan-schedule-type.focus{background:var(--greendim);color:var(--green)}
124
+ .plan-schedule-type.break{background:var(--bg4);color:var(--textdim)}
125
+ .plan-schedule-type.task{background:#3d2e00;color:var(--amber)}
126
+ .plan-action-item{padding:6px 0;display:flex;gap:8px;align-items:baseline}
127
+ .plan-action-badge{font-size:10px;padding:1px 6px;border-radius:4px;text-transform:uppercase;font-weight:700}
128
+ .plan-action-badge.critical{background:var(--red);color:#fff}
129
+ .plan-action-badge.high{background:var(--amber);color:#000}
130
+ .plan-action-badge.medium,.plan-action-badge.low{background:var(--bg4);color:var(--text)}
131
+ .plan-alert{padding:8px 12px;background:#2a0000;border:1px solid #5a0000;border-radius:var(--radius);margin-bottom:6px;color:var(--red)}
132
+ .plan-insight{color:var(--textdim);padding:4px 0}
133
+ .plan-meta{color:var(--textdim);font-size:11px;margin-top:12px;padding-top:8px;border-top:1px solid var(--border)}
134
+ .refresh-btn{background:var(--bg3);color:var(--cyan);padding:6px 14px;border-radius:var(--radius);font-size:11px;border:1px solid var(--border);transition:all .15s}
135
+ .refresh-btn:hover{background:var(--bg4);border-color:var(--cyan)}
136
+
137
+ /* Agents */
138
+ .agents-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:10px}
139
+ .agent-card{background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius);padding:14px;cursor:pointer;transition:all .15s}
140
+ .agent-card:hover{border-color:var(--green3);background:var(--bg3)}
141
+ .agent-name{font-size:13px;font-weight:700;color:var(--green);text-transform:uppercase;letter-spacing:1px}
142
+ .agent-category{font-size:10px;color:var(--textdim);margin-top:2px}
143
+ .agent-tagline{font-size:11px;color:var(--text);margin-top:6px;line-height:1.4}
144
+
145
+ /* Modal */
146
+ .modal-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.7);display:flex;align-items:center;justify-content:center;z-index:1000;opacity:0;pointer-events:none;transition:opacity .2s}
147
+ .modal-overlay.show{opacity:1;pointer-events:all}
148
+ .modal{background:var(--bg2);border:1px solid var(--borderbright);border-radius:8px;width:90%;max-width:600px;max-height:80vh;display:flex;flex-direction:column;overflow:hidden}
149
+ .modal-header{display:flex;align-items:center;justify-content:space-between;padding:16px;border-bottom:1px solid var(--border)}
150
+ .modal-header h2{font-size:14px;color:var(--green)}
151
+ .modal-close{background:none;color:var(--textdim);font-size:20px;padding:4px 8px;border-radius:4px}
152
+ .modal-close:hover{color:var(--text);background:var(--bg3)}
153
+ .modal-body{padding:16px;overflow-y:auto;flex:1}
154
+ .modal-body textarea{width:100%;min-height:80px;margin-bottom:12px}
155
+ .modal-body .response{background:var(--bg3);border:1px solid var(--border);border-radius:var(--radius);padding:12px;white-space:pre-wrap;word-wrap:break-word;max-height:300px;overflow-y:auto;color:var(--text);line-height:1.6}
156
+ .modal-footer{padding:12px 16px;border-top:1px solid var(--border);display:flex;gap:8px;justify-content:flex-end}
157
+ .modal-footer button{padding:8px 20px;border-radius:var(--radius);font-size:12px;font-weight:600}
158
+ .btn-primary{background:var(--green3);color:var(--bg)}
159
+ .btn-primary:hover{background:var(--green2)}
160
+ .btn-secondary{background:var(--bg3);color:var(--text);border:1px solid var(--border)}
161
+ .btn-secondary:hover{background:var(--bg4)}
162
+
163
+ /* Status indicator */
164
+ .status-dot{width:8px;height:8px;border-radius:50%;display:inline-block;margin-right:6px}
165
+ .status-dot.ok{background:var(--green)}
166
+ .status-dot.warn{background:var(--amber)}
167
+ .status-dot.err{background:var(--red)}
168
+
169
+ /* Loading */
170
+ .loading{text-align:center;padding:40px;color:var(--textdim)}
171
+ .spinner{display:inline-block;width:20px;height:20px;border:2px solid var(--border);border-top-color:var(--green);border-radius:50%;animation:spin .6s linear infinite;margin-bottom:8px}
172
+ @keyframes spin{to{transform:rotate(360deg)}}
173
+
174
+ /* Header */
175
+ .header-title{font-size:14px;color:var(--textbright);font-weight:600}
176
+ .header-time{margin-left:auto;color:var(--textdim);font-size:12px}
177
+ .header-status{font-size:11px}
178
+
179
+ /* Mobile */
180
+ #mobile-toggle{display:none;background:none;color:var(--green);font-size:20px;padding:4px 8px}
181
+ @media(max-width:768px){
182
+ #sidebar{position:fixed;left:-260px;top:0;height:100%;z-index:100;transition:left .25s;width:260px}
183
+ #sidebar.open{left:0}
184
+ #mobile-toggle{display:block}
185
+ .dash-grid{grid-template-columns:1fr}
186
+ .agents-grid{grid-template-columns:repeat(auto-fill,minmax(150px,1fr))}
187
+ .msg-user .msg-bubble,.msg-assistant .msg-bubble{max-width:95%}
188
+ .modal{width:95%;max-width:none}
189
+ }
190
+ </style>
191
+ </head>
192
+ <body>
193
+ <div id="app">
194
+ <nav id="sidebar">
195
+ <div class="sidebar-brand">
196
+ <h1>NHA</h1>
197
+ <p>Local Ops Console</p>
198
+ </div>
199
+ <div class="nav-section">
200
+ <div class="nav-section-label">Operations</div>
201
+ <div class="nav-item active" data-view="dashboard" onclick="switchView('dashboard')">
202
+ <span class="icon">&#9635;</span> Dashboard
203
+ </div>
204
+ <div class="nav-item" data-view="chat" onclick="switchView('chat')">
205
+ <span class="icon">&#9654;</span> Chat
206
+ </div>
207
+ <div class="nav-item" data-view="plan" onclick="switchView('plan')">
208
+ <span class="icon">&#9776;</span> Plan
209
+ </div>
210
+ <div class="nav-item" data-view="tasks" onclick="switchView('tasks')">
211
+ <span class="icon">&#9745;</span> Tasks <span class="badge" id="task-badge" style="display:none">0</span>
212
+ </div>
213
+ </div>
214
+ <div class="nav-section">
215
+ <div class="nav-section-label">Data</div>
216
+ <div class="nav-item" data-view="emails" onclick="switchView('emails')">
217
+ <span class="icon">&#9993;</span> Emails <span class="badge" id="email-badge" style="display:none">0</span>
218
+ </div>
219
+ <div class="nav-item" data-view="calendar" onclick="switchView('calendar')">
220
+ <span class="icon">&#128197;</span> Calendar
221
+ </div>
222
+ </div>
223
+ <div class="nav-section">
224
+ <div class="nav-section-label">Intelligence</div>
225
+ <div class="nav-item" data-view="agents" onclick="switchView('agents')">
226
+ <span class="icon">&#9733;</span> Agents <span class="badge">38</span>
227
+ </div>
228
+ </div>
229
+ </nav>
230
+
231
+ <div id="main">
232
+ <div id="header">
233
+ <button id="mobile-toggle" onclick="toggleSidebar()">&#9776;</button>
234
+ <span class="header-title" id="header-title">Dashboard</span>
235
+ <span class="header-status" id="header-status"></span>
236
+ <span class="header-time" id="header-time"></span>
237
+ </div>
238
+ <div id="content"></div>
239
+ </div>
240
+ </div>
241
+
242
+ <!-- Agent Modal -->
243
+ <div class="modal-overlay" id="agent-modal">
244
+ <div class="modal">
245
+ <div class="modal-header">
246
+ <h2 id="modal-agent-name">AGENT</h2>
247
+ <button class="modal-close" onclick="closeModal()">&times;</button>
248
+ </div>
249
+ <div class="modal-body">
250
+ <textarea id="modal-prompt" placeholder="Ask this agent something..."></textarea>
251
+ <div id="modal-response" class="response" style="display:none"></div>
252
+ </div>
253
+ <div class="modal-footer">
254
+ <button class="btn-secondary" onclick="closeModal()">Close</button>
255
+ <button class="btn-primary" id="modal-send" onclick="askAgent()">Ask</button>
256
+ </div>
257
+ </div>
258
+ </div>
259
+
260
+ <script>
261
+ // ── State ──────────────────────────────────────────────────────────────────
262
+ const API = '';
263
+ let currentView = 'dashboard';
264
+ let chatHistory = [];
265
+ let dashData = { emails: [], events: [], tasks: [], plan: null, status: null };
266
+ let agentsList = [];
267
+ let selectedAgent = null;
268
+
269
+ // ── Navigation ─────────────────────────────────────────────────────────────
270
+ function switchView(view) {
271
+ currentView = view;
272
+ document.querySelectorAll('.nav-item').forEach(el => {
273
+ el.classList.toggle('active', el.dataset.view === view);
274
+ });
275
+ const titles = {dashboard:'Dashboard',chat:'Chat',plan:'Daily Plan',tasks:'Tasks',emails:'Emails',calendar:'Calendar',agents:'Agents'};
276
+ document.getElementById('header-title').textContent = titles[view] || view;
277
+ document.getElementById('sidebar').classList.remove('open');
278
+ renderView();
279
+ }
280
+
281
+ function toggleSidebar() {
282
+ document.getElementById('sidebar').classList.toggle('open');
283
+ }
284
+
285
+ // ── Clock ──────────────────────────────────────────────────────────────────
286
+ function updateClock() {
287
+ const now = new Date();
288
+ document.getElementById('header-time').textContent = now.toLocaleTimeString('en-US',{hour:'2-digit',minute:'2-digit',second:'2-digit',hour12:false}) + ' ' + now.toLocaleDateString('en-US',{weekday:'short',month:'short',day:'numeric'});
289
+ }
290
+ setInterval(updateClock, 1000);
291
+ updateClock();
292
+
293
+ // ── API Helpers ────────────────────────────────────────────────────────────
294
+ async function apiGet(path) {
295
+ try {
296
+ const r = await fetch(API + path);
297
+ if (!r.ok) throw new Error(r.status + ' ' + r.statusText);
298
+ return await r.json();
299
+ } catch (e) {
300
+ console.error('API error:', path, e);
301
+ return null;
302
+ }
303
+ }
304
+
305
+ async function apiPost(path, body) {
306
+ try {
307
+ const r = await fetch(API + path, {
308
+ method: 'POST',
309
+ headers: {'Content-Type': 'application/json'},
310
+ body: JSON.stringify(body),
311
+ });
312
+ if (!r.ok) throw new Error(r.status + ' ' + r.statusText);
313
+ return await r.json();
314
+ } catch (e) {
315
+ console.error('API error:', path, e);
316
+ return null;
317
+ }
318
+ }
319
+
320
+ async function apiPatch(path) {
321
+ try {
322
+ const r = await fetch(API + path, {method: 'PATCH'});
323
+ if (!r.ok) throw new Error(r.status + ' ' + r.statusText);
324
+ return await r.json();
325
+ } catch (e) {
326
+ console.error('API error:', path, e);
327
+ return null;
328
+ }
329
+ }
330
+
331
+ // ── Data Loading ───────────────────────────────────────────────────────────
332
+ async function loadDashboard() {
333
+ const [status, emails, events, tasks] = await Promise.all([
334
+ apiGet('/api/status'),
335
+ apiGet('/api/emails'),
336
+ apiGet('/api/calendar'),
337
+ apiGet('/api/tasks'),
338
+ ]);
339
+ dashData.status = status;
340
+ dashData.emails = emails?.emails || [];
341
+ dashData.events = events?.events || [];
342
+ dashData.tasks = tasks?.tasks || [];
343
+ updateBadges();
344
+ }
345
+
346
+ async function loadPlan() {
347
+ const data = await apiGet('/api/plan');
348
+ dashData.plan = data?.plan || null;
349
+ return dashData.plan;
350
+ }
351
+
352
+ async function loadAgents() {
353
+ const data = await apiGet('/api/agents');
354
+ agentsList = data?.agents || [];
355
+ }
356
+
357
+ function updateBadges() {
358
+ const eb = document.getElementById('email-badge');
359
+ const tb = document.getElementById('task-badge');
360
+ if (dashData.emails.length > 0) {
361
+ eb.textContent = dashData.emails.length;
362
+ eb.style.display = '';
363
+ } else {
364
+ eb.style.display = 'none';
365
+ }
366
+ const pending = dashData.tasks.filter(t => t.status !== 'done').length;
367
+ if (pending > 0) {
368
+ tb.textContent = pending;
369
+ tb.style.display = '';
370
+ } else {
371
+ tb.style.display = 'none';
372
+ }
373
+ }
374
+
375
+ // ── Rendering ──────────────────────────────────────────────────────────────
376
+ function renderView() {
377
+ const el = document.getElementById('content');
378
+ switch (currentView) {
379
+ case 'dashboard': return renderDashboard(el);
380
+ case 'chat': return renderChat(el);
381
+ case 'plan': return renderPlan(el);
382
+ case 'tasks': return renderTasks(el);
383
+ case 'emails': return renderEmails(el);
384
+ case 'calendar': return renderCalendar(el);
385
+ case 'agents': return renderAgents(el);
386
+ }
387
+ }
388
+
389
+ function esc(s) {
390
+ if (!s) return '';
391
+ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
392
+ }
393
+
394
+ function fmtTime(iso) {
395
+ if (!iso) return '';
396
+ try {
397
+ return new Date(iso).toLocaleTimeString('en-US',{hour:'2-digit',minute:'2-digit',hour12:true});
398
+ } catch { return iso; }
399
+ }
400
+
401
+ // ── Dashboard ──────────────────────────────────────────────────────────────
402
+ function renderDashboard(el) {
403
+ const s = dashData.status;
404
+ const tasks = dashData.tasks;
405
+ const emails = dashData.emails;
406
+ const events = dashData.events;
407
+ const done = tasks.filter(t=>t.status==='done').length;
408
+ const pending = tasks.length - done;
409
+ const pct = tasks.length > 0 ? Math.round(done/tasks.length*100) : 0;
410
+
411
+ let statusHtml = '';
412
+ if (s) {
413
+ const dot = s.connected ? 'ok' : 'err';
414
+ statusHtml = '<span class="status-dot '+dot+'"></span>' + (s.connected ? 'Connected' : 'Disconnected') + ' &middot; ' + esc(s.provider || '');
415
+ document.getElementById('header-status').innerHTML = statusHtml;
416
+ }
417
+
418
+ el.innerHTML = '<div class="dash-grid">' +
419
+ '<div class="card"><div class="card-title">Tasks Today</div><div class="card-value">'+pending+'</div><div class="card-sub">'+done+' done / '+tasks.length+' total ('+pct+'%)</div></div>' +
420
+ '<div class="card"><div class="card-title">Unread Emails</div><div class="card-value">'+emails.length+'</div><div class="card-sub">'+(emails.length>0 ? esc(emails[0].from) : 'Inbox zero')+'</div></div>' +
421
+ '<div class="card"><div class="card-title">Events Today</div><div class="card-value">'+events.length+'</div><div class="card-sub">'+(events.length>0 ? esc(events[0].summary) : 'No events')+'</div></div>' +
422
+ '<div class="card"><div class="card-title">Agents</div><div class="card-value">38</div><div class="card-sub">Ready to assist</div></div>' +
423
+ '</div>' +
424
+ // Upcoming events
425
+ '<div class="dash-section"><h2>Upcoming Events</h2>' +
426
+ (events.length === 0 ? '<div class="card" style="color:var(--textdim)">No events scheduled for today.</div>' :
427
+ events.slice(0, 5).map(e => '<div class="card" style="padding:10px 14px;display:flex;gap:12px;align-items:center">' +
428
+ '<span style="color:var(--amber);min-width:100px">'+(e.isAllDay ? 'All day' : fmtTime(e.start)+' - '+fmtTime(e.end))+'</span>' +
429
+ '<span style="color:var(--textbright)">'+esc(e.summary)+'</span>' +
430
+ (e.location ? '<span style="color:var(--textdim);font-size:11px"> &middot; '+esc(e.location)+'</span>' : '') +
431
+ '</div>').join('')) +
432
+ '</div>' +
433
+ // Recent emails
434
+ '<div class="dash-section"><h2>Recent Emails</h2>' +
435
+ (emails.length === 0 ? '<div class="card" style="color:var(--textdim)">No unread emails.</div>' :
436
+ emails.slice(0, 5).map(e => '<div class="card" style="padding:10px 14px">' +
437
+ '<div style="display:flex;justify-content:space-between"><span class="email-from">'+esc(e.from)+'</span><span style="color:var(--textdim);font-size:10px">'+esc(e.date)+'</span></div>' +
438
+ '<div style="color:var(--textbright);margin-top:2px">'+esc(e.subject)+'</div>' +
439
+ '<div style="color:var(--textdim);font-size:11px;margin-top:2px">'+esc((e.snippet||'').slice(0,120))+'</div>' +
440
+ '</div>').join('')) +
441
+ '</div>' +
442
+ // Pending tasks
443
+ '<div class="dash-section"><h2>Pending Tasks</h2>' +
444
+ (pending === 0 ? '<div class="card" style="color:var(--textdim)">All tasks complete.</div>' :
445
+ tasks.filter(t=>t.status!=='done').slice(0,5).map(t => '<div class="card" style="padding:10px 14px;display:flex;gap:10px;align-items:center">' +
446
+ '<span class="task-priority '+esc(t.priority)+'">'+esc(t.priority)+'</span>' +
447
+ '<span>'+esc(t.description)+'</span>' +
448
+ '</div>').join('')) +
449
+ '</div>';
450
+ }
451
+
452
+ // ── Chat ───────────────────────────────────────────────────────────────────
453
+ let chatRendered = false;
454
+ function renderChat(el) {
455
+ if (!chatRendered || !document.getElementById('chat-view')) {
456
+ el.innerHTML = '<div id="chat-view">' +
457
+ '<div id="chat-messages"></div>' +
458
+ '<div id="chat-input-bar">' +
459
+ '<textarea id="chat-input" placeholder="Ask anything... manage emails, calendar, tasks" rows="1" onkeydown="chatKeydown(event)"></textarea>' +
460
+ '<button id="chat-send" onclick="sendChat()">Send</button>' +
461
+ '</div>' +
462
+ '</div>';
463
+ chatRendered = true;
464
+ renderChatMessages();
465
+ setTimeout(() => document.getElementById('chat-input')?.focus(), 100);
466
+ }
467
+ }
468
+
469
+ function renderChatMessages() {
470
+ const el = document.getElementById('chat-messages');
471
+ if (!el) return;
472
+ if (chatHistory.length === 0) {
473
+ el.innerHTML = '<div style="text-align:center;padding:60px 20px;color:var(--textdim)">' +
474
+ '<div style="font-size:32px;margin-bottom:16px;color:var(--green)">NHA Chat</div>' +
475
+ '<div>Personal Operations Assistant</div>' +
476
+ '<div style="margin-top:12px;font-size:11px">Try: "Show my unread emails" / "What\'s on my calendar?" / "Add a task to review the PR"</div>' +
477
+ '</div>';
478
+ return;
479
+ }
480
+ el.innerHTML = chatHistory.map(m => {
481
+ const cls = m.role === 'user' ? 'msg-user' : 'msg-assistant';
482
+ const label = m.role === 'user' ? 'You' : 'NHA';
483
+ return '<div class="msg '+cls+'"><div><div class="msg-label">'+label+'</div><div class="msg-bubble">'+esc(m.content)+'</div></div></div>';
484
+ }).join('');
485
+ el.scrollTop = el.scrollHeight;
486
+ }
487
+
488
+ function chatKeydown(e) {
489
+ if (e.key === 'Enter' && !e.shiftKey) {
490
+ e.preventDefault();
491
+ sendChat();
492
+ }
493
+ }
494
+
495
+ let chatBusy = false;
496
+ async function sendChat() {
497
+ if (chatBusy) return;
498
+ const input = document.getElementById('chat-input');
499
+ const text = input.value.trim();
500
+ if (!text) return;
501
+ input.value = '';
502
+
503
+ chatHistory.push({role:'user', content:text});
504
+ chatHistory.push({role:'assistant', content:'Thinking...'});
505
+ renderChatMessages();
506
+ chatBusy = true;
507
+ document.getElementById('chat-send').disabled = true;
508
+
509
+ const data = await apiPost('/api/chat', {message: text, history: chatHistory.slice(0,-2)});
510
+ chatHistory.pop(); // remove thinking
511
+ if (data?.response) {
512
+ chatHistory.push({role:'assistant', content: data.response});
513
+ if (data.toolResult) {
514
+ chatHistory.push({role:'assistant', content: '[Tool Result]\\n' + data.toolResult});
515
+ }
516
+ } else {
517
+ chatHistory.push({role:'assistant', content: data?.error || 'Error: could not get response.'});
518
+ }
519
+ renderChatMessages();
520
+ chatBusy = false;
521
+ document.getElementById('chat-send').disabled = false;
522
+ input.focus();
523
+ // Refresh dashboard data after chat (chat may have changed tasks/emails)
524
+ loadDashboard();
525
+ }
526
+
527
+ // ── Plan ───────────────────────────────────────────────────────────────────
528
+ async function renderPlan(el) {
529
+ el.innerHTML = '<div class="loading"><div class="spinner"></div><div>Loading plan...</div></div>';
530
+ const plan = dashData.plan || await loadPlan();
531
+ if (!plan) {
532
+ el.innerHTML = '<div class="card" style="text-align:center;padding:40px">' +
533
+ '<div style="color:var(--textdim);margin-bottom:16px">No plan generated yet.</div>' +
534
+ '<button class="btn-primary" onclick="refreshPlan()" id="gen-plan-btn">Generate Daily Plan</button>' +
535
+ '</div>';
536
+ return;
537
+ }
538
+ renderPlanContent(el, plan);
539
+ }
540
+
541
+ function renderPlanContent(el, plan) {
542
+ let html = '<div style="display:flex;align-items:center;gap:12px;margin-bottom:16px">' +
543
+ '<button class="refresh-btn" onclick="refreshPlan()">Refresh Plan</button>' +
544
+ (plan.metadata ? '<span style="color:var(--textdim);font-size:11px">Generated '+new Date(plan.metadata.generatedAt).toLocaleString()+'</span>' : '') +
545
+ '</div>';
546
+
547
+ // Executive summary
548
+ html += '<div class="plan-section"><h3>Executive Summary</h3><div class="plan-summary">'+esc(plan.executive_summary)+'</div></div>';
549
+
550
+ // Priority actions
551
+ if (plan.priority_actions?.length) {
552
+ html += '<div class="plan-section"><h3>Priority Actions</h3>';
553
+ for (const a of plan.priority_actions) {
554
+ html += '<div class="plan-action-item"><span class="plan-action-badge '+(a.priority||'medium')+'">'+esc(a.priority||'medium')+'</span>';
555
+ if (a.time) html += '<span style="color:var(--cyan)">'+esc(a.time)+'</span>';
556
+ html += '<span>'+esc(a.action)+'</span>';
557
+ if (a.source) html += '<span style="color:var(--textdim);font-size:10px">('+esc(a.source)+')</span>';
558
+ html += '</div>';
559
+ }
560
+ html += '</div>';
561
+ }
562
+
563
+ // Schedule
564
+ if (plan.schedule?.length) {
565
+ html += '<div class="plan-section"><h3>Schedule</h3>';
566
+ for (const s of plan.schedule) {
567
+ html += '<div class="plan-schedule-item">' +
568
+ '<span class="plan-schedule-time">'+esc(s.time_start)+' - '+esc(s.time_end)+'</span>' +
569
+ '<span class="plan-schedule-type '+(s.type||'task')+'">'+esc(s.type||'task')+'</span>' +
570
+ '<span style="flex:1"><span style="color:var(--textbright)">'+esc(s.title)+'</span>' +
571
+ (s.notes ? '<div style="color:var(--textdim);font-size:11px;margin-top:2px">'+esc(s.notes)+'</div>' : '') +
572
+ (s.preparation ? '<div style="color:var(--amber);font-size:11px;margin-top:2px">Prep: '+esc(s.preparation)+'</div>' : '') +
573
+ '</span></div>';
574
+ }
575
+ html += '</div>';
576
+ }
577
+
578
+ // Email actions
579
+ if (plan.email_actions?.length) {
580
+ html += '<div class="plan-section"><h3>Email Actions</h3>';
581
+ for (const e of plan.email_actions) {
582
+ const ac = e.action === 'reply' ? 'color:var(--green)' : e.action === 'flag' ? 'color:var(--amber)' : 'color:var(--textdim)';
583
+ html += '<div style="padding:6px 0"><span style="'+ac+';font-weight:600;text-transform:uppercase;font-size:11px">['+esc(e.action)+']</span> '+esc(e.subject)+' <span style="color:var(--textdim)">from '+esc(e.from)+'</span></div>';
584
+ }
585
+ html += '</div>';
586
+ }
587
+
588
+ // Security alerts
589
+ if (plan.security_alerts?.length) {
590
+ html += '<div class="plan-section"><h3>Security Alerts</h3>';
591
+ for (const a of plan.security_alerts) {
592
+ html += '<div class="plan-alert">'+esc(typeof a === 'string' ? a : a.message || JSON.stringify(a))+'</div>';
593
+ }
594
+ html += '</div>';
595
+ }
596
+
597
+ // Insights
598
+ if (plan.insights?.length) {
599
+ html += '<div class="plan-section"><h3>Insights</h3>';
600
+ for (const i of plan.insights) {
601
+ html += '<div class="plan-insight">&rarr; '+esc(typeof i === 'string' ? i : i.message || JSON.stringify(i))+'</div>';
602
+ }
603
+ html += '</div>';
604
+ }
605
+
606
+ // Metadata
607
+ if (plan.metadata) {
608
+ html += '<div class="plan-meta">Generated in '+(plan.metadata.durationMs/1000).toFixed(1)+'s by '+(plan.metadata.agentsUsed?.length||0)+' agents ('+esc(plan.metadata.provider)+')</div>';
609
+ }
610
+
611
+ el.innerHTML = html;
612
+ }
613
+
614
+ async function refreshPlan() {
615
+ const el = document.getElementById('content');
616
+ el.innerHTML = '<div class="loading"><div class="spinner"></div><div>Generating plan... This takes 30-90 seconds.</div></div>';
617
+ const data = await apiPost('/api/plan/refresh', {});
618
+ dashData.plan = data?.plan || null;
619
+ if (currentView === 'plan') renderPlan(el);
620
+ }
621
+
622
+ // ── Tasks ──────────────────────────────────────────────────────────────────
623
+ function renderTasks(el) {
624
+ const tasks = dashData.tasks;
625
+ let html = '<div class="task-add-bar">' +
626
+ '<input id="task-desc-input" placeholder="Add a new task..." onkeydown="if(event.key===\'Enter\')addTaskUI()">' +
627
+ '<select id="task-priority-select"><option value="medium">Medium</option><option value="high">High</option><option value="critical">Critical</option><option value="low">Low</option></select>' +
628
+ '<button onclick="addTaskUI()">Add</button>' +
629
+ '</div>';
630
+
631
+ if (tasks.length === 0) {
632
+ html += '<div class="card" style="text-align:center;padding:30px;color:var(--textdim)">No tasks for today. Add one above.</div>';
633
+ } else {
634
+ // Pending first, then done
635
+ const sorted = [...tasks].sort((a,b) => {
636
+ if (a.status==='done' && b.status!=='done') return 1;
637
+ if (a.status!=='done' && b.status==='done') return -1;
638
+ const pr = {critical:0,high:1,medium:2,low:3};
639
+ return (pr[a.priority]||2) - (pr[b.priority]||2);
640
+ });
641
+ for (const t of sorted) {
642
+ const isDone = t.status === 'done';
643
+ html += '<div class="task-item">' +
644
+ '<div class="task-check'+(isDone?' done':'')+'" onclick="toggleTask('+t.id+','+isDone+')"></div>' +
645
+ '<span class="task-desc'+(isDone?' done':'')+'">'+esc(t.description)+'</span>' +
646
+ '<span class="task-priority '+esc(t.priority)+'">'+esc(t.priority)+'</span>' +
647
+ '<span style="color:var(--textdim);font-size:10px">#'+t.id+'</span>' +
648
+ '</div>';
649
+ }
650
+ }
651
+ el.innerHTML = html;
652
+ }
653
+
654
+ async function addTaskUI() {
655
+ const descEl = document.getElementById('task-desc-input');
656
+ const prioEl = document.getElementById('task-priority-select');
657
+ const desc = descEl.value.trim();
658
+ if (!desc) return;
659
+ const data = await apiPost('/api/tasks', {description: desc, priority: prioEl.value});
660
+ if (data?.task) {
661
+ dashData.tasks.push(data.task);
662
+ descEl.value = '';
663
+ renderTasks(document.getElementById('content'));
664
+ updateBadges();
665
+ }
666
+ }
667
+
668
+ async function toggleTask(id, isDone) {
669
+ if (isDone) return; // no un-complete
670
+ const data = await apiPatch('/api/tasks/'+id+'/done');
671
+ if (data?.ok) {
672
+ const t = dashData.tasks.find(t=>t.id===id);
673
+ if (t) { t.status = 'done'; t.completedAt = new Date().toISOString(); }
674
+ renderTasks(document.getElementById('content'));
675
+ updateBadges();
676
+ }
677
+ }
678
+
679
+ // ── Emails ─────────────────────────────────────────────────────────────────
680
+ function renderEmails(el) {
681
+ const emails = dashData.emails;
682
+ if (emails.length === 0) {
683
+ el.innerHTML = '<div class="card" style="text-align:center;padding:40px;color:var(--textdim)">No unread emails. Inbox zero!</div>';
684
+ return;
685
+ }
686
+ el.innerHTML = emails.map(e => '<div class="email-item">' +
687
+ '<div style="display:flex;justify-content:space-between"><span class="email-from">'+esc(e.from)+'</span><span class="email-date">'+esc(e.date)+'</span></div>' +
688
+ '<div class="email-subject">'+esc(e.subject)+'</div>' +
689
+ '<div class="email-snippet">'+esc((e.snippet||'').slice(0,200))+'</div>' +
690
+ '</div>').join('');
691
+ }
692
+
693
+ // ── Calendar ───────────────────────────────────────────────────────────────
694
+ function renderCalendar(el) {
695
+ const events = dashData.events;
696
+ if (events.length === 0) {
697
+ el.innerHTML = '<div class="card" style="text-align:center;padding:40px;color:var(--textdim)">No events scheduled for today.</div>';
698
+ return;
699
+ }
700
+ el.innerHTML = events.map(e => {
701
+ const time = e.isAllDay ? 'All day' : fmtTime(e.start) + ' - ' + fmtTime(e.end);
702
+ return '<div class="event-item">' +
703
+ '<span class="event-time">'+esc(time)+'</span>' +
704
+ '<div style="flex:1">' +
705
+ '<div class="event-title">'+esc(e.summary)+'</div>' +
706
+ (e.location ? '<div class="event-location">'+esc(e.location)+'</div>' : '') +
707
+ (e.calendarName ? '<div class="event-cal">'+esc(e.calendarName)+'</div>' : '') +
708
+ (e.hangoutLink ? '<div><a href="'+esc(e.hangoutLink)+'" target="_blank" style="font-size:11px">Join call</a></div>' : '') +
709
+ '</div>' +
710
+ '</div>';
711
+ }).join('');
712
+ }
713
+
714
+ // ── Agents ─────────────────────────────────────────────────────────────────
715
+ function renderAgents(el) {
716
+ if (agentsList.length === 0) {
717
+ el.innerHTML = '<div class="loading"><div class="spinner"></div><div>Loading agents...</div></div>';
718
+ loadAgents().then(() => { if (currentView === 'agents') renderAgents(el); });
719
+ return;
720
+ }
721
+ el.innerHTML = '<div class="agents-grid">' + agentsList.map(a =>
722
+ '<div class="agent-card" data-agent="'+esc(a.name)+'" onclick="openAgent(this.dataset.agent)">' +
723
+ '<div class="agent-name">'+esc(a.name)+'</div>' +
724
+ '<div class="agent-category">'+esc(a.category || 'agent')+'</div>' +
725
+ '<div class="agent-tagline">'+esc(a.tagline || '')+'</div>' +
726
+ '</div>'
727
+ ).join('') + '</div>';
728
+ }
729
+
730
+ function openAgent(name) {
731
+ selectedAgent = name;
732
+ document.getElementById('modal-agent-name').textContent = name.toUpperCase();
733
+ document.getElementById('modal-prompt').value = '';
734
+ document.getElementById('modal-response').style.display = 'none';
735
+ document.getElementById('modal-response').textContent = '';
736
+ document.getElementById('agent-modal').classList.add('show');
737
+ document.getElementById('modal-prompt').focus();
738
+ }
739
+
740
+ function closeModal() {
741
+ document.getElementById('agent-modal').classList.remove('show');
742
+ selectedAgent = null;
743
+ }
744
+
745
+ async function askAgent() {
746
+ if (!selectedAgent) return;
747
+ const prompt = document.getElementById('modal-prompt').value.trim();
748
+ if (!prompt) return;
749
+ const respEl = document.getElementById('modal-response');
750
+ const sendBtn = document.getElementById('modal-send');
751
+ respEl.style.display = 'block';
752
+ respEl.textContent = 'Thinking...';
753
+ sendBtn.disabled = true;
754
+
755
+ const data = await apiPost('/api/ask', {agent: selectedAgent, prompt});
756
+ if (data?.response) {
757
+ respEl.textContent = data.response;
758
+ } else {
759
+ respEl.textContent = data?.error || 'Error getting response.';
760
+ }
761
+ sendBtn.disabled = false;
762
+ }
763
+
764
+ // Close modal on Escape
765
+ document.addEventListener('keydown', e => {
766
+ if (e.key === 'Escape') closeModal();
767
+ });
768
+
769
+ // Close modal on overlay click
770
+ document.getElementById('agent-modal').addEventListener('click', e => {
771
+ if (e.target.id === 'agent-modal') closeModal();
772
+ });
773
+
774
+ // ── Init ───────────────────────────────────────────────────────────────────
775
+ async function init() {
776
+ await loadDashboard();
777
+ loadPlan();
778
+ loadAgents();
779
+ renderView();
780
+ // Auto-refresh every 2 minutes
781
+ setInterval(() => loadDashboard().then(() => { if (currentView === 'dashboard') renderView(); }), 120000);
782
+ }
783
+
784
+ init();
785
+ </script>
786
+ </body>
787
+ </html>`;
788
+ }