youclaw 4.6.0__py3-none-any.whl

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.
youclaw/dashboard.py ADDED
@@ -0,0 +1,1347 @@
1
+ """
2
+ YouClaw Web Dashboard
3
+ Web interface for monitoring and managing YouClaw.
4
+ """
5
+
6
+ from aiohttp import web
7
+ import aiohttp_jinja2
8
+ import jinja2
9
+ import json
10
+ import asyncio
11
+ import os
12
+ from pathlib import Path
13
+ from datetime import datetime
14
+ from .ollama_client import ollama_client
15
+ from .memory_manager import memory_manager
16
+ from .skills_manager import skill_manager
17
+ from .scheduler_manager import scheduler_manager
18
+ from .env_manager import env_manager
19
+
20
+ from .config import config
21
+ import logging
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # Dashboard routes
26
+ routes = web.RouteTableDef()
27
+
28
+ async def verify_session(request):
29
+ """Verify X-Session-Token against stored credentials"""
30
+ username = request.headers.get('X-Session-User')
31
+ token = request.headers.get('X-Session-Token')
32
+ if not username or not token: return None
33
+
34
+ async with memory_manager.db.execute("SELECT password_hash, role FROM users WHERE username = ?", (username,)) as cursor:
35
+ row = await cursor.fetchone()
36
+ if row:
37
+ pw_hash, role = row
38
+ import hashlib
39
+ from .config import config
40
+ expected_token = hashlib.sha256(f"{username}{pw_hash}{config.bot.prefix}".encode()).hexdigest()
41
+ if token == expected_token:
42
+ return {"username": username, "role": role}
43
+ return None
44
+
45
+ async def get_user_identity(request):
46
+ """Get the linked platform:id for the current session user + token verification"""
47
+ auth = await verify_session(request)
48
+ if not auth: return None, None
49
+
50
+ platform, user_id = await memory_manager.get_linked_identity(auth['username'])
51
+ return platform, user_id
52
+
53
+ async def check_admin(request):
54
+ """Check if the session user is an admin + token verification"""
55
+ auth = await verify_session(request)
56
+ return auth and auth['role'] == 'admin'
57
+
58
+
59
+ @routes.get('/')
60
+ async def serve_html(request):
61
+ return web.Response(
62
+ text=DASHBOARD_HTML,
63
+ content_type='text/html',
64
+ headers={'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0'}
65
+ )
66
+
67
+
68
+ @routes.post('/api/auth/register')
69
+ async def api_register(request):
70
+ """Register a new dashboard account"""
71
+ try:
72
+ data = await request.json()
73
+ username = data.get('username')
74
+ password = data.get('password')
75
+ if not username or not password:
76
+ return web.json_response({"error": "Username and password required"}, status=400)
77
+
78
+ success = await memory_manager.create_user(username, password)
79
+ if success:
80
+ return web.json_response({"success": True})
81
+ return web.json_response({"error": "Username already exists"}, status=400)
82
+ except Exception as e:
83
+ return web.json_response({"error": str(e)}, status=500)
84
+
85
+
86
+ @routes.post('/api/auth/login')
87
+ async def api_login(request):
88
+ """Login and return user info"""
89
+ try:
90
+ data = await request.json()
91
+ user = await memory_manager.verify_user(data.get('username'), data.get('password'))
92
+ if user:
93
+ return web.json_response({"success": True, "user": user})
94
+ return web.json_response({"error": "Invalid credentials"}, status=401)
95
+ except Exception as e:
96
+ return web.json_response({"error": str(e)}, status=500)
97
+
98
+
99
+ @routes.post('/api/auth/link')
100
+ async def api_link_account(request):
101
+ """Link dashboard account to bot platform identity"""
102
+ try:
103
+ username = request.headers.get('X-Session-User')
104
+ if not username: return web.json_response({"error": "Auth Required"}, status=401)
105
+
106
+ data = await request.json()
107
+ platform = data.get('platform')
108
+ user_id = data.get('user_id')
109
+
110
+ if not platform or not user_id:
111
+ return web.json_response({"error": "Platform and ID required"}, status=400)
112
+
113
+ await memory_manager.link_account(username, platform, user_id)
114
+ return web.json_response({"success": True})
115
+ except Exception as e:
116
+ return web.json_response({"error": str(e)}, status=500)
117
+
118
+
119
+ @routes.get('/api/stats')
120
+ async def api_stats(request):
121
+ """Get bot statistics for current user"""
122
+ try:
123
+ platform, user_id = await get_user_identity(request)
124
+ is_admin = await check_admin(request)
125
+
126
+ if not user_id and not is_admin:
127
+ return web.json_response({"error": "Account not linked to a bot platform"}, status=403)
128
+
129
+ # Get memory stats for this user if linked
130
+ count = 0
131
+ if user_id:
132
+ async with memory_manager.db.execute(
133
+ "SELECT COUNT(*) FROM conversations WHERE platform=? AND user_id=?",
134
+ (platform, user_id)
135
+ ) as cursor:
136
+ count = (await cursor.fetchone())[0]
137
+
138
+ # Get global stats
139
+ async with memory_manager.db.execute("SELECT COUNT(*) FROM conversations") as cursor:
140
+ total_messages = (await cursor.fetchone())[0]
141
+ async with memory_manager.db.execute("SELECT COUNT(DISTINCT user_id) FROM conversations") as cursor:
142
+ unique_users = (await cursor.fetchone())[0]
143
+
144
+ from personality_manager import PERSONALITIES, DEFAULT_PERSONALITY
145
+ active_p = await memory_manager.get_global_setting("active_personality", DEFAULT_PERSONALITY)
146
+ personality_name = PERSONALITIES.get(active_p, PERSONALITIES[DEFAULT_PERSONALITY])['name']
147
+
148
+ ollama_health = await ollama_client.check_health()
149
+
150
+ stats = {
151
+ "status": "online",
152
+ "user_messages": count,
153
+ "is_admin": is_admin,
154
+ "is_linked": bool(user_id),
155
+ "ollama_status": "online" if ollama_health else "offline",
156
+ "model": ollama_client.model,
157
+ "telegram_enabled": config.telegram.enabled,
158
+ "discord_enabled": config.discord.enabled,
159
+ "timestamp": datetime.now().isoformat(),
160
+ "total_messages": total_messages,
161
+ "unique_users": unique_users,
162
+ "active_personality": personality_name,
163
+ "user_identity": f"{platform}:{user_id}" if user_id else "Guest"
164
+ }
165
+
166
+ if is_admin:
167
+ stats["available_models"] = await ollama_client.get_available_models()
168
+
169
+ return web.json_response(stats)
170
+ except Exception as e:
171
+ return web.json_response({"error": str(e)}, status=500)
172
+
173
+
174
+ @routes.get('/api/conversations')
175
+ async def api_conversations(request):
176
+ """Get recent conversations for current user"""
177
+ try:
178
+ platform, user_id = await get_user_identity(request)
179
+ if not user_id: return web.json_response({"conversations": []})
180
+
181
+ async with memory_manager.db.execute("""
182
+ SELECT platform, user_id, channel_id,
183
+ MAX(timestamp) as last_message,
184
+ COUNT(*) as message_count
185
+ FROM conversations
186
+ WHERE platform=? AND user_id=?
187
+ GROUP BY channel_id
188
+ ORDER BY last_message DESC
189
+ LIMIT 50
190
+ """, (platform, user_id)) as cursor:
191
+ rows = await cursor.fetchall()
192
+ conversations = [{"platform": r[0], "user_id": r[1], "channel_id": r[2], "last_message": r[3], "message_count": r[4]} for r in rows]
193
+ return web.json_response({"conversations": conversations})
194
+ except Exception as e:
195
+ return web.json_response({"error": str(e)}, status=500)
196
+
197
+
198
+ @routes.get('/api/skills')
199
+ async def api_skills(request):
200
+ """Get available skills"""
201
+ skills = skill_manager.get_all_skills()
202
+ return web.json_response({"skills": skills})
203
+
204
+
205
+ @routes.get('/api/jobs')
206
+ async def api_get_jobs(request):
207
+ """Get jobs belonging to the current user"""
208
+ try:
209
+ platform, user_id = await get_user_identity(request)
210
+ if not user_id: return web.json_response({"jobs": []})
211
+
212
+ jobs = []
213
+ for job in scheduler_manager.scheduler.get_jobs():
214
+ if len(job.args) >= 2 and job.args[0] == platform and job.args[1] == user_id:
215
+ jobs.append({
216
+ "id": job.id,
217
+ "next_run": job.next_run_time.isoformat() if job.next_run_time else None,
218
+ "prompt": job.args[2] if len(job.args) > 2 else "Mission"
219
+ })
220
+ return web.json_response({"jobs": jobs})
221
+ except Exception as e:
222
+ return web.json_response({"error": str(e)}, status=500)
223
+
224
+
225
+ @routes.post('/api/jobs/schedule')
226
+ async def api_schedule_job(request):
227
+ """Schedule a new AI Cron Job for the current user"""
228
+ try:
229
+ req_platform, req_user_id = await get_user_identity(request)
230
+ if not req_user_id: return web.json_response({"error": "Account linking required"}, status=403)
231
+
232
+ data = await request.json()
233
+ prompt = data.get('prompt')
234
+ frequency = data.get('frequency', '60')
235
+ channel = data.get('channel', req_platform) # Default to user's platform
236
+
237
+ if not prompt:
238
+ return web.json_response({"error": "Prompt required"}, status=400)
239
+
240
+ import hashlib
241
+ job_id = f"cron_{hashlib.md5(f'{req_user_id}{prompt}{channel}'.encode()).hexdigest()[:8]}"
242
+ cron_expr = f"*/{frequency} * * * *" if int(frequency) < 60 else f"0 */{int(int(frequency)/60)} * * *"
243
+
244
+ await scheduler_manager.add_ai_cron_job(channel, req_user_id, prompt, cron_expr, job_id)
245
+ return web.json_response({"success": True, "job_id": job_id})
246
+ except Exception as e:
247
+ return web.json_response({"error": str(e)}, status=500)
248
+
249
+
250
+ @routes.post('/api/jobs/delete')
251
+ async def api_delete_job(request):
252
+ """Cancel a user's job"""
253
+ try:
254
+ platform, user_id = await get_user_identity(request)
255
+ data = await request.json()
256
+ job_id = data.get('job_id')
257
+
258
+ job = scheduler_manager.scheduler.get_job(job_id)
259
+ if job and len(job.args) >= 2 and job.args[0] == platform and job.args[1] == user_id:
260
+ scheduler_manager.scheduler.remove_job(job_id)
261
+ return web.json_response({"success": True})
262
+ return web.json_response({"error": "Unauthorized or not found"}, status=403)
263
+ except Exception as e:
264
+ return web.json_response({"error": str(e)}, status=500)
265
+
266
+
267
+ @routes.get('/api/system/personality')
268
+ async def api_get_personality(request):
269
+ """Get list of personalities and current active one"""
270
+ from personality_manager import PERSONALITIES, DEFAULT_PERSONALITY
271
+ active = await memory_manager.get_global_setting("active_personality", DEFAULT_PERSONALITY)
272
+ return web.json_response({
273
+ "personalities": PERSONALITIES,
274
+ "active": active
275
+ })
276
+
277
+
278
+ @routes.post('/api/system/personality')
279
+ async def api_set_personality(request):
280
+ """Admin Only: Switch personality"""
281
+ if not await check_admin(request): return web.json_response({"error": "Unauthorized"}, status=403)
282
+ try:
283
+ data = await request.json()
284
+ key = data.get('personality')
285
+ from personality_manager import PERSONALITIES
286
+ if key in PERSONALITIES:
287
+ await memory_manager.set_global_setting("active_personality", key)
288
+ return web.json_response({"success": True})
289
+ return web.json_response({"error": "Invalid personality"}, status=400)
290
+ except Exception as e:
291
+ return web.json_response({"error": str(e)}, status=500)
292
+
293
+
294
+ @routes.get('/api/system/env')
295
+ async def api_get_env(request):
296
+ """Admin Only: Get current tokens"""
297
+ if not await check_admin(request): return web.json_response({"error": "Unauthorized"}, status=403)
298
+ try:
299
+ env_vars = env_manager.get_all()
300
+ return web.json_response({
301
+ "telegram_token": env_vars.get("TELEGRAM_BOT_TOKEN", ""),
302
+ "discord_token": env_vars.get("DISCORD_BOT_TOKEN", ""),
303
+ "search_url": config.search_url,
304
+ "email": {
305
+ "imap_host": config.email.imap_host,
306
+ "imap_port": config.email.imap_port,
307
+ "smtp_host": config.email.smtp_host,
308
+ "smtp_port": config.email.smtp_port,
309
+ "user": config.email.user,
310
+ "enabled": config.email.enabled
311
+ }
312
+ })
313
+ except Exception as e:
314
+ return web.json_response({"error": str(e)}, status=500)
315
+
316
+
317
+ @routes.post('/api/system/secrets')
318
+ async def api_save_system_secrets(request):
319
+ """Admin Only: Update tokens"""
320
+ if not await check_admin(request): return web.json_response({"error": "Unauthorized"}, status=403)
321
+ try:
322
+ data = await request.json()
323
+ if 'telegram_token' in data:
324
+ val = data['telegram_token']
325
+ env_manager.set_key("TELEGRAM_BOT_TOKEN", val)
326
+ await memory_manager.set_global_setting("telegram_token", val)
327
+ if 'discord_token' in data:
328
+ val = data['discord_token']
329
+ env_manager.set_key("DISCORD_BOT_TOKEN", val)
330
+ await memory_manager.set_global_setting("discord_token", val)
331
+ if 'search_url' in data:
332
+ val = data['search_url']
333
+ env_manager.set_key("SEARCH_ENGINE_URL", val)
334
+ await memory_manager.set_global_setting("search_url", val)
335
+
336
+ # Email Secrets
337
+ if 'email' in data:
338
+ e = data['email']
339
+ if 'imap_host' in e: await memory_manager.set_global_setting("email_imap_host", e['imap_host'])
340
+ if 'imap_port' in e: await memory_manager.set_global_setting("email_imap_port", str(e['imap_port']))
341
+ if 'smtp_host' in e: await memory_manager.set_global_setting("email_smtp_host", e['smtp_host'])
342
+ if 'smtp_port' in e: await memory_manager.set_global_setting("email_smtp_port", str(e['smtp_port']))
343
+ if 'user' in e: await memory_manager.set_global_setting("email_user", e['user'])
344
+ if 'password' in e: await memory_manager.set_global_setting("email_password", e['password'])
345
+
346
+ await config.refresh_from_db()
347
+
348
+ bot_instance = request.app.get('bot')
349
+ if bot_instance: asyncio.create_task(bot_instance.restart_handlers())
350
+ return web.json_response({"success": True})
351
+ except Exception as e:
352
+ return web.json_response({"error": str(e)}, status=500)
353
+
354
+
355
+ @routes.post('/api/system/toggle_channel')
356
+ async def api_toggle_channel(request):
357
+ """Admin Only: Toggle Telegram/Discord connectivity"""
358
+ if not await check_admin(request): return web.json_response({"error": "Unauthorized"}, status=403)
359
+ try:
360
+ data = await request.json()
361
+ channel = data.get('channel')
362
+ enabled = data.get('enabled')
363
+
364
+ if channel not in ['telegram', 'discord', 'email']:
365
+ return web.json_response({"error": "Invalid channel"}, status=400)
366
+
367
+ await memory_manager.set_global_setting(f"{channel}_enabled", "true" if enabled else "false")
368
+ await config.refresh_from_db()
369
+
370
+ bot_instance = request.app.get('bot')
371
+ if bot_instance: asyncio.create_task(bot_instance.restart_handlers())
372
+
373
+ return web.json_response({"success": True})
374
+ except Exception as e:
375
+ return web.json_response({"error": str(e)}, status=500)
376
+
377
+
378
+ @routes.post('/api/memory/clear')
379
+ async def api_clear_memory(request):
380
+ """User: Clear own memory"""
381
+ try:
382
+ platform, user_id = await get_user_identity(request)
383
+ if not user_id: return web.json_response({"error": "Linking Required"}, status=403)
384
+
385
+ await memory_manager.db.execute("DELETE FROM conversations WHERE platform=? AND user_id=?", (platform, user_id))
386
+ await memory_manager.db.commit()
387
+ return web.json_response({"success": True, "message": "Personal memory cleared!"})
388
+ except Exception as e:
389
+ return web.json_response({"error": str(e)}, status=500)
390
+
391
+
392
+ @routes.get('/api/chat/stream')
393
+ async def api_chat_stream(request):
394
+ """Streaming dashboard-to-model chat"""
395
+ try:
396
+ username = request.query.get('user')
397
+ message = request.query.get('message')
398
+ force_search = request.query.get('search') == 'true'
399
+
400
+ if not username or not message: return web.Response(text="Error: Missing params", status=400)
401
+
402
+ platform, user_id = await memory_manager.get_linked_identity(username)
403
+ ctx = {"platform": platform or "dashboard", "user_id": user_id or f"dash_{username}"}
404
+ history = await memory_manager.get_conversation_history(ctx["platform"], ctx["user_id"])
405
+
406
+ await memory_manager.add_message(ctx["platform"], ctx["user_id"], "user", message)
407
+ profile = await memory_manager.get_user_profile(ctx["platform"], ctx["user_id"])
408
+ if not profile.get("name"): profile["name"] = username
409
+
410
+ response = web.StreamResponse(status=200, reason='OK', headers={'Content-Type': 'text/plain'})
411
+ await response.prepare(request)
412
+
413
+ full_response = ""
414
+ # Use ReAct streaming with tools
415
+ async for chunk in ollama_client.chat_with_tools_stream(
416
+ messages=history + [{"role": "user", "content": message}],
417
+ user_profile=profile,
418
+ context=ctx
419
+ ):
420
+ await response.write(chunk.encode())
421
+ full_response += chunk
422
+
423
+ await memory_manager.add_message(ctx["platform"], ctx["user_id"], "assistant", full_response)
424
+ await response.write_eof()
425
+ return response
426
+ except Exception as e:
427
+ logger.error(f"Stream Error: {e}")
428
+ return web.Response(text=f"Protocol Fault: {str(e)}", status=500)
429
+
430
+
431
+ @routes.post('/api/model/switch')
432
+ async def api_switch_model(request):
433
+ """Admin Only: Switch model"""
434
+ if not await check_admin(request): return web.json_response({"error": "Unauthorized"}, status=403)
435
+ try:
436
+ data = await request.json()
437
+ model_name = data.get('model')
438
+ success = await ollama_client.switch_model(model_name)
439
+ if success:
440
+ await memory_manager.set_global_setting("active_model", model_name)
441
+ return web.json_response({"success": success})
442
+ except Exception as e:
443
+ return web.json_response({"error": str(e)}, status=500)
444
+
445
+
446
+ DASHBOARD_HTML = """
447
+ <!DOCTYPE html>
448
+ <html lang="en" data-theme="dark">
449
+ <head>
450
+ <meta charset="UTF-8">
451
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
452
+ <title>YouClaw V4.7 | Justice Neural Hub 🦞</title>
453
+ <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
454
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
455
+ <style>
456
+ :root {
457
+ --bg: #050510;
458
+ --surface: rgba(17, 24, 39, 0.7);
459
+ --surface-hover: rgba(31, 41, 55, 0.85);
460
+ --border: rgba(255, 255, 255, 0.1);
461
+ --text-main: #ffffff;
462
+ --text-dim: #94a3b8;
463
+ --primary: #8b5cf6;
464
+ --accent: #d946ef;
465
+ --primary-gradient: linear-gradient(135deg, #6366f1 0%, #a855f7 100%);
466
+ --glass: blur(30px) saturate(180%);
467
+ --sidebar-width: 300px;
468
+ --bubble-user: var(--primary-gradient);
469
+ --bubble-ai: rgba(255, 255, 255, 0.05);
470
+ --nav-glow: 0 0 20px rgba(139, 92, 246, 0.4);
471
+ --primary-glow: rgba(139, 92, 246, 0.3);
472
+ --secondary: #10b981;
473
+ --danger: #ef4444;
474
+ --card-shadow: 0 20px 50px -12px rgba(0, 0, 0, 0.5);
475
+ }
476
+
477
+ [data-theme="light"] {
478
+ --bg: #f8fafc;
479
+ --surface: rgba(255, 255, 255, 0.9);
480
+ --surface-hover: #ffffff;
481
+ --border: rgba(0, 0, 0, 0.1);
482
+ --text-main: #0f172a;
483
+ --text-dim: #64748b;
484
+ --primary: #6366f1;
485
+ --accent: #d946ef;
486
+ --bubble-user: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);
487
+ --bubble-ai: #ffffff;
488
+ --primary-glow: rgba(99, 102, 241, 0.2);
489
+ --glass: blur(10px);
490
+ --input-bg: rgba(0, 0, 0, 0.03);
491
+ --input-area-bg: rgba(0, 0, 0, 0.02);
492
+ }
493
+
494
+ :root {
495
+ --input-bg: rgba(255, 255, 255, 0.05);
496
+ --input-area-bg: rgba(0, 0, 0, 0.2);
497
+ }
498
+
499
+ * { margin: 0; padding: 0; box-sizing: border-box; }
500
+
501
+ body {
502
+ font-family: 'Plus Jakarta Sans', sans-serif;
503
+ background: var(--bg);
504
+ color: var(--text-main);
505
+ min-height: 100vh;
506
+ display: flex;
507
+ overflow: hidden;
508
+ transition: all 0.4s ease;
509
+ }
510
+
511
+ .sidebar {
512
+ width: var(--sidebar-width);
513
+ height: 100vh;
514
+ background: var(--surface);
515
+ backdrop-filter: var(--glass);
516
+ border-right: 1px solid var(--border);
517
+ padding: 40px 24px;
518
+ display: flex;
519
+ flex-direction: column;
520
+ gap: 40px;
521
+ z-index: 100;
522
+ }
523
+ .sidebar-logo { font-size: 1.5rem; font-weight: 800; letter-spacing: -1px; margin-bottom: 20px; }
524
+ .nav-list { list-style: none; display: flex; flex-direction: column; gap: 8px; }
525
+ .nav-item {
526
+ padding: 16px 20px; border-radius: 16px; cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
527
+ display: flex; align-items: center; gap: 16px; font-weight: 700; color: var(--text-dim);
528
+ border: 1px solid transparent;
529
+ }
530
+ .nav-item i { font-style: normal; font-size: 1.2rem; filter: grayscale(1); transition: 0.3s; }
531
+ .nav-item:hover { background: var(--border); color: var(--text-main); transform: translateX(5px); }
532
+ .nav-item:hover i { filter: grayscale(0); transform: scale(1.1); }
533
+ .nav-item.active {
534
+ background: var(--primary); color: white; border-color: rgba(255,255,255,0.1);
535
+ box-shadow: 0 10px 30px var(--primary-glow);
536
+ }
537
+ .nav-item.active i { filter: grayscale(0); }
538
+
539
+ .main-stage {
540
+ flex: 1;
541
+ height: 100vh;
542
+ overflow-y: auto;
543
+ position: relative;
544
+ background: radial-gradient(at 0% 0%, var(--primary-glow) 0px, transparent 50%);
545
+ }
546
+
547
+ .container {
548
+ max-width: 1400px; margin: 0 auto; padding: 48px;
549
+ opacity: 0; transform: translateY(10px); transition: all 0.6s ease;
550
+ }
551
+ .container.active { opacity: 1; transform: translateY(0); }
552
+
553
+ .auth-portal {
554
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%;
555
+ background: rgba(2, 6, 23, 0.7); backdrop-filter: blur(40px);
556
+ z-index: 10000; display: flex; align-items: center; justify-content: center;
557
+ }
558
+ .auth-card { background: var(--surface); border: 1px solid var(--border); padding: 48px; border-radius: 40px; width: 100%; max-width: 440px; text-align: center; }
559
+
560
+ .header-bar {
561
+ display: flex; justify-content: flex-end; align-items: center; gap: 24px;
562
+ margin-bottom: 40px; padding: 0 0 20px 0; border-bottom: 1px solid var(--border);
563
+ }
564
+
565
+ .dashboard-grid { display: grid; grid-template-columns: repeat(12, 1fr); gap: 32px; }
566
+ .card {
567
+ background: var(--surface); backdrop-filter: var(--glass); border: 1px solid var(--border);
568
+ border-radius: 32px; padding: 32px; box-shadow: var(--card-shadow); transition: all 0.3s ease;
569
+ }
570
+ .card:hover { border-color: var(--primary); }
571
+ .card-title { font-size: 1.1rem; font-weight: 800; margin-bottom: 24px; display: flex; align-items: center; gap: 12px; }
572
+
573
+ .btn {
574
+ padding: 16px; border-radius: 16px; font-weight: 800; font-size: 0.9rem;
575
+ cursor: pointer; border: none; transition: all 0.3s ease; font-family: inherit;
576
+ display: flex; align-items: center; justify-content: center; gap: 10px;
577
+ }
578
+ .btn-primary { background: var(--primary); color: white; box-shadow: 0 4px 12px var(--primary-glow); }
579
+ .btn-outline { background: transparent; border: 2px solid var(--border); color: var(--text-main); }
580
+
581
+ .input-group { margin-bottom: 20px; }
582
+ .input-label { display: block; font-size: 0.75rem; color: var(--text-dim); margin-bottom: 8px; font-weight: 800; text-transform: uppercase; letter-spacing: 1px; }
583
+ .text-input {
584
+ width: 100%; background: rgba(0, 0, 0, 0.05); border: 1px solid var(--border);
585
+ border-radius: 16px; padding: 14px 20px; color: var(--text-main); font-family: inherit;
586
+ }
587
+
588
+ .stat-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; }
589
+ .stat-item { background: rgba(124, 58, 237, 0.05); padding: 24px; border-radius: 24px; text-align: center; border: 1px solid var(--border); }
590
+ .stat-val { font-size: 2rem; font-weight: 800; }
591
+
592
+ .toggle-row { display: flex; justify-content: space-between; align-items: center; padding: 12px 0; }
593
+ .switch { position: relative; display: inline-block; width: 44px; height: 24px; }
594
+ .switch input { opacity: 0; width: 0; height: 0; }
595
+ .slider {
596
+ position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0;
597
+ background-color: var(--border); transition: .4s; border-radius: 34px;
598
+ }
599
+ .slider:before {
600
+ position: absolute; content: ""; height: 18px; width: 18px; left: 3px; bottom: 3px;
601
+ background-color: white; transition: .4s; border-radius: 50%;
602
+ }
603
+ input:checked + .slider { background-color: var(--secondary); }
604
+ input:checked + .slider:before { transform: translateX(20px); }
605
+
606
+ #neural-terminal-view { display: none; padding: 20px; height: 100%; }
607
+ .chat-layout { height: 100%; display: grid; grid-template-columns: 1fr 340px; gap: 32px; max-width: 1600px; margin: 0 auto; }
608
+
609
+ .chat-main {
610
+ display: flex; flex-direction: column; background: var(--surface);
611
+ border: 1px solid var(--border); border-radius: 40px;
612
+ box-shadow: 0 30px 60px -12px rgba(0,0,0,0.5);
613
+ overflow: hidden; backdrop-filter: var(--glass);
614
+ }
615
+
616
+ #chat-messages {
617
+ flex: 1; overflow-y: auto; padding: 40px; display: flex;
618
+ flex-direction: column; gap: 32px; scroll-behavior: smooth;
619
+ background: radial-gradient(circle at top right, rgba(139, 92, 246, 0.05), transparent 40%);
620
+ }
621
+
622
+ .msg {
623
+ max-width: 75%; padding: 20px 28px; border-radius: 28px; line-height: 1.7;
624
+ font-size: 1rem; position: relative; transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
625
+ animation: msgEnter 0.5s cubic-bezier(0.4, 0, 0.2, 1);
626
+ }
627
+
628
+ @keyframes msgEnter { from { opacity: 0; transform: translateY(20px) scale(0.98); } to { opacity: 1; transform: translateY(0) scale(1); } }
629
+
630
+ .msg-user {
631
+ align-self: flex-end; background: var(--bubble-user); color: white;
632
+ border-bottom-right-radius: 4px; box-shadow: 0 15px 30px rgba(139, 92, 246, 0.3);
633
+ font-weight: 500;
634
+ }
635
+
636
+ .msg-ai {
637
+ align-self: flex-start; background: var(--bubble-ai); color: var(--text-main);
638
+ border-bottom-left-radius: 4px; border: 1px solid var(--border);
639
+ backdrop-filter: blur(10px); box-shadow: 0 10px 20px rgba(0,0,0,0.2);
640
+ }
641
+
642
+ .chat-input-area {
643
+ padding: 30px 40px; background: var(--input-area-bg);
644
+ border-top: 1px solid var(--border); display: flex; gap: 20px; align-items: center;
645
+ }
646
+
647
+ .chat-field {
648
+ flex: 1; background: var(--input-bg); border: 1px solid var(--border);
649
+ border-radius: 20px; padding: 18px 24px; color: var(--text-main); font-family: inherit;
650
+ font-size: 1rem; transition: 0.3s;
651
+ }
652
+ .chat-field:focus { border-color: var(--primary); outline: none; background: rgba(255,255,255,0.08); box-shadow: 0 0 20px var(--primary-glow); }
653
+
654
+ .pulse {
655
+ width: 8px; height: 8px; background: var(--accent); border-radius: 50%;
656
+ display: inline-block; margin-right: 8px; box-shadow: 0 0 10px var(--accent);
657
+ animation: synapticPulse 1.5s infinite;
658
+ }
659
+ @keyframes synapticPulse { 0% { opacity: 0.3; transform: scale(0.8); } 50% { opacity: 1; transform: scale(1.2); } 100% { opacity: 0.3; transform: scale(0.8); } }
660
+
661
+ .send-btn {
662
+ width: 56px; height: 56px; border-radius: 20px; background: var(--primary-gradient);
663
+ border: none; color: white; cursor: pointer; display: flex; align-items: center;
664
+ justify-content: center; font-size: 1.2rem; transition: 0.3s;
665
+ box-shadow: 0 10px 20px var(--primary-glow);
666
+ }
667
+ .send-btn:hover { transform: scale(1.05) rotate(5deg); box-shadow: 0 15px 30px var(--primary-glow); }
668
+ .send-btn:disabled { opacity: 0.5; cursor: wait; }
669
+ .p-pill {
670
+ padding: 10px 18px; border-radius: 14px; background: rgba(0,0,0,0.1);
671
+ border: 1px solid var(--border); cursor: pointer; transition: all 0.3s;
672
+ font-size: 0.8rem; font-weight: 700; color: var(--text-dim);
673
+ }
674
+ .p-pill:hover { border-color: var(--primary); color: var(--text-main); }
675
+ .p-pill.active { background: var(--primary); color: white; border-color: var(--primary); box-shadow: 0 4px 12px var(--primary-glow); }
676
+
677
+ .dot {
678
+ width: 5px; height: 5px; background: var(--primary); border-radius: 50%;
679
+ opacity: 0.6; box-shadow: 0 0 8px var(--primary);
680
+ animation: pulse-neon 1.5s infinite ease-in-out;
681
+ }
682
+ .dot:nth-child(2) { animation-delay: 0.2s; }
683
+ .dot:nth-child(3) { animation-delay: 0.4s; }
684
+ @keyframes pulse-neon {
685
+ 0%, 100% { transform: scale(1); opacity: 0.4; box-shadow: 0 0 2px var(--primary); }
686
+ 50% { transform: scale(1.4); opacity: 1; box-shadow: 0 0 12px var(--primary); }
687
+ }
688
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
689
+
690
+ .eye-btn {
691
+ position: absolute; right: 12px; top: 50%; transform: translateY(-50%);
692
+ background: none; border: none; cursor: pointer; font-size: 1.2rem;
693
+ opacity: 0.6; transition: opacity 0.2s;
694
+ }
695
+ .eye-btn:hover { opacity: 1; }
696
+
697
+ #web-search-toggle { transition: all 0.3s; border: 1px solid var(--border); }
698
+ #web-search-toggle.active { background: rgba(0, 255, 157, 0.2); color: var(--secondary); border-color: var(--secondary); box-shadow: 0 0 10px rgba(0,255,157,0.2); }
699
+
700
+ .channel-group {
701
+ background: rgba(255, 255, 255, 0.02); padding: 20px; border-radius: 20px; border: 1px solid var(--border);
702
+ transition: all 0.3s ease;
703
+ }
704
+ .channel-group.offline { opacity: 0.4; pointer-events: none; }
705
+ .channel-group.offline .switch { pointer-events: all; }
706
+
707
+ #admin-panel { display: none; }
708
+ ::-webkit-scrollbar { width: 8px; }
709
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 10px; }
710
+ </style>
711
+ </head>
712
+ <body>
713
+
714
+ <!-- Auth Layer -->
715
+ <div id="auth-overlay" class="auth-portal">
716
+ <div class="auth-card" id="login-view">
717
+ <div style="font-size: 4rem; margin-bottom: 32px;">🦞</div>
718
+ <h1 style="margin-bottom: 8px; font-weight: 800;">YOUCLAW</h1>
719
+ <p style="color: var(--text-dim); margin-bottom: 32px;">Platform Control Center</p>
720
+ <div class="input-group"><span class="input-label">Username</span><input type="text" id="auth-username" class="text-input"></div>
721
+ <div class="input-group"><span class="input-label">Password</span><input type="password" id="auth-password" class="text-input"></div>
722
+ <button class="btn btn-primary" style="width: 100%;" onclick="doLogin()">Enter System 🔐</button>
723
+ <button class="btn btn-outline" style="width: 100%; margin-top: 16px;" onclick="showAuthView('register-view')">Register Agent</button>
724
+ </div>
725
+ <div class="auth-card" id="register-view" style="display: none;">
726
+ <h1 style="margin-bottom: 8px; font-weight: 800;">New Profile</h1>
727
+ <p style="color: var(--text-dim); margin-bottom: 32px;">Initialize Neural Protocol</p>
728
+ <div class="input-group"><span class="input-label">Username</span><input type="text" id="reg-username" class="text-input"></div>
729
+ <div class="input-group"><span class="input-label">Password</span><input type="password" id="reg-password" class="text-input"></div>
730
+ <button class="btn btn-primary" style="width: 100%;" onclick="doRegister()">Initialize Account 🚀</button>
731
+ <button class="btn btn-outline" style="width: 100%; margin-top: 16px;" onclick="showAuthView('login-view')">Back to Auth</button>
732
+ </div>
733
+ <div class="auth-card" id="link-view" style="display: none;">
734
+ <h1 style="margin-bottom: 8px; font-weight: 800;">Neural Link</h1>
735
+ <p style="color: var(--text-dim); margin-bottom: 32px;">Sync platform identity</p>
736
+ <div class="input-group"><span class="input-label">Architecture</span><select id="link-platform" class="text-input"><option value="telegram">Telegram</option><option value="discord">Discord</option></select></div>
737
+ <div class="input-group"><span class="input-label">Protocol ID</span><input type="text" id="link-id" class="text-input" placeholder="Platform user ID"></div>
738
+ <button class="btn btn-primary" style="width: 100%;" onclick="doLink()">Secure Link 🔗</button>
739
+ </div>
740
+ </div>
741
+
742
+ <!-- Sidebar Navigation -->
743
+ <nav class="sidebar" id="app-sidebar" style="display: none;">
744
+ <div>
745
+ <div class="sidebar-logo">🦞 YOUCLAW <span style="font-size: 0.6rem; color: var(--primary);">V4.6</span></div>
746
+ <div class="nav-list">
747
+ <div class="nav-item active" id="nav-dash" onclick="switchView('dashboard')"><i>📊</i> Control Center</div>
748
+ <div class="nav-item" id="nav-chat" onclick="switchView('chat')"><i>💬</i> Neural Terminal</div>
749
+ </div>
750
+ </div>
751
+ <div style="margin-top: auto;">
752
+ <div style="font-size: 0.7rem; color: var(--text-dim); font-weight: 800; text-transform: uppercase;">Operator</div>
753
+ <div id="display-user" style="font-weight: 800; color: var(--primary); margin-top: 4px;">...</div>
754
+ <button class="btn btn-outline" style="margin-top: 20px; width: 100%; padding: 12px;" onclick="doLogout()">Sign Out</button>
755
+ </div>
756
+ </nav>
757
+
758
+ <!-- Main Stage -->
759
+ <main class="main-stage">
760
+ <div class="container" id="main-container">
761
+ <div class="header-bar">
762
+ <button class="theme-toggle" id="theme-btn" onclick="toggleTheme()">🌓</button>
763
+ </div>
764
+
765
+ <!-- View 1: Control Center Dashboard -->
766
+ <div id="dashboard-view">
767
+ <div class="dashboard-grid">
768
+ <div class="card" style="grid-column: span 12; border-color: var(--danger); display: none;" id="link-warning">
769
+ <div style="display: flex; justify-content: space-between; align-items: center;">
770
+ <div><h3 style="color: var(--danger); font-weight: 800;">⚠️ Connectivity Restricted</h3><p style="color: var(--text-dim);">Identity link required for full mission capability.</p></div>
771
+ <button class="btn btn-primary" style="width: auto;" onclick="showAuthView('link-view')">Sync Identity</button>
772
+ </div>
773
+ </div>
774
+
775
+ <div class="card" style="grid-column: span 12;">
776
+ <div class="card-title">📊 Intelligence Summary</div>
777
+ <div class="stat-grid">
778
+ <div class="stat-item"><div class="input-label">Neural States</div><div class="stat-val" id="stat-messages">0</div></div>
779
+ <div class="stat-item"><div class="input-label">Connectivity</div><div class="stat-val" style="color: var(--secondary);">OPTIMAL</div></div>
780
+ <div class="stat-item"><div class="input-label">Active Engine</div><div class="stat-val" id="stat-model" style="font-size: 1rem; color: var(--primary);">...</div></div>
781
+ </div>
782
+ </div>
783
+
784
+ <div class="card" style="grid-column: span 6;">
785
+ <div class="card-title">🌐 Platform Connectivity</div>
786
+ <div style="display: flex; flex-direction: column; gap: 24px;">
787
+ <div class="channel-group">
788
+ <div class="toggle-row" style="padding: 0; margin-bottom: 12px;">
789
+ <div><span style="font-weight: 700;">Telegram Protocol</span><div style="font-size: 0.8rem; color: var(--text-dim);">Active relay to global network</div></div>
790
+ <label class="switch"><input type="checkbox" id="toggle-tg" onchange="toggleChannel('telegram', this)"><span class="slider"></span></label>
791
+ </div>
792
+ <div class="input-group" style="margin: 0; position: relative;">
793
+ <input type="password" id="vault-tg" class="text-input" placeholder="Telegram Token" style="padding-right: 50px;">
794
+ <button class="eye-btn" onclick="toggleSecret('vault-tg')">👁️</button>
795
+ </div>
796
+ </div>
797
+
798
+ <div class="channel-group">
799
+ <div class="toggle-row" style="padding: 0; margin-bottom: 12px;">
800
+ <div><span style="font-weight: 700;">Discord Architecture</span><div style="font-size: 0.8rem; color: var(--text-dim);">Secure tunnel to Discord guild</div></div>
801
+ <label class="switch"><input type="checkbox" id="toggle-dc" onchange="toggleChannel('discord', this)"><span class="slider"></span></label>
802
+ </div>
803
+ <div class="input-group" style="margin: 0; position: relative;">
804
+ <input type="password" id="vault-dc" class="text-input" placeholder="Discord Token" style="padding-right: 50px;">
805
+ <button class="eye-btn" onclick="toggleSecret('vault-dc')">👁️</button>
806
+ </div>
807
+ </div>
808
+
809
+ <div class="channel-group">
810
+ <div style="margin-bottom: 12px;">
811
+ <span style="font-weight: 700;">Neural Search Node</span>
812
+ <div style="font-size: 0.8rem; color: var(--text-dim);">Deep pulse data source</div>
813
+ </div>
814
+ <div class="input-group" style="margin: 0;">
815
+ <input type="text" id="vault-search" class="text-input" placeholder="http://ip:port/search">
816
+ </div>
817
+ </div>
818
+
819
+ <div class="channel-group">
820
+ <div class="toggle-row" style="padding: 0; margin-bottom: 12px;">
821
+ <div><span style="font-weight: 700;">Email Node Protocol</span><div style="font-size: 0.8rem; color: var(--text-dim);">Neural link to IMAP/SMTP</div></div>
822
+ <label class="switch"><input type="checkbox" id="toggle-email" onchange="toggleChannel('email', this)"><span class="slider"></span></label>
823
+ </div>
824
+ <div style="display: grid; grid-template-columns: 1fr 80px; gap: 8px; margin-bottom: 8px;">
825
+ <input type="text" id="vault-imap-host" class="text-input" placeholder="IMAP Host">
826
+ <input type="number" id="vault-imap-port" class="text-input" placeholder="Port" value="993">
827
+ </div>
828
+ <div style="display: grid; grid-template-columns: 1fr 80px; gap: 8px; margin-bottom: 8px;">
829
+ <input type="text" id="vault-smtp-host" class="text-input" placeholder="SMTP Host">
830
+ <input type="number" id="vault-smtp-port" class="text-input" placeholder="Port" value="587">
831
+ </div>
832
+ <div class="input-group" style="margin: 0; margin-bottom: 8px;">
833
+ <input type="text" id="vault-email-user" class="text-input" placeholder="Email User">
834
+ </div>
835
+ <div class="input-group" style="margin: 0; position: relative;">
836
+ <input type="password" id="vault-email-pass" class="text-input" placeholder="Email Password" style="padding-right: 50px;">
837
+ <button class="eye-btn" onclick="toggleSecret('vault-email-pass')">👁️</button>
838
+ </div>
839
+ </div>
840
+
841
+ <button class="btn btn-primary" style="margin-top: 8px;" onclick="saveSecrets(this)">Secure Vault 🔐</button>
842
+ </div>
843
+ </div>
844
+
845
+ <div class="card" style="grid-column: span 6;">
846
+ <div class="card-title">⚡ Automate Mission (Cron Job)</div>
847
+ <div class="input-group">
848
+ <span class="input-label">Briefing Protocol</span>
849
+ <input type="text" id="cron-prompt" class="text-input" placeholder="e.g. Give me a summary of today's tech news">
850
+ </div>
851
+ <div style="display: flex; gap: 16px;">
852
+ <div class="input-group" style="flex: 1;">
853
+ <span class="input-label">Frequency (Min)</span>
854
+ <input type="number" id="cron-freq" class="text-input" value="60">
855
+ </div>
856
+ <div class="input-group" style="flex: 1;">
857
+ <span class="input-label">Target Channel</span>
858
+ <select id="cron-channel" class="text-input">
859
+ <option value="telegram" id="cron-opt-tg">Telegram Bot</option>
860
+ <option value="discord" id="cron-opt-dc">Discord Bot</option>
861
+ </select>
862
+ </div>
863
+ </div>
864
+ <button class="btn btn-primary" style="width: 100%;" onclick="scheduleCron()">Activate Cron Job 🚀</button>
865
+ </div>
866
+
867
+ <div class="card" style="grid-column: span 12;">
868
+ <div class="card-title">⏰ Active Cron Jobs</div>
869
+ <div id="jobs-list" class="dashboard-grid" style="grid-template-columns: repeat(3, 1fr); gap: 24px;"></div>
870
+ </div>
871
+ </div>
872
+
873
+ <div id="admin-panel" style="margin-top: 32px;">
874
+ <h2 style="font-size: 0.8rem; letter-spacing: 3px; color: var(--text-dim); margin-bottom: 24px;">ROOT PROTOCOLS</h2>
875
+ <div class="dashboard-grid">
876
+ <div class="card" style="grid-column: span 12;">
877
+ <div class="card-title">⚙️ Core Migration</div>
878
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 24px; align-items: end;">
879
+ <div class="input-group" style="margin: 0;">
880
+ <span class="input-label">Standard Engines</span>
881
+ <select id="model-list" class="text-input"></select>
882
+ </div>
883
+ <div style="display: flex; gap: 16px;">
884
+ <button class="btn btn-outline" style="flex: 1;" onclick="switchModel(this)">Migrate Core</button>
885
+ <button class="btn btn-outline" style="flex: 1; color: var(--danger); border-color: var(--danger);" onclick="clearMemory()">Purge States</button>
886
+ </div>
887
+ </div>
888
+ </div>
889
+ <div class="card" style="grid-column: span 12; margin-top: 24px;">
890
+ <div class="card-title">🧬 Neural Soul Architecture</div>
891
+ <div style="display: grid; grid-template-columns: 1fr auto; gap: 24px; align-items: start;">
892
+ <div class="input-group" style="margin: 0;">
893
+ <span class="input-label">Active Personality</span>
894
+ <div id="personality-container" style="display: flex; gap: 12px; flex-wrap: wrap; margin-top: 8px;"></div>
895
+ </div>
896
+ <div class="stat-item" style="border: 1px solid var(--border); background: rgba(0,0,0,0.1); padding: 16px 24px;">
897
+ <div class="input-label">Current Soul</div>
898
+ <div class="stat-val" id="current-soul-display" style="font-size: 1.1rem; color: var(--primary);">Loading...</div>
899
+ </div>
900
+ </div>
901
+ </div>
902
+ </div>
903
+ </div>
904
+ </div>
905
+
906
+ </div>
907
+
908
+ <!-- View 2: Neural Terminal Chatbox -->
909
+ <div id="neural-terminal-view">
910
+ <div class="chat-layout">
911
+ <div class="chat-main">
912
+ <div id="chat-messages"></div>
913
+ <div class="chat-input-area">
914
+ <input type="text" id="chat-input" class="chat-field" placeholder="Whisper your intent..." onkeydown="if(event.key==='Enter') sendChat()">
915
+ <button id="send-btn" class="send-btn" onclick="sendChat()">🚀</button>
916
+ </div>
917
+ </div>
918
+
919
+ <div class="chat-sidebar">
920
+ <div class="card" style="height: 100%;">
921
+ <div class="card-title">✨ Active Neural Soul</div>
922
+ <div id="current-soul-display-chat" style="font-size: 1.5rem; font-weight: 800; color: var(--accent); margin-bottom: 24px;">Syncing...</div>
923
+
924
+ <div class="input-label">Shift Frequency</div>
925
+ <div id="personality-container-chat" style="display: flex; flex-wrap: wrap; gap: 10px; margin-top: 12px;"></div>
926
+
927
+ <div style="margin-top: 40px;">
928
+ <div class="input-label">System Statistics</div>
929
+ <div style="display: flex; flex-direction: column; gap: 16px; margin-top: 12px;">
930
+ <div style="display: flex; justify-content: space-between;">
931
+ <span style="color: var(--text-dim);">Neural Core</span>
932
+ <span id="stat-model-chat" style="font-weight: 800;">Loading...</span>
933
+ </div>
934
+ <div style="display: flex; justify-content: space-between;">
935
+ <span style="color: var(--text-dim);">Synapses Fried</span>
936
+ <span id="stat-messages-chat" style="font-weight: 800;">0</span>
937
+ </div>
938
+ </div>
939
+ </div>
940
+
941
+ <div style="margin-top: 40px; padding: 20px; background: rgba(0,0,0,0.1); border-radius: 20px; border: 1px solid var(--border);">
942
+ <div class="input-label" style="margin-bottom: 12px;">Neural Activity</div>
943
+ <div id="search-status" style="font-size: 0.8rem; color: var(--text-dim);">
944
+ <span class="pulse" style="background: var(--secondary);"></span> Listening for queries...
945
+ </div>
946
+ </div>
947
+ </div>
948
+ </div>
949
+ </div>
950
+ </div>
951
+ </main>
952
+
953
+ <script>
954
+ let session_user = null;
955
+ let state_hash = { jobs: '', stats: '' };
956
+
957
+ function toggleTheme() {
958
+ const html = document.documentElement;
959
+ const newTheme = html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
960
+ html.setAttribute('data-theme', newTheme);
961
+ localStorage.setItem('yc_theme', newTheme);
962
+ document.getElementById('theme-btn').innerText = newTheme === 'dark' ? '🌓' : '☀️';
963
+ }
964
+
965
+ function toggleSecret(id) {
966
+ const el = document.getElementById(id);
967
+ el.type = el.type === 'password' ? 'text' : 'password';
968
+ }
969
+
970
+ let webMode = false;
971
+ function toggleWebMode() {
972
+ webMode = !webMode;
973
+ document.getElementById('web-search-toggle').classList.toggle('active', webMode);
974
+ }
975
+
976
+ function switchView(view) {
977
+ document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'));
978
+ document.getElementById('dashboard-view').style.display = 'none';
979
+ document.getElementById('neural-terminal-view').style.display = 'none';
980
+
981
+ if(view === 'dashboard') {
982
+ document.getElementById('nav-dash').classList.add('active');
983
+ document.getElementById('dashboard-view').style.display = 'block';
984
+ } else {
985
+ document.getElementById('nav-chat').classList.add('active');
986
+ document.getElementById('neural-terminal-view').style.display = 'block';
987
+ document.getElementById('chat-input').focus();
988
+ }
989
+ }
990
+
991
+ function showAuthView(id) {
992
+ document.querySelectorAll('.auth-card').forEach(v => v.style.display = 'none');
993
+ document.getElementById(id).style.display = 'block';
994
+ document.getElementById('auth-overlay').style.display = 'flex';
995
+ }
996
+
997
+ async function doRegister() {
998
+ const u = document.getElementById('reg-username').value;
999
+ const p = document.getElementById('reg-password').value;
1000
+ const res = await apiCall('/api/auth/register', 'POST', { username:u, password:p });
1001
+ if(res.success) { alert("Access granted. Initializing auth..."); showAuthView('login-view'); }
1002
+ else alert("Protocol Denied: " + res.error);
1003
+ }
1004
+
1005
+ async function doLogin() {
1006
+ const u = document.getElementById('auth-username').value;
1007
+ const p = document.getElementById('auth-password').value;
1008
+ const res = await apiCall('/api/auth/login', 'POST', { username:u, password:p });
1009
+ if(res.success && res.user.token) {
1010
+ session_user = u;
1011
+ localStorage.setItem('yc_session_user', u);
1012
+ localStorage.setItem('yc_session_token', res.user.token);
1013
+ initDashboard();
1014
+ } else alert("Neural auth failed.");
1015
+ }
1016
+
1017
+ async function doLink() {
1018
+ const p = document.getElementById('link-platform').value;
1019
+ const id = document.getElementById('link-id').value;
1020
+ const res = await apiCall('/api/auth/link', 'POST', { platform:p, user_id:id });
1021
+ if(res.success) { alert("Link Established."); location.reload(); }
1022
+ else alert("Link Fault.");
1023
+ }
1024
+
1025
+ function doLogout() {
1026
+ localStorage.removeItem('yc_session_user');
1027
+ localStorage.removeItem('yc_session_token');
1028
+ location.reload();
1029
+ }
1030
+
1031
+ function toggleTheme() {
1032
+ const current = document.documentElement.getAttribute('data-theme');
1033
+ const target = current === 'dark' ? 'light' : 'dark';
1034
+ document.documentElement.setAttribute('data-theme', target);
1035
+ localStorage.setItem('yc_theme', target);
1036
+ document.getElementById('theme-btn').innerText = target === 'dark' ? '🌓' : '☀️';
1037
+ }
1038
+
1039
+ async function apiCall(url, method = 'GET', body = null) {
1040
+ const token = localStorage.getItem('yc_session_token');
1041
+ const h = {
1042
+ 'X-Session-User': session_user,
1043
+ 'X-Session-Token': token,
1044
+ 'Content-Type': 'application/json'
1045
+ };
1046
+ const options = { method, headers: h };
1047
+ if (body) options.body = JSON.stringify(body);
1048
+ const res = await fetch(url, options);
1049
+ const data = await res.json();
1050
+ return data;
1051
+ }
1052
+
1053
+ async function initDashboard() {
1054
+ session_user = localStorage.getItem('yc_session_user');
1055
+ if(!session_user) return showAuthView('login-view');
1056
+
1057
+ const savedTheme = localStorage.getItem('yc_theme') || 'dark';
1058
+ document.documentElement.setAttribute('data-theme', savedTheme);
1059
+ document.getElementById('theme-btn').innerText = savedTheme === 'dark' ? '🌓' : '☀️';
1060
+
1061
+ document.getElementById('auth-overlay').style.display = 'none';
1062
+ document.getElementById('app-sidebar').style.display = 'flex';
1063
+ document.getElementById('main-container').classList.add('active');
1064
+ document.getElementById('display-user').innerText = session_user;
1065
+
1066
+ updateDashboard();
1067
+ setInterval(updateDashboard, 5000);
1068
+ }
1069
+
1070
+ async function updateDashboard() {
1071
+ const stats = await apiCall('/api/stats');
1072
+ if(!stats || stats.error) return;
1073
+
1074
+ document.getElementById('link-warning').style.display = stats.is_linked ? 'none' : 'block';
1075
+ document.getElementById('stat-messages').innerText = stats.user_messages;
1076
+ document.getElementById('stat-model').innerText = stats.model;
1077
+
1078
+ const msgChat = document.getElementById('stat-messages-chat');
1079
+ const modelChat = document.getElementById('stat-model-chat');
1080
+ if(msgChat) msgChat.innerText = stats.user_messages;
1081
+ if(modelChat) modelChat.innerText = stats.model;
1082
+
1083
+ document.getElementById('toggle-tg').checked = stats.telegram_enabled;
1084
+ document.getElementById('toggle-dc').checked = stats.discord_enabled;
1085
+
1086
+ // Sync Cron Channel Detection
1087
+ const cronTg = document.getElementById('cron-opt-tg');
1088
+ const cronDc = document.getElementById('cron-opt-dc');
1089
+ cronTg.disabled = !stats.telegram_enabled;
1090
+ cronDc.disabled = !stats.discord_enabled;
1091
+ cronTg.innerText = stats.telegram_enabled ? "Telegram Bot" : "Telegram (Offline)";
1092
+ cronDc.innerText = stats.discord_enabled ? "Discord Bot" : "Discord (Offline)";
1093
+
1094
+ // Update Channel Group States
1095
+ document.getElementById('toggle-tg').closest('.channel-group').classList.toggle('offline', !stats.telegram_enabled);
1096
+ document.getElementById('toggle-dc').closest('.channel-group').classList.toggle('offline', !stats.discord_enabled);
1097
+
1098
+ if(stats.is_admin) {
1099
+ document.getElementById('admin-panel').style.display = 'block';
1100
+ const ms = document.getElementById('model-list');
1101
+ if(ms.options.length === 0 && stats.available_models) {
1102
+ stats.available_models.forEach(m => {
1103
+ const o = document.createElement('option'); o.value = o.innerText = m;
1104
+ if(m === stats.model) o.selected = true;
1105
+ ms.appendChild(o);
1106
+ });
1107
+ }
1108
+ if(!window.vaultLoaded) { loadVault(); window.vaultLoaded = true; }
1109
+ if(!window.personalitiesLoaded) loadPersonalities();
1110
+ }
1111
+
1112
+ const jobs = await apiCall('/api/jobs');
1113
+ if(jobs && !jobs.error && JSON.stringify(jobs.jobs) !== state_hash.jobs) {
1114
+ document.getElementById('jobs-list').innerHTML = jobs.jobs.map(j => `
1115
+ <div class="item-card">
1116
+ <div style="font-weight: 800; font-size: 0.9rem;">#${j.id}</div>
1117
+ <div style="font-size: 0.8rem; color: var(--text-dim); margin: 8px 0;">${j.prompt.substring(0,60)}...</div>
1118
+ <button onclick="deleteJob('${j.id}')" style="background:var(--danger); border:none; color:white; padding:6px 12px; border-radius:8px; font-weight:800; cursor:pointer; font-size:0.7rem;">ABORT</button>
1119
+ </div>
1120
+ `).join('') || '<div style="grid-column: span 3; color: var(--text-dim); text-align:center;">No active heartbeats</div>';
1121
+ state_hash.jobs = JSON.stringify(jobs.jobs);
1122
+ }
1123
+
1124
+ const convs = await apiCall('/api/conversations');
1125
+ if(convs && !convs.error) {
1126
+ document.getElementById('conversations-list').innerHTML = convs.conversations.map(c => `
1127
+ <div class="item-card">
1128
+ <div style="font-size: 0.85rem; font-weight: 800;">Thread: ${c.channel_id.substring(0,8)}...</div>
1129
+ <div style="font-size: 0.75rem; color: var(--text-dim);">${c.message_count} states synced</div>
1130
+ </div>
1131
+ `).join('');
1132
+ }
1133
+
1134
+ // Sync Mission Control if active
1135
+ const missionView = document.getElementById('mission-view');
1136
+ if(missionView && missionView.style.display === 'block') {
1137
+ updateMissionControl();
1138
+ }
1139
+ }
1140
+
1141
+ async function toggleChannel(channel, el) {
1142
+ const res = await apiCall('/api/system/toggle_channel', 'POST', { channel: channel, enabled: el.checked });
1143
+ if(!res.success) { alert("Toggle Failure."); el.checked = !el.checked; }
1144
+ }
1145
+
1146
+ async function scheduleCron() {
1147
+ const prompt = document.getElementById('cron-prompt').value;
1148
+ const freq = document.getElementById('cron-freq').value;
1149
+ const sel = document.getElementById('cron-channel');
1150
+ const channel = sel.value;
1151
+ if(!prompt) return alert("Briefing required.");
1152
+ if(sel.options[sel.selectedIndex].disabled) return alert("Selected channel is offline. Activate it first.");
1153
+
1154
+ const res = await apiCall('/api/jobs/schedule', 'POST', {
1155
+ prompt: prompt,
1156
+ frequency: freq,
1157
+ channel: channel
1158
+ });
1159
+ if(res.success) {
1160
+ alert("Cron Job Activated! 🚀");
1161
+ document.getElementById('cron-prompt').value = '';
1162
+ updateDashboard();
1163
+ } else alert("Activation Fault: " + res.error);
1164
+ }
1165
+
1166
+ async function sendChat() {
1167
+ const input = document.getElementById('chat-input');
1168
+ const btn = document.getElementById('send-btn');
1169
+ const msg = input.value.trim();
1170
+ if(!msg || btn.disabled) return;
1171
+
1172
+ const box = document.getElementById('chat-messages');
1173
+ box.innerHTML += `<div class="msg msg-user">${msg}</div>`;
1174
+ input.value = '';
1175
+ input.disabled = btn.disabled = true;
1176
+
1177
+ const indicator = document.createElement('div');
1178
+ indicator.className = 'typing-indicator';
1179
+ indicator.innerHTML = '<div class="typing-label">Thinking</div><div class="dot"></div><div class="dot"></div><div class="dot"></div>';
1180
+ box.appendChild(indicator);
1181
+ box.scrollTop = box.scrollHeight;
1182
+
1183
+ const searchStatus = document.getElementById('search-status');
1184
+ if(searchStatus) searchStatus.innerHTML = '<span class="pulse"></span> Synapsing Neural Streams...';
1185
+
1186
+ try {
1187
+ const url = `/api/chat/stream?user=${encodeURIComponent(session_user)}&message=${encodeURIComponent(msg)}`;
1188
+ const response = await fetch(url);
1189
+ const reader = response.body.getReader();
1190
+ const decoder = new TextDecoder();
1191
+
1192
+ const aiMsg = document.createElement('div');
1193
+ aiMsg.className = 'msg msg-ai';
1194
+ let fullAiResponse = '';
1195
+
1196
+ let firstChunk = true;
1197
+ while (true) {
1198
+ const { done, value } = await reader.read();
1199
+ if (done) break;
1200
+
1201
+ if (firstChunk) {
1202
+ indicator.remove();
1203
+ box.appendChild(aiMsg);
1204
+ firstChunk = false;
1205
+ }
1206
+
1207
+ const chunk = decoder.decode(value, { stream: true });
1208
+ fullAiResponse += chunk;
1209
+ aiMsg.innerHTML = marked.parse(fullAiResponse);
1210
+ box.scrollTop = box.scrollHeight;
1211
+ }
1212
+ } catch (err) {
1213
+ if(indicator.parentNode) indicator.remove();
1214
+ box.innerHTML += `<div class="msg msg-ai" style="color:var(--danger)">Protocol Fault</div>`;
1215
+ } finally {
1216
+ input.disabled = btn.disabled = false;
1217
+ input.focus();
1218
+ box.scrollTop = box.scrollHeight;
1219
+ if(searchStatus) searchStatus.innerHTML = '<span class="pulse" style="background: var(--secondary);"></span> Listening for queries...';
1220
+ updateDashboard();
1221
+ }
1222
+ }
1223
+
1224
+ async function deleteJob(id) {
1225
+ if(!confirm("Terminate this mission?")) return;
1226
+ const res = await apiCall('/api/jobs/delete', 'POST', { job_id: id });
1227
+ if(res.success) updateDashboard();
1228
+ }
1229
+
1230
+ async function loadVault() {
1231
+ const data = await apiCall('/api/system/env');
1232
+ if(data && !data.error) {
1233
+ document.getElementById('vault-tg').value = data.telegram_token || "";
1234
+ document.getElementById('vault-dc').value = data.discord_token || "";
1235
+ document.getElementById('vault-search').value = data.search_url || "";
1236
+
1237
+ if(data.email) {
1238
+ document.getElementById('toggle-email').checked = data.email.enabled;
1239
+ document.getElementById('vault-imap-host').value = data.email.imap_host || "";
1240
+ document.getElementById('vault-imap-port').value = data.email.imap_port || 993;
1241
+ document.getElementById('vault-smtp-host').value = data.email.smtp_host || "";
1242
+ document.getElementById('vault-smtp-port').value = data.email.smtp_port || 587;
1243
+ document.getElementById('vault-email-user').value = data.email.user || "";
1244
+ }
1245
+ }
1246
+ }
1247
+
1248
+ async function loadPersonalities() {
1249
+ const data = await apiCall('/api/system/personality');
1250
+ if(!data || data.error) return;
1251
+
1252
+ const container = document.getElementById('personality-container');
1253
+ const containerChat = document.getElementById('personality-container-chat');
1254
+ if(container) container.innerHTML = '';
1255
+ if(containerChat) containerChat.innerHTML = '';
1256
+
1257
+ for(const [id, p] of Object.entries(data.personalities)) {
1258
+ const pill = document.createElement('div');
1259
+ pill.className = `p-pill ${id === data.active ? 'active' : ''}`;
1260
+ pill.innerText = p.name;
1261
+ pill.title = p.description;
1262
+ pill.onclick = () => switchPersonality(id);
1263
+
1264
+ if(container) container.appendChild(pill);
1265
+
1266
+ const pillChat = pill.cloneNode(true);
1267
+ pillChat.onclick = () => switchPersonality(id);
1268
+ if(containerChat) containerChat.appendChild(pillChat);
1269
+ }
1270
+ const soulName = data.personalities[data.active].name;
1271
+ const soulDisp = document.getElementById('current-soul-display');
1272
+ const soulChat = document.getElementById('current-soul-display-chat');
1273
+ if(soulDisp) soulDisp.innerText = soulName;
1274
+ if(soulChat) soulChat.innerText = soulName;
1275
+
1276
+ window.personalitiesLoaded = true;
1277
+ }
1278
+
1279
+ async function switchPersonality(id) {
1280
+ const res = await apiCall('/api/system/personality', 'POST', { personality: id });
1281
+ if(res.success) {
1282
+ loadPersonalities();
1283
+ updateDashboard();
1284
+ } else alert("Personality shift failed.");
1285
+ }
1286
+
1287
+ async function saveSecrets(btn) {
1288
+ btn.innerText = "Syncing..."; btn.disabled = true;
1289
+ const res = await apiCall('/api/system/secrets', 'POST', {
1290
+ telegram_token: document.getElementById('vault-tg').value,
1291
+ discord_token: document.getElementById('vault-dc').value,
1292
+ search_url: document.getElementById('vault-search').value,
1293
+ email: {
1294
+ imap_host: document.getElementById('vault-imap-host').value,
1295
+ imap_port: parseInt(document.getElementById('vault-imap-port').value),
1296
+ smtp_host: document.getElementById('vault-smtp-host').value,
1297
+ smtp_port: parseInt(document.getElementById('vault-smtp-port').value),
1298
+ user: document.getElementById('vault-email-user').value,
1299
+ password: document.getElementById('vault-email-pass').value
1300
+ }
1301
+ });
1302
+ if(res.success) alert("Vault updated.");
1303
+ btn.innerText = "Apply Changes"; btn.disabled = false;
1304
+ }
1305
+
1306
+ async function switchModel(btn) {
1307
+ btn.innerText = "Migrating..."; btn.disabled = true;
1308
+ const res = await apiCall('/api/model/switch', 'POST', { model: document.getElementById('model-list').value });
1309
+ if(res.success) alert("Neural engine migrated.");
1310
+ btn.innerText = "Migrate Core"; btn.disabled = false;
1311
+ }
1312
+
1313
+ async function clearMemory() {
1314
+ if(!confirm("Purge neural state? Permanent action.")) return;
1315
+ await apiCall('/api/memory/clear', 'POST');
1316
+ updateDashboard();
1317
+ }
1318
+
1319
+ initDashboard();
1320
+ </script>
1321
+ </body>
1322
+ </html>
1323
+ """
1324
+
1325
+
1326
+ async def run_dashboard(bot_instance=None, port=8080):
1327
+ """Run the dashboard web server"""
1328
+ app = web.Application()
1329
+ app['bot'] = bot_instance
1330
+ app.router.add_routes(routes)
1331
+
1332
+ await ollama_client.initialize()
1333
+ await memory_manager.initialize()
1334
+
1335
+ print(f"🦞 YouClaw Dashboard starting on http://0.0.0.0:{port}")
1336
+ runner = web.AppRunner(app)
1337
+ await runner.setup()
1338
+ site = web.TCPSite(runner, '0.0.0.0', port)
1339
+ await site.start()
1340
+
1341
+ try: await asyncio.Event().wait()
1342
+ finally: await runner.cleanup()
1343
+
1344
+
1345
+ if __name__ == "__main__":
1346
+ asyncio.run(run_dashboard())
1347
+