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