MuhammadMahmoud commited on
Commit
5bd18a7
Β·
1 Parent(s): 252e5a2

enhance dash styles

Browse files
app/api/admin_ops.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, Body
2
+ from pydantic import BaseModel
3
+ import logging
4
+
5
+ from app.core.auth import verify_api_key
6
+ from app.services.chat.api.llm_router import llm_router, circuit_registry
7
+ import app.services.chat.api.llm_router as router_module
8
+ from app.core.redis_client import redis_client
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ router = APIRouter(
13
+ prefix="/admin",
14
+ tags=["admin"],
15
+ dependencies=[Depends(verify_api_key)]
16
+ )
17
+
18
+ class BulkheadUpdate(BaseModel):
19
+ limit: int
20
+
21
+ class CircuitTune(BaseModel):
22
+ failure_threshold: int
23
+ recovery_timeout: int
24
+
25
+ class ModelBanReq(BaseModel):
26
+ model_name: str
27
+
28
+ class KillSwitchReq(BaseModel):
29
+ active: bool
30
+
31
+
32
+ # ─── Provider Controls ────────────────────────────────────────────────────────
33
+
34
+ @router.post("/provider/{name}/disable")
35
+ async def disable_provider(name: str):
36
+ """Manually force a provider offline unconditionally."""
37
+ state = llm_router.provider_states.get(name)
38
+ if not state:
39
+ raise HTTPException(status_code=404, detail="Provider not found")
40
+ state.is_permanently_disabled = True
41
+ logger.critical(f"Admin action: Provider {name} FORCE DISABLED.")
42
+ return {"status": "success", "provider": name, "disabled": True}
43
+
44
+ @router.post("/provider/{name}/enable")
45
+ async def enable_provider(name: str):
46
+ """Restore a manually disabled provider."""
47
+ state = llm_router.provider_states.get(name)
48
+ if not state:
49
+ raise HTTPException(status_code=404, detail="Provider not found")
50
+ state.is_permanently_disabled = False
51
+ state.disable_until = 0.0
52
+ logger.warning(f"Admin action: Provider {name} ENABLED.")
53
+ return {"status": "success", "provider": name, "disabled": False}
54
+
55
+ @router.post("/provider/{name}/bulkhead")
56
+ async def update_bulkhead(name: str, payload: BulkheadUpdate):
57
+ """Dynamically resize concurrency limit to throttle/boost a provider."""
58
+ ok = llm_router.set_bulkhead_limit(name, payload.limit)
59
+ if not ok:
60
+ raise HTTPException(status_code=404, detail="Provider not found")
61
+ return {"status": "success", "provider": name, "new_limit": payload.limit}
62
+
63
+
64
+ # ─── Circuit Controls ─────────────────────────────────────────────────────────
65
+
66
+ @router.post("/circuit/{name}/trip")
67
+ async def trip_circuit(name: str):
68
+ """Force circuit OPEN (throws it into cooldown natively)."""
69
+ cb = circuit_registry.get(name)
70
+ if not cb:
71
+ raise HTTPException(status_code=404, detail="Circuit not found")
72
+ cb.record_failure(recovery_override=86400) # Open for 24h (admin tripped)
73
+ logger.critical(f"Admin action: Circuit for {name} TRIPPED.")
74
+ return {"status": "success", "provider": name, "circuit_state": cb.state.value}
75
+
76
+ @router.post("/circuit/{name}/reset")
77
+ async def reset_circuit(name: str):
78
+ """Force circuit CLOSED (instant heal)."""
79
+ cb = circuit_registry.get(name)
80
+ if not cb:
81
+ raise HTTPException(status_code=404, detail="Circuit not found")
82
+ cb.record_success()
83
+ state = llm_router.provider_states.get(name)
84
+ if state:
85
+ state.consecutive_failures = 0
86
+ state.cooldown_until = 0.0
87
+ logger.warning(f"Admin action: Circuit for {name} RESET to CLOSED.")
88
+ return {"status": "success", "provider": name, "circuit_state": cb.state.value}
89
+
90
+
91
+ @router.post("/circuit/{name}/tune")
92
+ async def tune_circuit(name: str, payload: CircuitTune):
93
+ """Dynamically adjust failure threshold and base recovery timeout."""
94
+ cb = circuit_registry.get(name)
95
+ if not cb:
96
+ raise HTTPException(status_code=404, detail="Circuit not found")
97
+ cb.failure_threshold = max(1, payload.failure_threshold)
98
+ cb._recovery_base = max(1, payload.recovery_timeout)
99
+ cb.recovery_timeout = cb._recovery_base
100
+ logger.warning(f"Admin action: Circuit {name} tuned (thr={cb.failure_threshold}, timeout={cb._recovery_base})")
101
+ return {"status": "success", "provider": name, "tuned": payload.dict()}
102
+
103
+
104
+ # ─── Model & Global Controls ──────────────────────────────────────────────────
105
+
106
+ @router.post("/model/ban")
107
+ async def ban_model(payload: ModelBanReq):
108
+ """Add a model string to the global banlist."""
109
+ router_module.banned_models.add(payload.model_name)
110
+ logger.critical(f"Admin action: Model {payload.model_name} GLOBALLY BANNED.")
111
+ return {"status": "success", "banned_models": list(router_module.banned_models)}
112
+
113
+ @router.post("/model/unban")
114
+ async def unban_model(payload: ModelBanReq):
115
+ """Remove a model string from the global banlist."""
116
+ router_module.banned_models.discard(payload.model_name)
117
+ logger.warning(f"Admin action: Model {payload.model_name} unbanned.")
118
+ return {"status": "success", "banned_models": list(router_module.banned_models)}
119
+
120
+ @router.post("/system/killswitch")
121
+ async def toggle_killswitch(payload: KillSwitchReq):
122
+ """Global emergency stop. All LLM calls return 503 instantly."""
123
+ router_module.KILL_SWITCH_ACTIVE = payload.active
124
+ if payload.active:
125
+ logger.critical("🚨 ADMIN ACTION: GLOBAL KILL SWITCH ACTIVATED. System halted.")
126
+ else:
127
+ logger.warning("🟒 ADMIN ACTION: GLOBAL KILL SWITCH DEACTIVATED. System restored.")
128
+ return {"status": "success", "kill_switch_active": payload.active}
129
+
130
+ @router.post("/system/flush-cache")
131
+ async def flush_cache():
132
+ """Nuke all Redis keys (cache & telemetry). Extreme warning."""
133
+ if redis_client.is_connected and redis_client.redis:
134
+ try:
135
+ await redis_client.redis.flushdb()
136
+ logger.critical("Admin action: Redis FLUSHDB executed.")
137
+ return {"status": "success", "msg": "Redis flushed"}
138
+ except Exception as e:
139
+ raise HTTPException(status_code=500, detail=str(e))
140
+ raise HTTPException(status_code=503, detail="Redis offline")
141
+
142
+ @router.get("/config-state")
143
+ async def get_config_state():
144
+ """Retrieve current dynamic config state for Dashboard UI sync."""
145
+ return {
146
+ "kill_switch_active": router_module.KILL_SWITCH_ACTIVE,
147
+ "banned_models": list(router_module.banned_models),
148
+ }
app/api/metrics.py CHANGED
@@ -1,6 +1,7 @@
1
  from fastapi import APIRouter
2
  from app.core.telemetry import telemetry
3
  from app.services.chat.api.llm_router import circuit_registry, llm_router
 
4
  from app.core.redis_client import redis_client
5
  from app.services.chat.memory.memory_manager import session_memory
6
  import time
@@ -52,6 +53,8 @@ async def get_metrics():
52
  "uptime_seconds": int(time.time() - START_TIME),
53
  "active_sessions": session_memory.get_stats().get("active_sessions", 0),
54
  "python_version": sys.version.split(" ")[0],
 
 
55
  }
56
 
57
  return {
 
1
  from fastapi import APIRouter
2
  from app.core.telemetry import telemetry
3
  from app.services.chat.api.llm_router import circuit_registry, llm_router
4
+ import app.services.chat.api.llm_router as router_module
5
  from app.core.redis_client import redis_client
6
  from app.services.chat.memory.memory_manager import session_memory
7
  import time
 
53
  "uptime_seconds": int(time.time() - START_TIME),
54
  "active_sessions": session_memory.get_stats().get("active_sessions", 0),
55
  "python_version": sys.version.split(" ")[0],
56
+ "kill_switch_active": router_module.KILL_SWITCH_ACTIVE,
57
+ "banned_models": list(router_module.banned_models),
58
  }
59
 
60
  return {
app/services/chat/api/llm_router.py CHANGED
@@ -32,6 +32,16 @@ from contextlib import asynccontextmanager
32
 
33
  logger = logging.getLogger(__name__)
34
 
 
 
 
 
 
 
 
 
 
 
35
 
36
  def _get_model_name(client) -> str:
37
  """Extract the first active model name from a provider client.
@@ -350,6 +360,26 @@ class LLMRouter:
350
  # Local fallback
351
  self._local_active_requests = max(0, self._local_active_requests - 1)
352
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
  def get_bulkhead_metrics(self) -> dict:
354
  data = {}
355
  for name, sem in self._semaphores.items():
@@ -443,6 +473,11 @@ class LLMRouter:
443
  access_token: Optional[str] = None,
444
  ) -> Tuple[str, List[ChatMessage], Optional[ToolConfirmationRequest]]:
445
 
 
 
 
 
 
446
  request_id = str(uuid.uuid4())[:8]
447
  last_error = None
448
  token, used_redis = await self._incr_requests()
@@ -462,6 +497,11 @@ class LLMRouter:
462
  call_api_bulkhead_skipped: list[str] = []
463
 
464
  for name, client in call_api_scored:
 
 
 
 
 
465
  cb = circuit_registry.get(name, failure_threshold=3, recovery_timeout=30)
466
  if not cb.can_execute():
467
  logger.debug(f"Circuit [{name}] is OPEN β€” skipping")
 
32
 
33
  logger = logging.getLogger(__name__)
34
 
35
+ # ──────────────────────────────────────────────────────────────────────────────
36
+ # Admin Control Primitives β€” controllable at runtime via /api/admin/*
37
+ # ──────────────────────────────────────────────────────────────────────────────
38
+
39
+ # Global kill switch β€” when True, ALL LLM calls are rejected immediately with 503
40
+ KILL_SWITCH_ACTIVE: bool = False
41
+
42
+ # Banned model signatures β€” any model in this set is skipped by all providers
43
+ banned_models: set[str] = set()
44
+
45
 
46
  def _get_model_name(client) -> str:
47
  """Extract the first active model name from a provider client.
 
360
  # Local fallback
361
  self._local_active_requests = max(0, self._local_active_requests - 1)
362
 
363
+ def set_bulkhead_limit(self, provider: str, new_limit: int) -> bool:
364
+ """Dynamically resize the concurrency semaphore for a provider (Admin API).
365
+
366
+ Creates or replaces the semaphore with a fresh asyncio.Semaphore.
367
+ Returns True if applied, False if the provider is unknown.
368
+ """
369
+ known = [n for n, _ in self.providers]
370
+ if provider not in known:
371
+ return False
372
+ new_limit = max(1, new_limit) # Always allow at least 1 slot
373
+ self._concurrency_budget[provider] = new_limit
374
+ # Replace the semaphore β€” existing in-flight calls on the old semaphore
375
+ # will complete normally; new acquisitions use the fresh one.
376
+ self._semaphores[provider] = asyncio.Semaphore(new_limit)
377
+ set_bulkhead_capacity(provider, new_limit)
378
+ logger.warning(
379
+ f"[Admin] Bulkhead for '{provider}' resized to {new_limit} concurrent slots."
380
+ )
381
+ return True
382
+
383
  def get_bulkhead_metrics(self) -> dict:
384
  data = {}
385
  for name, sem in self._semaphores.items():
 
473
  access_token: Optional[str] = None,
474
  ) -> Tuple[str, List[ChatMessage], Optional[ToolConfirmationRequest]]:
475
 
476
+ global KILL_SWITCH_ACTIVE, banned_models
477
+ if KILL_SWITCH_ACTIVE:
478
+ logger.critical("🚨 Global Kill Switch is ACTIVE. Rejecting llm call_api.")
479
+ raise HTTPException(status_code=503, detail="Service administrative pause.")
480
+
481
  request_id = str(uuid.uuid4())[:8]
482
  last_error = None
483
  token, used_redis = await self._incr_requests()
 
497
  call_api_bulkhead_skipped: list[str] = []
498
 
499
  for name, client in call_api_scored:
500
+ model_signature = _get_model_name(client)
501
+ if model_signature in banned_models:
502
+ logger.warning(f"Skipping {name} β€” model {model_signature} is globally BANNED.")
503
+ continue
504
+
505
  cb = circuit_registry.get(name, failure_threshold=3, recovery_timeout=30)
506
  if not cb.can_execute():
507
  logger.debug(f"Circuit [{name}] is OPEN β€” skipping")
app/static/dashboard.html CHANGED
@@ -10,16 +10,18 @@
10
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
11
  <style>
12
  :root {
13
- --bg-base: #0f111a;
14
- --bg-panel: #171a23;
15
- --bg-hover: #222633;
16
- --border: #2b3040;
17
  --text-main: #f8fafc;
18
  --text-dim: #94a3b8;
19
- --text-muted: #64748b;
20
  --accent: #3b82f6;
 
21
  --critical: #ef4444;
22
  --warning: #f59e0b;
 
23
  --ok: #10b981;
24
  --font-ui: 'Inter', sans-serif;
25
  --font-mono: 'Fira Code', monospace;
@@ -27,274 +29,253 @@
27
 
28
  * { margin: 0; padding: 0; box-sizing: border-box; }
29
 
30
- body {
31
- font-family: var(--font-ui);
32
- background: var(--bg-base);
33
- color: var(--text-main);
34
- display: flex;
35
- height: 100vh;
36
- overflow: hidden;
37
- }
38
-
39
- /* ─── Sidebar ─── */
40
- .sidebar {
41
- width: 280px;
42
- background: var(--bg-panel);
43
- border-right: 1px solid var(--border);
44
- display: flex;
45
- flex-direction: column;
46
- z-index: 10;
47
- }
48
-
49
- .brand {
50
- padding: 24px;
51
- border-bottom: 1px solid var(--border);
52
- display: flex;
53
- align-items: center;
54
- gap: 12px;
55
- }
56
-
57
- .brand-icon {
58
- width: 36px; height: 36px;
59
- background: linear-gradient(135deg, var(--accent), #8b5cf6);
60
- border-radius: 8px;
61
- display: flex; align-items: center; justify-content: center;
62
- font-weight: 800; font-size: 18px;
63
- box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
64
- }
65
-
66
- .sys-info {
67
- padding: 24px;
68
- flex: 1;
69
- }
70
-
71
- .sys-item {
72
- margin-bottom: 20px;
73
- }
74
-
75
- .sys-label {
76
- font-size: 11px;
77
- text-transform: uppercase;
78
- letter-spacing: 1px;
79
- color: var(--text-muted);
80
- margin-bottom: 6px;
81
- }
82
-
83
- .sys-val {
84
- font-size: 14px;
85
- font-weight: 600;
86
- font-family: var(--font-mono);
87
- }
88
-
89
- /* ─── Main Content ─── */
90
- .main {
91
- flex: 1;
92
- display: flex;
93
- flex-direction: column;
94
- overflow-y: auto;
95
- position: relative;
96
- }
97
-
98
- .top-bar {
99
- padding: 16px 32px;
100
- background: rgba(23, 26, 35, 0.8);
101
- backdrop-filter: blur(10px);
102
- border-bottom: 1px solid var(--border);
103
- display: flex;
104
- justify-content: space-between;
105
- align-items: center;
106
- position: sticky;
107
- top: 0;
108
- z-index: 20;
109
- }
110
-
111
- .badge-status {
112
- padding: 6px 12px;
113
- border-radius: 20px;
114
- font-size: 12px; font-weight: 600;
115
- display: flex; align-items: center; gap: 8px;
116
- }
117
-
118
- .badge-status.healthy { background: rgba(16, 185, 129, 0.1); color: var(--ok); border: 1px solid rgba(16, 185, 129, 0.3); }
119
- .badge-status.degraded { background: rgba(245, 158, 11, 0.1); color: var(--warning); border: 1px solid rgba(245, 158, 11, 0.3); }
120
-
121
- .dashboard-content {
122
- padding: 32px;
123
- max-width: 1600px;
124
- margin: 0 auto;
125
- width: 100%;
126
- }
127
-
128
- .section-title {
129
- font-size: 14px; text-transform: uppercase; letter-spacing: 1px;
130
- color: var(--text-dim); margin: 32px 0 16px; border-bottom: 1px solid var(--border);
131
- padding-bottom: 8px; display: flex; justify-content: space-between;
132
- }
133
-
134
- /* ─── Grids ─── */
135
  .grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }
136
  .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
137
- .grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
138
-
139
- /* ─── Panel ─── */
140
- .panel {
141
- background: var(--bg-panel);
142
- border: 1px solid var(--border);
143
- border-radius: 8px;
144
- padding: 20px;
145
- transition: border-color 0.2s;
146
- }
147
- .panel:hover { border-color: #3f475e; }
148
 
149
- .kpi-val { font-size: 32px; font-weight: 700; margin: 8px 0 4px; font-family: var(--font-mono); }
150
- .kpi-lbl { font-size: 12px; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.5px; }
151
- .kpi-sub { font-size: 12px; color: var(--text-muted); }
152
 
153
  /* ─── Tables ─── */
154
- table { width: 100%; border-collapse: collapse; font-size: 13px; }
155
- th { text-align: left; padding: 12px; border-bottom: 1px solid var(--border); color: var(--text-dim); font-weight: 600; text-transform: uppercase; font-size: 11px; letter-spacing: 0.5px; }
156
- td { padding: 12px; border-bottom: 1px solid var(--border); font-family: var(--font-mono); }
157
  tr:last-child td { border-bottom: none; }
158
  tr:hover td { background: var(--bg-hover); }
159
 
160
- /* ─── Elements ─── */
161
- .status-dot {
162
- display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px;
163
- }
164
- .bg-ok { background: var(--ok); box-shadow: 0 0 8px rgba(16, 185, 129, 0.4); }
165
- .bg-warn { background: var(--warning); box-shadow: 0 0 8px rgba(245, 158, 11, 0.4); }
166
- .bg-crit { background: var(--critical); box-shadow: 0 0 8px rgba(239, 68, 68, 0.4); }
167
  .text-ok { color: var(--ok); } .text-warn { color: var(--warning); } .text-crit { color: var(--critical); }
168
-
169
- .progress { height: 4px; background: var(--border); border-radius: 2px; overflow: hidden; margin-top: 8px; }
170
- .progress-bar { height: 100%; transition: width 0.3s; }
171
-
172
- .chart-wrap { height: 240px; margin-top: 16px; }
173
-
174
- .tag { padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; background: var(--bg-hover); border: 1px solid var(--border); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  </style>
176
  </head>
177
  <body>
178
 
179
- <!-- Sidebar -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  <aside class="sidebar">
181
  <div class="brand">
182
  <div class="brand-icon">A</div>
183
  <div>
184
- <div style="font-weight: 700; font-size: 15px;">Awn Command</div>
185
- <div style="font-size: 12px; color: var(--text-muted);">Admin NOC View</div>
186
  </div>
187
  </div>
188
  <div class="sys-info">
189
  <div class="sys-item">
190
- <div class="sys-label">Uptime</div>
191
  <div class="sys-val" id="sysUptime">β€”</div>
192
  </div>
193
  <div class="sys-item">
194
- <div class="sys-label">Python Env</div>
195
  <div class="sys-val" id="sysPython">β€”</div>
196
  </div>
197
  <div class="sys-item">
198
  <div class="sys-label">Active Chat Sessions</div>
199
- <div class="sys-val" id="sysSessions">β€”</div>
200
  </div>
201
- <div class="sys-item">
202
- <div class="sys-label">Redis State</div>
203
- <div class="sys-val" id="sysRedis">β€”</div>
 
 
 
 
 
 
 
 
 
204
  </div>
205
  </div>
206
  </aside>
207
 
208
- <!-- Main -->
209
  <main class="main">
210
  <header class="top-bar">
211
- <div class="badge-status healthy" id="mainStatus">
212
- <div class="status-dot bg-ok"></div>
213
- <span id="mainStatusText">System Healthy</span>
214
- </div>
215
- <div style="font-size:12px; color:var(--text-dim);">
216
- Last Sync: <span id="syncTime">β€”</span>
217
  </div>
218
  </header>
219
 
220
  <div class="dashboard-content">
221
 
222
- <!-- KPIs -->
223
- <div class="grid-4" style="margin-bottom: 32px;">
 
 
 
 
 
 
 
224
  <div class="panel">
225
  <div class="kpi-lbl">Router Score (Avg)</div>
226
- <div class="kpi-val" id="kpiScore">β€”</div>
227
- <div class="kpi-sub">Out of 100 max</div>
228
  </div>
229
  <div class="panel">
230
  <div class="kpi-lbl">Global Success Rate</div>
231
  <div class="kpi-val" id="kpiSuccess">β€”</div>
232
- <div class="kpi-sub">Sliding window</div>
233
  </div>
234
  <div class="panel">
235
- <div class="kpi-lbl">Provider Availability</div>
236
  <div class="kpi-val" id="kpiProviders">β€”</div>
237
- <div class="kpi-sub">Active endpoints</div>
238
  </div>
239
  <div class="panel">
240
- <div class="kpi-lbl">Redis RTT (p50)</div>
241
- <div class="kpi-val" id="kpiRedisRtt">β€”</div>
242
- <div class="kpi-sub">Database latency</div>
243
  </div>
244
  </div>
245
 
246
- <!-- Detailed Models Table -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  <div class="section-title">
248
- Detailed Model Telemetry
249
- <span style="font-size:11px; font-weight:normal;">Aggregated runtime empirical metrics</span>
250
  </div>
251
  <div class="panel" style="padding:0; overflow-x:auto;">
252
  <table id="modelsTable">
253
  <thead>
254
  <tr>
255
  <th>Model Signature</th>
 
256
  <th>Success %</th>
257
- <th>Avg Latency (ms)</th>
258
- <th>Avg TTFT (ms)</th>
259
- <th>Calls βœ“</th>
260
- <th>Errors βœ—</th>
261
- <th>Last Used</th>
262
  </tr>
263
  </thead>
264
- <tbody>
265
- <!-- Populated by JS -->
266
- </tbody>
267
  </table>
268
  </div>
269
 
270
- <!-- Provider Health & Breakers -->
271
- <div class="section-title">Routing Protocol Status & Protection</div>
272
- <div class="grid-2">
273
- <div class="panel">
274
- <div class="kpi-lbl" style="margin-bottom:16px;">Provider Scoring Matrix</div>
275
- <div id="providerRanks">
276
- <!-- JS -->
277
- </div>
278
- </div>
279
- <div class="panel">
280
- <div class="kpi-lbl" style="margin-bottom:16px;">Circuit Breakers & Bulkhead Limits</div>
281
- <div id="protectionRanks">
282
- <!-- JS -->
283
- </div>
284
- </div>
285
- </div>
286
-
287
  <!-- Charts -->
288
- <div class="section-title">Live Trend Analysis</div>
289
  <div class="grid-2">
290
- <div class="panel">
291
- <div class="kpi-lbl">Latency Profiling (ms)</div>
292
- <div class="chart-wrap"><canvas id="chartLat"></canvas></div>
293
- </div>
294
- <div class="panel">
295
- <div class="kpi-lbl">Success Rate Discrepancy (%)</div>
296
- <div class="chart-wrap"><canvas id="chartSr"></canvas></div>
297
- </div>
298
  </div>
299
 
300
  </div>
@@ -302,210 +283,240 @@
302
 
303
  <script>
304
  const METRICS_URL = '/api/ai/metrics';
 
305
  let chartLat, chartSr;
306
  const trendData = {};
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
 
308
- function formatTime(seconds) {
309
- if (seconds < 60) return `${Math.floor(seconds)}s`;
310
- if (seconds < 3600) return `${Math.floor(seconds/60)}m`;
311
- return `${(seconds/3600).toFixed(1)}h`;
 
 
 
312
  }
313
 
314
- function initCharts() {
315
- const cfg = {
316
- responsive: true, maintainAspectRatio: false,
317
- animation: false,
318
- plugins: { legend: { labels: { color: '#94a3b8', font:{family:'Inter'} } }, tooltip: { mode: 'index' } },
319
- scales: {
320
- x: { display: false },
321
- y: { grid: { color: '#2b3040' }, border:{display:false}, ticks: { color: '#64748b' } }
322
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
  };
 
324
 
325
- chartLat = new Chart(document.getElementById('chartLat'), {
326
- type: 'line', data: { labels: [], datasets: [] }, options: cfg
327
- });
328
 
329
- const cfgSr = JSON.parse(JSON.stringify(cfg));
330
- cfgSr.scales.y.min = 0; cfgSr.scales.y.max = 100;
331
- chartSr = new Chart(document.getElementById('chartSr'), {
332
- type: 'line', data: { labels: [], datasets: [] }, options: cfgSr
333
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
  }
335
 
336
- function updateSys(system, redis) {
337
- if(system) {
338
- document.getElementById('sysUptime').textContent = formatTime(system.uptime_seconds);
339
- document.getElementById('sysPython').textContent = `v${system.python_version}`;
340
- document.getElementById('sysSessions').textContent = system.active_sessions;
 
 
 
 
 
 
 
341
  }
342
  if(redis) {
343
- document.getElementById('sysRedis').innerHTML = redis.connected ?
344
- `<span class="text-ok">Connected</span>` : `<span class="text-crit">Offline</span>`;
345
- document.getElementById('kpiRedisRtt').textContent = redis.last_ping_ms ? `${redis.last_ping_ms}ms` : 'β€”';
346
  }
347
  }
348
 
349
- function updateModels(models) {
350
- const tbody = document.querySelector('#modelsTable tbody');
351
- tbody.innerHTML = '';
352
-
353
- // Sort by total calls descending
354
- const entries = Object.entries(models || {}).sort((a,b) => b[1].total_calls - a[1].total_calls);
355
-
356
- entries.forEach(([name, m]) => {
357
- const srClass = m.success_rate >= 95 ? 'text-ok' : (m.success_rate >= 80 ? 'text-warn' : 'text-crit');
358
- const lastUnix = m.last_used_unix;
359
- const lastUsedStr = lastUnix ? new Date(lastUnix * 1000).toLocaleTimeString() : 'Never';
360
-
361
- tbody.innerHTML += `
362
- <tr>
363
- <td><span class="tag">${name}</span></td>
364
- <td class="${srClass}">${m.success_rate.toFixed(1)}%</td>
365
- <td>${m.avg_latency_ms}</td>
366
- <td>${m.avg_ttft_ms || 'β€”'}</td>
367
- <td class="text-ok">${m.total_calls - m.total_errors}</td>
368
- <td class="text-crit">${m.total_errors}</td>
369
- <td style="color:var(--text-muted)">${lastUsedStr}</td>
370
- </tr>
371
- `;
372
- });
373
- if(entries.length === 0) tbody.innerHTML = `<tr><td colspan="7" style="text-align:center;">No telemetry data yet</td></tr>`;
374
- }
375
 
376
- function updateProviders(providers, breakers) {
377
- let totalScore = 0, totalSr = 0, count = 0, active = 0;
378
- const pGrid = document.getElementById('providerRanks');
379
- const protGrid = document.getElementById('protectionRanks');
380
- pGrid.innerHTML = ''; protGrid.innerHTML = '';
381
-
382
- const timeLabel = new Date().toLocaleTimeString();
383
-
384
- for (const [name, p] of Object.entries(providers)) {
385
- count++;
386
- totalScore += p.score;
387
- // FIX: p.success_rate_window is already a percentage (0-100) from the python backend.
388
- const sr = p.success_rate_window || 0;
389
- totalSr += sr;
390
-
391
  const cb = breakers[name] || {};
392
- if (cb.state === 'closed') active++;
393
-
394
- // Build Trend Data
395
- if(!trendData[name]) trendData[name] = { lat: [], sr: [] };
396
- trendData[name].lat.push(p.latency_p50_ms || 0);
397
- trendData[name].sr.push(sr);
398
- if(trendData[name].lat.length > 30) { trendData[name].lat.shift(); trendData[name].sr.shift(); }
399
-
400
- // UI: Provider Routing Rank
401
- const scClass = p.score >= 120 ? 'text-ok' : (p.score >= 80 ? 'text-warn' : 'text-crit');
402
- pGrid.innerHTML += `
403
- <div style="margin-bottom:16px;">
404
- <div style="display:flex; justify-content:space-between; margin-bottom:4px;">
405
- <span style="font-weight:600">${name}</span>
406
- <span class="${scClass}" style="font-family:var(--font-mono)">Score: ${p.score.toFixed(1)}</span>
407
- </div>
408
- <div style="display:flex; justify-content:space-between; font-size:11px; color:var(--text-dim);">
409
- <span>SR: ${sr.toFixed(1)}% | Latency: ${p.latency_p50_ms}ms</span>
410
- <span>Streak: ${p.consecutive_failures} fails</span>
411
- </div>
412
- <div class="progress"><div class="progress-bar ${scClass.replace('text','bg')}" style="width:${Math.min((p.score/200)*100, 100)}%"></div></div>
413
- </div>
414
- `;
415
 
416
- // UI: Resilience (Breakers + Bulkhead)
417
- const cbClass = cb.state === 'closed' ? 'bg-ok' : (cb.state === 'half_open' ? 'bg-warn' : 'bg-crit');
418
- const bh = p.bulkhead || {};
419
- const bhUsed = bh.configured ? (bh.configured - bh.available) : 0;
 
 
420
 
421
- protGrid.innerHTML += `
422
- <div style="margin-bottom:16px; padding:12px; background:var(--bg-hover); border-radius:6px; border:1px solid var(--border);">
423
- <div style="display:flex; justify-content:space-between; margin-bottom:8px;">
424
- <div style="font-weight:600">${name}</div>
425
- <div class="tag" style="display:flex; align-items:center;">
426
- <div class="status-dot ${cbClass}"></div> ${cb.state || 'closed'}
 
 
 
 
 
 
 
 
427
  </div>
428
- </div>
429
- <div style="display:flex; justify-content:space-between; font-size:12px; color:var(--text-dim); font-family:var(--font-mono);">
430
- <span>Bulkhead: ${bhUsed}/${bh.configured || '?'} slots</span>
431
- <span class="text-warning">${cb.state !== 'closed' ? `Retry in ${cb.seconds_until_retry}s` : 'Active'}</span>
432
- </div>
433
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
434
  `;
 
 
 
 
 
435
  }
436
-
437
- // Update main KPIs
438
- document.getElementById('kpiProviders').textContent = `${active}/${count}`;
439
- document.getElementById('kpiScore').textContent = count ? (totalScore/count).toFixed(0) : 'β€”';
440
- document.getElementById('kpiSuccess').textContent = count ? (totalSr/count).toFixed(1) + '%' : 'β€”';
441
-
442
- // Top banner status
443
- const banner = document.getElementById('mainStatus');
444
- const bTxt = document.getElementById('mainStatusText');
445
- if(active === 0 && count > 0) {
446
- banner.className = 'badge-status degraded';
447
- banner.querySelector('.status-dot').className = 'status-dot bg-crit';
448
- bTxt.textContent = 'ALL PROVIDERS DOWN - FATAL';
449
- bTxt.style.color = 'var(--critical)';
450
- } else if (active < count) {
451
- banner.className = 'badge-status degraded';
452
- banner.querySelector('.status-dot').className = 'status-dot bg-warn';
453
- bTxt.textContent = `DEGRADED (${active}/${count} Online)`;
454
- bTxt.style.color = 'var(--warning)';
455
- } else {
456
- banner.className = 'badge-status healthy';
457
- banner.querySelector('.status-dot').className = 'status-dot bg-ok';
458
- bTxt.textContent = 'System Healthy';
459
- }
460
-
461
- updateCharts(timeLabel);
462
  }
463
 
464
- function updateCharts(timeLabel) {
465
- const names = Object.keys(trendData);
466
- if(names.length === 0) return;
467
-
468
- const maxLen = Math.max(...names.map(n => trendData[n].lat.length));
469
- const labels = Array(maxLen).fill('');
470
- labels[labels.length-1] = timeLabel;
471
-
472
- const colors = ['#3b82f6', '#8b5cf6', '#10b981', '#f59e0b'];
473
-
474
- chartLat.data.labels = labels;
475
- chartLat.data.datasets = names.map((n, i) => ({
476
- label: n, data: trendData[n].lat,
477
- borderColor: colors[i%4], borderWidth: 2, pointRadius: 0, tension: 0.3
478
- }));
479
- chartLat.update();
480
-
481
- chartSr.data.labels = labels;
482
- chartSr.data.datasets = names.map((n, i) => ({
483
- label: n, data: trendData[n].sr,
484
- borderColor: colors[i%4], borderWidth: 2, pointRadius: 0, tension: 0.3
485
- }));
486
- chartSr.update();
 
 
 
 
 
 
 
 
 
 
 
487
  }
488
 
 
489
  async function fetchLoop() {
 
 
490
  try {
491
  const res = await fetch(METRICS_URL);
492
  const data = await res.json();
493
-
494
  document.getElementById('syncTime').textContent = new Date().toLocaleTimeString();
495
-
496
- updateSys(data.system, data.redis);
497
- updateModels(data.models);
498
- updateProviders(data.providers, data.breakers);
499
- } catch (err) {
500
- console.error("Dashboard sync failed", err);
501
- }
502
- setTimeout(fetchLoop, 5000);
503
  }
504
 
505
- document.addEventListener('DOMContentLoaded', () => {
506
- initCharts();
507
- fetchLoop();
508
- });
509
  </script>
510
  </body>
511
  </html>
 
10
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
11
  <style>
12
  :root {
13
+ --bg-base: #0b0d14;
14
+ --bg-panel: #131620;
15
+ --bg-hover: #1c202f;
16
+ --border: #242938;
17
  --text-main: #f8fafc;
18
  --text-dim: #94a3b8;
19
+ --text-muted: #475569;
20
  --accent: #3b82f6;
21
+ --accent-hover: #2563eb;
22
  --critical: #ef4444;
23
  --warning: #f59e0b;
24
+ --warning-hover: #d97706;
25
  --ok: #10b981;
26
  --font-ui: 'Inter', sans-serif;
27
  --font-mono: 'Fira Code', monospace;
 
29
 
30
  * { margin: 0; padding: 0; box-sizing: border-box; }
31
 
32
+ body { font-family: var(--font-ui); background: var(--bg-base); color: var(--text-main); display: flex; height: 100vh; overflow: hidden; }
33
+
34
+ /* ─── Layout ─── */
35
+ .sidebar { width: 300px; background: var(--bg-panel); border-right: 1px solid var(--border); display: flex; flex-direction: column; z-index: 10; box-shadow: 4px 0 24px rgba(0,0,0,0.2); }
36
+ .main { flex: 1; display: flex; flex-direction: column; overflow-y: auto; position: relative; }
37
+
38
+ /* ─── Typography & Utilities ─── */
39
+ .h-title { font-weight: 700; font-size: 16px; letter-spacing: -0.3px; }
40
+ .section-title { font-size: 15px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-dim); margin: 32px 0 16px; border-bottom: 1px solid var(--border); padding-bottom: 12px; display: flex; justify-content: space-between; align-items:flex-end; }
41
+ .mono { font-family: var(--font-mono); }
42
+ .flex { display: flex; } .items-center { align-items: center; } .gap-2 { gap: 8px; } .justify-between { justify-content: space-between; }
43
+
44
+ /* ─── Brand ─── */
45
+ .brand { padding: 24px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 14px; }
46
+ .brand-icon { width: 40px; height: 40px; background: linear-gradient(135deg, var(--critical), #8b5cf6); border-radius: 8px; display: flex; align-items: center; justify-content: center; font-weight: 800; font-size: 18px; box-shadow: 0 4px 16px rgba(239, 68, 68, 0.4); text-shadow: 0 2px 4px rgba(0,0,0,0.3); }
47
+
48
+ /* ─── Sidebar Items ─── */
49
+ .sys-info { padding: 24px; flex: 1; overflow-y: auto; }
50
+ .sys-item { margin-bottom: 24px; background: rgba(0,0,0,0.2); padding: 12px; border-radius: 8px; border: 1px solid var(--border); }
51
+ .sys-label { font-size: 11px; text-transform: uppercase; letter-spacing: 1px; color: var(--text-muted); margin-bottom: 8px; font-weight: 600;}
52
+ .sys-val { font-size: 15px; font-weight: 600; font-family: var(--font-mono); color: var(--accent); }
53
+
54
+ /* ─── Buttons ─── */
55
+ .btn { background: var(--bg-hover); color: var(--text-main); border: 1px solid var(--border); padding: 8px 14px; border-radius: 6px; font-size: 12px; font-weight: 600; cursor: pointer; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); display: inline-flex; align-items: center; gap: 6px; letter-spacing: 0.3px; }
56
+ .btn:hover { background: #2a3040; border-color: #3f475e; transform: translateY(-1px); }
57
+ .btn:active { transform: translateY(1px); }
58
+ .btn-crit { background: rgba(239,68,68,0.1); border-color: rgba(239,68,68,0.3); color: var(--critical); }
59
+ .btn-crit:hover { background: rgba(239,68,68,0.2); border-color: var(--critical); }
60
+ .btn-ok { background: rgba(16,185,129,0.1); border-color: rgba(16,185,129,0.3); color: var(--ok); }
61
+ .btn-ok:hover { background: rgba(16,185,129,0.2); border-color: var(--ok); }
62
+ .btn-warn { background: rgba(245,158,11,0.1); border-color: rgba(245,158,11,0.3); color: var(--warning); }
63
+ .btn-warn:hover { background: rgba(245,158,11,0.2); border-color: var(--warning); }
64
+
65
+ /* ─── Dashboard ─── */
66
+ .top-bar { padding: 16px 32px; background: rgba(19, 22, 32, 0.85); backdrop-filter: blur(12px); border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; position: sticky; top: 0; z-index: 20; }
67
+ .dashboard-content { padding: 32px; max-width: 1600px; margin: 0 auto; width: 100%; }
68
+
69
+ /* ─── Grids & Panels ─── */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  .grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }
71
  .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
72
+ .panel { background: var(--bg-panel); border: 1px solid var(--border); border-radius: 10px; padding: 24px; transition: border-color 0.2s, box-shadow 0.2s; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); }
73
+ .panel:hover { border-color: #3f475e; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); }
 
 
 
 
 
 
 
 
 
74
 
75
+ .kpi-val { font-size: 36px; font-weight: 700; margin: 12px 0 6px; font-family: var(--font-mono); letter-spacing: -1px; }
76
+ .kpi-lbl { font-size: 13px; color: var(--text-dim); font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }
77
+ .kpi-sub { font-size: 12px; color: var(--text-muted); font-weight: 500; }
78
 
79
  /* ─── Tables ─── */
80
+ table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 13px; }
81
+ th { text-align: left; padding: 16px; border-bottom: 1px solid var(--border); color: var(--text-dim); font-weight: 600; text-transform: uppercase; font-size: 11px; letter-spacing: 0.5px; background: rgba(0,0,0,0.2); }
82
+ td { padding: 16px; border-bottom: 1px solid var(--border); vertical-align: middle; }
83
  tr:last-child td { border-bottom: none; }
84
  tr:hover td { background: var(--bg-hover); }
85
 
86
+ /* ─── Utilities ─── */
87
+ .status-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; }
88
+ .bg-ok { background: var(--ok); box-shadow: 0 0 8px rgba(16, 185, 129, 0.6); }
89
+ .bg-warn { background: var(--warning); box-shadow: 0 0 8px rgba(245, 158, 11, 0.6); }
90
+ .bg-crit { background: var(--critical); box-shadow: 0 0 8px rgba(239, 68, 68, 0.6); }
 
 
91
  .text-ok { color: var(--ok); } .text-warn { color: var(--warning); } .text-crit { color: var(--critical); }
92
+
93
+ .progress { height: 6px; background: rgba(0,0,0,0.4); border-radius: 3px; overflow: hidden; margin-top: 8px; border: 1px solid var(--border);}
94
+ .progress-bar { height: 100%; transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1); }
95
+ .chart-wrap { height: 260px; margin-top: 16px; }
96
+ .tag { padding: 4px 10px; border-radius: 6px; font-size: 11px; font-weight: 600; background: var(--bg-base); border: 1px solid var(--border); font-family: var(--font-mono); }
97
+
98
+ /* ─── Modals & Overlays ─── */
99
+ .overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(11, 13, 20, 0.85); backdrop-filter: blur(8px); display: none; align-items: center; justify-content: center; z-index: 9999; opacity: 0; transition: opacity 0.2s; }
100
+ .overlay.active { display: flex; opacity: 1; }
101
+
102
+ .modal { background: var(--bg-panel); border: 1px solid var(--border); padding: 32px; border-radius: 12px; width: 420px; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5); transform: translateY(20px); transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); }
103
+ .overlay.active .modal { transform: translateY(0); }
104
+
105
+ .modal-title { font-size: 18px; font-weight: 700; margin-bottom: 8px; display: flex; align-items: center; gap: 8px; }
106
+ .modal-desc { font-size: 13px; color: var(--text-dim); margin-bottom: 24px; line-height: 1.5; }
107
+
108
+ .form-group { margin-bottom: 16px; text-align: left; }
109
+ .form-label { display: block; font-size: 12px; font-weight: 600; color: var(--text-dim); margin-bottom: 6px; }
110
+ .form-input { width: 100%; padding: 12px; background: rgba(0,0,0,0.2); border: 1px solid var(--border); color: white; border-radius: 6px; font-family: var(--font-mono); font-size: 14px; outline: none; transition: border-color 0.2s; }
111
+ .form-input:focus { border-color: var(--accent); box-shadow: 0 0 0 2px rgba(59,130,246,0.2); }
112
+
113
+ .modal-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 32px; }
114
+
115
+ /* Toast system */
116
+ #toastContainer { position: fixed; bottom: 32px; right: 32px; z-index: 10000; display: flex; flex-direction: column; gap: 12px; }
117
+ .toast { background: var(--bg-panel); border: 1px solid var(--border); color: white; padding: 14px 20px; border-radius: 8px; font-size: 13px; font-weight: 500; box-shadow: 0 10px 25px rgba(0,0,0,0.5); animation: toastIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); display: flex; align-items: center; gap: 12px; min-width: 300px; }
118
+ @keyframes toastIn { from { transform: translateX(120%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
119
+
120
+ /* Danger Banner */
121
+ .danger-banner { background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.4); padding: 16px 24px; border-radius: 10px; margin-bottom: 32px; display: none; align-items: center; gap: 16px; color: var(--critical); font-weight: 700; font-size: 15px; animation: pulse-danger 2s infinite; letter-spacing: 0.5px; }
122
+ @keyframes pulse-danger { 0%,100% { box-shadow: 0 0 0px 0px rgba(239, 68, 68, 0.1); } 50% { box-shadow: 0 0 20px 8px rgba(239, 68, 68, 0.15); } }
123
+
124
+ /* Admin Controls specific */
125
+ .controls-wrapper { display: flex; flex-direction: column; gap: 8px; }
126
+ .controls-row { display: flex; gap: 8px; flex-wrap: wrap; }
127
  </style>
128
  </head>
129
  <body>
130
 
131
+ <!-- Auth Gate -->
132
+ <div id="authOverlay" class="overlay active" style="background:#0b0d14;">
133
+ <div class="modal" style="text-align: center;">
134
+ <div class="brand-icon" style="margin: 0 auto 20px;">πŸ”</div>
135
+ <h2 class="modal-title" style="justify-content: center;">Restricted Access</h2>
136
+ <p class="modal-desc">Enter your X-API-Key to unlock the Command Center.</p>
137
+ <input type="password" id="apiKeyIn" class="form-input" style="text-align:center; margin-bottom:24px;" placeholder="β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’β€’" onkeypress="if(event.key==='Enter') saveKey()">
138
+ <button class="btn btn-ok" style="width:100%; justify-content:center; padding:12px; font-size:14px;" onclick="saveKey()">Unlock Console</button>
139
+ </div>
140
+ </div>
141
+
142
+ <!-- Generic Modal for Tuning -->
143
+ <div id="tuneModal" class="overlay">
144
+ <div class="modal">
145
+ <h2 class="modal-title" id="tuneTitle">Tune Parameter</h2>
146
+ <p class="modal-desc" id="tuneDesc">Modify runtime constraints.</p>
147
+ <div id="tuneFields"></div>
148
+ <div class="modal-actions">
149
+ <button class="btn" onclick="closeModal()">Cancel</button>
150
+ <button class="btn btn-ok" id="tuneConfirm">Apply Changes</button>
151
+ </div>
152
+ </div>
153
+ </div>
154
+
155
+ <div id="toastContainer"></div>
156
+
157
  <aside class="sidebar">
158
  <div class="brand">
159
  <div class="brand-icon">A</div>
160
  <div>
161
+ <div style="font-weight: 800; font-size: 16px; letter-spacing: -0.5px;">Awn Command</div>
162
+ <div style="font-size: 12px; color: var(--accent); font-weight: 600; letter-spacing: 0.5px;">NOC CORE v2.0</div>
163
  </div>
164
  </div>
165
  <div class="sys-info">
166
  <div class="sys-item">
167
+ <div class="sys-label">System Uptime</div>
168
  <div class="sys-val" id="sysUptime">β€”</div>
169
  </div>
170
  <div class="sys-item">
171
+ <div class="sys-label">Runtime Env</div>
172
  <div class="sys-val" id="sysPython">β€”</div>
173
  </div>
174
  <div class="sys-item">
175
  <div class="sys-label">Active Chat Sessions</div>
176
+ <div class="sys-val" id="sysSessions" style="color:var(--text-main)">β€”</div>
177
  </div>
178
+
179
+ <div style="margin-top: 48px; border-top: 1px dashed var(--border); padding-top: 32px;">
180
+ <div class="sys-label" style="color:var(--critical); display:flex; align-items:center; gap:6px;">⚠️ Danger Zone</div>
181
+ <div style="display:flex; flex-direction:column; gap:16px; margin-top:20px;">
182
+ <button class="btn btn-crit" style="padding:12px; justify-content:center;" onclick="adminCmd('POST', '/api/ai/admin/system/killswitch', {active:!globalKillSwitch})">
183
+ <span id="killBtnLbl">⚑ Trigger Kill Switch</span>
184
+ </button>
185
+ <button class="btn" style="padding:12px; justify-content:center; color:var(--text-dim);" onclick="if(confirm('Warning: This destroys all telemetry histories. Proceed?')) adminCmd('POST', '/api/ai/admin/system/flush-cache')">
186
+ πŸ—‘ Nuke Redis Cache
187
+ </button>
188
+ <button class="btn" onclick="logout()" style="justify-content:center; margin-top:32px; background:transparent; border-color:transparent; color:var(--text-dim);">πŸ”’ Lock Console</button>
189
+ </div>
190
  </div>
191
  </div>
192
  </aside>
193
 
 
194
  <main class="main">
195
  <header class="top-bar">
196
+ <div class="h-title">Global LLM Routing Engine</div>
197
+ <div style="font-size:12px; color:var(--text-dim); display:flex; gap:20px; align-items:center; font-weight:500;">
198
+ <span style="display:flex; align-items:center; gap:6px;"><div class="status-dot bg-ok"></div> Secure</span>
199
+ <span style="background:rgba(0,0,0,0.3); padding:4px 10px; border-radius:12px; border:1px solid var(--border);">Last Sync: <span id="syncTime" class="mono">Never</span></span>
 
 
200
  </div>
201
  </header>
202
 
203
  <div class="dashboard-content">
204
 
205
+ <div class="danger-banner" id="killSwitchBanner">
206
+ <div style="font-size:24px;">🚫</div>
207
+ <div>
208
+ <div style="font-size:18px; margin-bottom:4px;">SYSTEM HALTED</div>
209
+ <div style="font-size:13px; font-weight:500; opacity:0.9;">Global Kill Switch is active. All inbound LLM routing is returning 503 artificially.</div>
210
+ </div>
211
+ </div>
212
+
213
+ <div class="grid-4" style="margin-bottom: 40px;">
214
  <div class="panel">
215
  <div class="kpi-lbl">Router Score (Avg)</div>
216
+ <div class="kpi-val text-ok" id="kpiScore">β€”</div>
217
+ <div class="kpi-sub">Out of 100 maximum</div>
218
  </div>
219
  <div class="panel">
220
  <div class="kpi-lbl">Global Success Rate</div>
221
  <div class="kpi-val" id="kpiSuccess">β€”</div>
222
+ <div class="kpi-sub">Sliding memory window</div>
223
  </div>
224
  <div class="panel">
225
+ <div class="kpi-lbl">Infrastructure Health</div>
226
  <div class="kpi-val" id="kpiProviders">β€”</div>
227
+ <div class="kpi-sub">Active nodes</div>
228
  </div>
229
  <div class="panel">
230
+ <div class="kpi-lbl">Redis Cache RTT</div>
231
+ <div class="kpi-val mono text-accent" style="color:var(--accent)" id="kpiRedisRtt">β€”</div>
232
+ <div class="kpi-sub" id="kpiRedisSub">Disconnected</div>
233
  </div>
234
  </div>
235
 
236
+ <!-- Provider Table with Full Actions -->
237
+ <div class="section-title">
238
+ <span>Provider Matrix & Routing Constraints</span>
239
+ </div>
240
+ <div class="panel" style="padding:0; overflow-x:auto;">
241
+ <table id="providersTable">
242
+ <thead>
243
+ <tr>
244
+ <th>Integration Node</th>
245
+ <th>Status & Breaker</th>
246
+ <th>Score / SR</th>
247
+ <th>Concurrency (Bulkhead)</th>
248
+ <th style="min-width: 320px;">Administrative Restraints</th>
249
+ </tr>
250
+ </thead>
251
+ <tbody><!-- JS --></tbody>
252
+ </table>
253
+ </div>
254
+
255
+ <!-- Model Banlist Table -->
256
  <div class="section-title">
257
+ <span>Micro-Model Precision Control</span>
 
258
  </div>
259
  <div class="panel" style="padding:0; overflow-x:auto;">
260
  <table id="modelsTable">
261
  <thead>
262
  <tr>
263
  <th>Model Signature</th>
264
+ <th>Traffic Volume</th>
265
  <th>Success %</th>
266
+ <th>P50 Lat/TTFT</th>
267
+ <th>Routing Rule</th>
 
 
 
268
  </tr>
269
  </thead>
270
+ <tbody><!-- JS --></tbody>
 
 
271
  </table>
272
  </div>
273
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
  <!-- Charts -->
275
+ <div class="section-title" style="margin-top:40px;">Real-time Telemetry Profiling</div>
276
  <div class="grid-2">
277
+ <div class="panel"><div class="kpi-lbl">Latency Dist. (ms)</div><div class="chart-wrap"><canvas id="chartLat"></canvas></div></div>
278
+ <div class="panel"><div class="kpi-lbl">Success Rate Discrepancy (%)</div><div class="chart-wrap"><canvas id="chartSr"></canvas></div></div>
 
 
 
 
 
 
279
  </div>
280
 
281
  </div>
 
283
 
284
  <script>
285
  const METRICS_URL = '/api/ai/metrics';
286
+ let API_KEY = localStorage.getItem('awn_api_key');
287
  let chartLat, chartSr;
288
  const trendData = {};
289
+ let globalKillSwitch = false;
290
+ let globalBannedModels = [];
291
+
292
+ // Auth
293
+ if(API_KEY) { document.getElementById('authOverlay').classList.remove('active'); setTimeout(() => document.getElementById('authOverlay').style.display='none', 200); }
294
+ function saveKey() {
295
+ const v = document.getElementById('apiKeyIn').value.trim();
296
+ if(v) { API_KEY = v; localStorage.setItem('awn_api_key', v); document.getElementById('authOverlay').classList.remove('active'); setTimeout(() => document.getElementById('authOverlay').style.display='none', 200); fetchLoop(); }
297
+ }
298
+ function logout() {
299
+ localStorage.removeItem('awn_api_key'); API_KEY = null;
300
+ document.getElementById('authOverlay').style.display='flex';
301
+ setTimeout(() => document.getElementById('authOverlay').classList.add('active'), 10);
302
+ }
303
 
304
+ // Toasts
305
+ function showToast(msg, type='ok') {
306
+ const c = document.getElementById('toastContainer');
307
+ const el = document.createElement('div'); el.className = 'toast';
308
+ el.innerHTML = `<div class="status-dot ${type==='ok'?'bg-ok':'bg-crit'}" style="box-shadow:none;transform:scale(1.2)"></div> ${msg}`;
309
+ c.appendChild(el);
310
+ setTimeout(() => { el.style.opacity = '0'; el.style.transform = 'translateX(100%)'; setTimeout(()=>el.remove(), 300); }, 4000);
311
  }
312
 
313
+ // Execution
314
+ async function adminCmd(method, path, payload=null) {
315
+ try {
316
+ const res = await fetch(path, { method: method, headers: { 'X-API-Key': API_KEY, 'Content-Type': 'application/json' }, body: payload ? JSON.stringify(payload) : null });
317
+ const data = await res.json();
318
+ if(res.ok) { showToast(`Command executed successfully`, 'ok'); fetchLoop(); closeModal(); }
319
+ else { showToast(`Failed: ${data.detail || res.statusText}`, 'crit'); if(res.status === 401) logout(); }
320
+ } catch(e) { showToast(`Network Hook Error: ${e.message}`, 'crit'); }
321
+ }
322
+
323
+ // Modal Logic
324
+ const modal = document.getElementById('tuneModal');
325
+ let currentModalAction = null;
326
+
327
+ function closeModal() { modal.classList.remove('active'); setTimeout(()=>modal.style.display='none', 200); }
328
+ function openTuneBulkhead(name, currentLimit) {
329
+ document.getElementById('tuneTitle').innerHTML = `βš™οΈ Tune Bulkhead: <span class="tag">${name}</span>`;
330
+ document.getElementById('tuneDesc').textContent = 'Adjust the concurrency semaphore limit instantly.';
331
+ document.getElementById('tuneFields').innerHTML = `<div class="form-group"><label class="form-label">Concurrency Limit</label><input type="number" id="inpBulk" class="form-input" value="${currentLimit}" min="1"></div>`;
332
+ modal.style.display = 'flex'; setTimeout(()=>modal.classList.add('active'), 10);
333
+
334
+ document.getElementById('tuneConfirm').onclick = () => {
335
+ const v = parseInt(document.getElementById('inpBulk').value);
336
+ if(v>0) adminCmd('POST', `/api/ai/admin/provider/${name}/bulkhead`, {limit:v});
337
+ };
338
+ }
339
+ function openTuneCircuit(name, currentFail, currentTimeout) {
340
+ document.getElementById('tuneTitle').innerHTML = `πŸ›‘οΈ Tune Circuit: <span class="tag">${name}</span>`;
341
+ document.getElementById('tuneDesc').textContent = 'Modify dynamic fault tolerance thresholds.';
342
+ document.getElementById('tuneFields').innerHTML = `
343
+ <div class="form-group"><label class="form-label">Failure Threshold (Strikes)</label><input type="number" id="inpFail" class="form-input" value="${currentFail}" min="1"></div>
344
+ <div class="form-group"><label class="form-label">Base Recovery Timeout (Seconds)</label><input type="number" id="inpTimeout" class="form-input" value="${currentTimeout}" min="1"></div>
345
+ `;
346
+ modal.style.display = 'flex'; setTimeout(()=>modal.classList.add('active'), 10);
347
+
348
+ document.getElementById('tuneConfirm').onclick = () => {
349
+ const tf = parseInt(document.getElementById('inpFail').value);
350
+ const rt = parseInt(document.getElementById('inpTimeout').value);
351
+ if(tf>0 && rt>0) adminCmd('POST', `/api/ai/admin/circuit/${name}/tune`, {failure_threshold: tf, recovery_timeout: rt});
352
  };
353
+ }
354
 
 
 
 
355
 
356
+ // Formatting
357
+ function formatTime(s) {
358
+ if (s < 60) return `${Math.floor(s)}s`;
359
+ if (s < 3600) return `${Math.floor(s/60)}m`;
360
+ if (s < 86400) return `${(s/3600).toFixed(1)}h`;
361
+ return `${(s/86400).toFixed(1)}d`;
362
+ }
363
+
364
+ // Charts
365
+ const cols = ['#3b82f6', '#8b5cf6', '#10b981', '#f59e0b', '#ec4899'];
366
+ function initCharts() {
367
+ const cfg = { responsive: true, maintainAspectRatio: false, animation: false, plugins: { legend: { labels: { color: '#94a3b8', font:{family:'Inter'} } }, tooltip: { mode: 'index', backgroundColor:'rgba(19,22,32,0.9)', titleColor:'#fff', bodyFont:{family:'Fira Code'}, borderColor:'#242938', borderWidth:1 } }, scales: { x: { display: false }, y: { grid: { color: '#242938' }, border:{display:false}, ticks: { color: '#475569', font:{family:'Fira Code'} } } } };
368
+ chartLat = new Chart(document.getElementById('chartLat'), { type: 'line', data: { labels: [], datasets: [] }, options: cfg });
369
+ const cfgSr = JSON.parse(JSON.stringify(cfg)); cfgSr.scales.y.min = 0; cfgSr.scales.y.max = 100;
370
+ chartSr = new Chart(document.getElementById('chartSr'), { type: 'line', data: { labels: [], datasets: [] }, options: cfgSr });
371
+ }
372
+ function drawCharts() {
373
+ const names = Object.keys(trendData); if(!names.length) return;
374
+ const lbl = new Date().toLocaleTimeString();
375
+ const maxL = Math.max(...names.map(n=>trendData[n].lat.length));
376
+ const x = Array(maxL).fill(''); x[x.length-1]=lbl;
377
+ chartLat.data.labels = x; chartSr.data.labels = x;
378
+ chartLat.data.datasets = names.map((n,i)=>({ label:n, data:trendData[n].lat, borderColor:cols[i%cols.length], backgroundColor:cols[i%cols.length]+'22', fill:true, borderWidth:2, pointRadius:0, tension:0.4 }));
379
+ chartSr.data.datasets = names.map((n,i)=>({ label:n, data:trendData[n].sr, borderColor:cols[i%cols.length], backgroundColor:cols[i%cols.length]+'22', fill:true, borderWidth:2, pointRadius:0, tension:0.4 }));
380
+ chartLat.update(); chartSr.update();
381
  }
382
 
383
+ // Render Functions
384
+ function renderSys(sys, redis) {
385
+ if(sys) {
386
+ document.getElementById('sysUptime').textContent = formatTime(sys.uptime_seconds);
387
+ document.getElementById('sysPython').textContent = `v${sys.python_version}`;
388
+ document.getElementById('sysSessions').textContent = sys.active_sessions;
389
+
390
+ globalKillSwitch = sys.kill_switch_active;
391
+ globalBannedModels = sys.banned_models || [];
392
+
393
+ document.getElementById('killSwitchBanner').style.display = globalKillSwitch ? 'flex' : 'none';
394
+ document.getElementById('killBtnLbl').textContent = globalKillSwitch ? '🟒 Heal System (Deactivate Kill)' : '⚑ Trigger Kill Switch';
395
  }
396
  if(redis) {
397
+ document.getElementById('kpiRedisSub').textContent = redis.connected ? 'OK / Connected' : 'Down';
398
+ document.getElementById('kpiRedisSub').className = redis.connected ? 'kpi-sub text-ok' : 'kpi-sub text-crit';
399
+ document.getElementById('kpiRedisRtt').textContent = redis.connected && redis.last_ping_ms ? `${redis.last_ping_ms}ms` : 'β€”';
400
  }
401
  }
402
 
403
+ function renderProviders(providers, breakers) {
404
+ const tbody = document.querySelector('#providersTable tbody');
405
+ tbody.innerHTML = ''; let tScore=0, tSr=0, act=0, cnt=0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
406
 
407
+ for(const [name, p] of Object.entries(providers)) {
408
+ cnt++; tScore+=p.score; tSr+=(p.success_rate_window||0);
 
 
 
 
 
 
 
 
 
 
 
 
 
409
  const cb = breakers[name] || {};
410
+ const disabled = p.circuit_state === 'disabled' || (cb.state === 'open' && p.score < 0);
411
+ if(cb.state === 'closed' && !disabled) act++;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
412
 
413
+ const stateUi = disabled ? `<span class="status-dot bg-crit"></span> <span class="text-crit" style="font-weight:600">DISABLED</span>` :
414
+ (cb.state==='closed' ? `<span class="status-dot bg-ok"></span> <span class="text-ok">CLOSED (Live)</span>` : `<span class="status-dot bg-warn"></span> <span class="text-warn" style="font-weight:600">OPEN/TRIP (${cb.seconds_until_retry||0}s)</span>`);
415
+
416
+ const bh = p.bulkhead||{};
417
+ const used = bh.configured ? bh.configured - bh.available : 0;
418
+ const perc = bh.configured ? Math.min((used/bh.configured)*100, 100) : 0;
419
 
420
+ tbody.innerHTML += `
421
+ <tr style="background: ${disabled ? 'rgba(0,0,0,0.4)': 'transparent'}; opacity: ${disabled ? 0.7 : 1}">
422
+ <td><div style="font-weight:700; font-size:14px; margin-bottom:4px;">${name}</div> <div class="tag">${cb.failure_threshold||3} thresh / ${cb.recovery_timeout_s||60}s t/o</div></td>
423
+ <td>
424
+ <div style="margin-bottom:6px;">${stateUi}</div>
425
+ <div style="font-size:11px; color:var(--text-muted); font-family:var(--font-mono);">Streak: ${p.consecutive_failures}x fails</div>
426
+ </td>
427
+ <td class="mono">
428
+ <div style="margin-bottom:4px; font-weight:600" class="${p.score>=100?'text-ok':'text-warn'}">Sc: ${p.score.toFixed(1)}</div>
429
+ <div style="font-size:12px;" class="${p.success_rate_window>=90?'text-ok':'text-crit'}">SR: ${(p.success_rate_window||0).toFixed(1)}%</div>
430
+ </td>
431
+ <td>
432
+ <div class="flex justify-between" style="font-size:11px; margin-bottom:4px; font-weight:600; font-family:var(--font-mono);">
433
+ <span>${used} Active</span> <span style="color:var(--text-dim)">Cap: ${bh.configured||'?'}</span>
434
  </div>
435
+ <div class="progress"><div class="progress-bar ${perc>80?'bg-crit':'bg-accent'}" style="width:${perc}%"></div></div>
436
+ </td>
437
+ <td>
438
+ <div class="controls-wrapper">
439
+ <div class="controls-row">
440
+ ${disabled
441
+ ? `<button class="btn btn-ok" onclick="adminCmd('POST','/api/ai/admin/provider/${name}/enable')">βœ“ Enable</button>`
442
+ : `<button class="btn btn-crit" onclick="adminCmd('POST','/api/ai/admin/provider/${name}/disable')">β›” Power Off</button>`}
443
+ <button class="btn btn-warn" onclick="adminCmd('POST','/api/ai/admin/circuit/${name}/trip')">πŸ”ͺ Trip</button>
444
+ <button class="btn btn-ok" style="background:transparent;" onclick="adminCmd('POST','/api/ai/admin/circuit/${name}/reset')">↻ Reset</button>
445
+ </div>
446
+ <div class="controls-row">
447
+ <button class="btn" style="background:transparent; color:var(--text-dim)" onclick="openTuneCircuit('${name}', ${cb.failure_threshold||3}, ${cb.recovery_base_s||60})">βš™οΈ Tune Circuit</button>
448
+ <button class="btn" style="background:transparent; color:var(--text-dim)" onclick="openTuneBulkhead('${name}', ${bh.configured||16})">πŸ“Š Adjust Slots</button>
449
+ </div>
450
+ </div>
451
+ </td>
452
+ </tr>
453
  `;
454
+
455
+ // charts prep
456
+ if(!trendData[name]) trendData[name]={lat:[], sr:[]};
457
+ trendData[name].lat.push(p.latency_p50_ms||0); trendData[name].sr.push(p.success_rate_window||0);
458
+ if(trendData[name].lat.length>40) { trendData[name].lat.shift(); trendData[name].sr.shift(); }
459
  }
460
+
461
+ document.getElementById('kpiProviders').textContent = `${act}/${cnt}`;
462
+ document.getElementById('kpiScore').className = (cnt?(tScore/cnt):0) >= 100 ? 'kpi-val text-ok' : 'kpi-val text-warn';
463
+ document.getElementById('kpiScore').textContent = cnt ? (tScore/cnt).toFixed(0) : 'β€”';
464
+ document.getElementById('kpiSuccess').textContent = cnt ? +(tSr/cnt).toFixed(1) + '%' : 'β€”';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
465
  }
466
 
467
+ function renderModels(models) {
468
+ const tbody = document.querySelector('#modelsTable tbody');
469
+ tbody.innerHTML = '';
470
+ const entries = Object.entries(models||{}).sort((a,b)=>b[1].total_calls - a[1].total_calls);
471
+
472
+ entries.forEach(([name, m]) => {
473
+ const isBanned = globalBannedModels.includes(name);
474
+ const s = m.success_rate;
475
+ const srClass = s>=95 ? 'text-ok' : (s>=70 ? 'text-warn' : 'text-crit');
476
+
477
+ tbody.innerHTML += `
478
+ <tr style="background: ${isBanned ? 'rgba(0,0,0,0.3)': 'transparent'}; opacity: ${isBanned ? 0.6 : 1}">
479
+ <td style="${isBanned ? 'text-decoration:line-through' : ''}">
480
+ <div class="tag" style="background: ${isBanned ? 'var(--bg-hover)' : 'rgba(59,130,246,0.1)'}; color: ${isBanned ? 'var(--text-muted)' : 'var(--accent)'}; border-color: ${isBanned ? 'transparent' : 'rgba(59,130,246,0.2)'}">${name}</div>
481
+ </td>
482
+ <td class="mono">
483
+ <span class="text-ok">βœ“ ${m.total_calls - m.total_errors}</span>
484
+ <span style="color:var(--text-dim); margin:0 4px">|</span>
485
+ <span class="text-crit">βœ— ${m.total_errors}</span>
486
+ </td>
487
+ <td class="mono ${srClass}" style="font-weight:600">${s.toFixed(1)}%</td>
488
+ <td class="mono">
489
+ <span style="color:var(--text-main)">${m.avg_latency_ms}ms</span> <span style="font-size:11px;color:var(--text-muted)">lat</span>
490
+ <span style="color:var(--text-dim); margin:0 4px">/</span>
491
+ <span style="color:var(--text-main)">${m.avg_ttft_ms||'β€”'}ms</span> <span style="font-size:11px;color:var(--text-muted)">ttft</span>
492
+ </td>
493
+ <td>
494
+ ${isBanned
495
+ ? `<button class="btn btn-ok" onclick="adminCmd('POST','/api/ai/admin/model/unban',{model_name:'${name}'})">πŸ”“ Remit Ban</button>`
496
+ : `<button class="btn btn-crit" onclick="adminCmd('POST','/api/ai/admin/model/ban',{model_name:'${name}'})">β›” Absolute Block</button>`}
497
+ </td>
498
+ </tr>
499
+ `;
500
+ });
501
  }
502
 
503
+ let loopTimer;
504
  async function fetchLoop() {
505
+ if(!API_KEY) return;
506
+ clearTimeout(loopTimer);
507
  try {
508
  const res = await fetch(METRICS_URL);
509
  const data = await res.json();
 
510
  document.getElementById('syncTime').textContent = new Date().toLocaleTimeString();
511
+ renderSys(data.system, data.redis);
512
+ renderProviders(data.providers, data.breakers);
513
+ renderModels(data.models);
514
+ drawCharts();
515
+ } catch(e) { console.error("Sync Error", e); }
516
+ loopTimer = setTimeout(fetchLoop, 5000);
 
 
517
  }
518
 
519
+ document.addEventListener('DOMContentLoaded', () => { initCharts(); if(API_KEY) fetchLoop(); });
 
 
 
520
  </script>
521
  </body>
522
  </html>
main.py CHANGED
@@ -123,8 +123,10 @@ def create_application() -> FastAPI:
123
 
124
  # Routes
125
  from app.api.api import public_router
 
126
  application.include_router(api_router, prefix="/api")
127
  application.include_router(public_router, prefix="/api")
 
128
  application.include_router(metrics_router) # /metrics for Prometheus/Grafana
129
 
130
  @application.get("/", include_in_schema=False)
 
123
 
124
  # Routes
125
  from app.api.api import public_router
126
+ from app.api.admin_ops import router as admin_router
127
  application.include_router(api_router, prefix="/api")
128
  application.include_router(public_router, prefix="/api")
129
+ application.include_router(admin_router, prefix="/api/ai")
130
  application.include_router(metrics_router) # /metrics for Prometheus/Grafana
131
 
132
  @application.get("/", include_in_schema=False)