agent-control-plane 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,45 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <meta name="color-scheme" content="light dark" />
7
+ <title>ACP Worker Dashboard</title>
8
+ <link rel="stylesheet" href="./styles.css" />
9
+ </head>
10
+ <body>
11
+ <main class="page">
12
+ <header class="hero">
13
+ <div>
14
+ <p class="eyebrow">Agent Control Plane</p>
15
+ <h1>Worker Dashboard</h1>
16
+ <p class="subtitle">
17
+ Track active runs, resident controllers, queue pressure, and provider failover in one place.
18
+ </p>
19
+ <p class="subtitle">
20
+ Lifecycle shows whether a worker session finished cleanly. Result shows whether that cycle implemented work, reported findings, or stopped blocked.
21
+ </p>
22
+ </div>
23
+ <div class="hero-actions">
24
+ <div class="hero-controls">
25
+ <button id="theme-toggle" type="button" aria-label="Toggle dark mode">Dark mode</button>
26
+ <button id="refresh-button" type="button">Refresh now</button>
27
+ </div>
28
+ <div class="meta">
29
+ <div>Auto refresh: <strong>5s</strong></div>
30
+ <div id="generated-at">Loading snapshot...</div>
31
+ </div>
32
+ </div>
33
+ </header>
34
+
35
+ <section id="overview" class="overview"></section>
36
+ <section id="profiles" class="profiles"></section>
37
+ </main>
38
+
39
+ <template id="empty-table-template">
40
+ <div class="empty-state">No data right now.</div>
41
+ </template>
42
+
43
+ <script src="./app.js" defer></script>
44
+ </body>
45
+ </html>
@@ -27,11 +27,11 @@ async def broadcast_snapshot():
27
27
  if not ws_clients:
28
28
  return
29
29
  payload = build_snapshot()
30
- encoded = json.dumps(payload, indent=2).encode("utf-8")
30
+ encoded = json.dumps(payload, indent=2)
31
31
  disconnected = set()
32
32
  for ws in ws_clients:
33
33
  try:
34
- await ws.send_bytes(encoded)
34
+ await ws.send_str(encoded)
35
35
  except Exception:
36
36
  disconnected.add(ws)
37
37
  for ws in disconnected:
@@ -40,13 +40,20 @@ async def broadcast_snapshot():
40
40
 
41
41
  async def snapshot_handler(request: web.Request) -> web.Response:
42
42
  """HTTP endpoint: GET /api/snapshot.json"""
43
- payload = build_snapshot()
44
- encoded = json.dumps(payload, indent=2).encode("utf-8")
45
- return web.Response(
46
- body=encoded,
47
- content_type="application/json; charset=utf-8",
48
- headers={"Cache-Control": "no-store"},
49
- )
43
+ try:
44
+ payload = build_snapshot()
45
+ encoded = json.dumps(payload, indent=2).encode("utf-8")
46
+ return web.Response(
47
+ body=encoded,
48
+ content_type="application/json",
49
+ headers={"Cache-Control": "no-store"},
50
+ )
51
+ except Exception as e:
52
+ import traceback
53
+ error_detail = traceback.format_exc()
54
+ print(f"ERROR in snapshot_handler: {e}", flush=True)
55
+ print(error_detail, flush=True)
56
+ raise
50
57
 
51
58
 
52
59
  async def doctor_handler(request: web.Request) -> web.Response:
@@ -80,7 +87,7 @@ async def doctor_handler(request: web.Request) -> web.Response:
80
87
  payload = {"output": output.decode("utf-8", errors="replace")}
81
88
  return web.Response(
82
89
  body=json.dumps(payload),
83
- content_type="application/json; charset=utf-8",
90
+ content_type="application/json",
84
91
  )
85
92
  except asyncio.TimeoutError:
86
93
  return web.Response(
@@ -92,10 +99,51 @@ async def doctor_handler(request: web.Request) -> web.Response:
92
99
  payload = {"error": exc.returncode, "output": exc.output}
93
100
  return web.Response(
94
101
  body=json.dumps(payload),
95
- content_type="application/json; charset=utf-8",
102
+ content_type="application/json",
96
103
  )
97
104
 
98
105
 
106
+ async def scheduler_status_handler(request: web.Request) -> web.Response:
107
+ """HTTP endpoint: GET /api/scheduler-status"""
108
+
109
+ # Check if scheduler is running
110
+ state_dir = Path.home() / ".agent-runtime" / "control-plane" / "kick-scheduler"
111
+ pid_file = state_dir / "pid"
112
+ log_file = state_dir / "kick-scheduler.log"
113
+
114
+ is_running = False
115
+ pid = None
116
+ if pid_file.is_file():
117
+ pid = pid_file.read_text().strip()
118
+ if pid:
119
+ try:
120
+ os.kill(int(pid), 0) # Check if process exists
121
+ is_running = True
122
+ except (ProcessLookupError, PermissionError):
123
+ is_running = False
124
+
125
+ # Read last few lines of log
126
+ last_log_lines = []
127
+ if log_file.is_file():
128
+ try:
129
+ lines = log_file.read_text().strip().split("\n")
130
+ last_log_lines = lines[-5:] if len(lines) > 5 else lines
131
+ except Exception:
132
+ pass
133
+
134
+ payload = {
135
+ "is_running": is_running,
136
+ "pid": pid if is_running else None,
137
+ "state_dir": str(state_dir),
138
+ "last_log_lines": last_log_lines,
139
+ "message": "Scheduler status from real state",
140
+ }
141
+ return web.Response(
142
+ body=json.dumps(payload, indent=2),
143
+ content_type="application/json",
144
+ )
145
+
146
+
99
147
  async def profile_export_handler(request: web.Request) -> web.Response:
100
148
  """HTTP endpoint: GET /api/profile/export?profile_id=xxx"""
101
149
  profile_id = request.query.get("profile_id", "")
@@ -123,7 +171,7 @@ async def profile_export_handler(request: web.Request) -> web.Response:
123
171
  payload = {"profile_id": profile_id, "config": config, "config_file": str(config_file)}
124
172
  return web.Response(
125
173
  body=json.dumps(payload),
126
- content_type="application/json; charset=utf-8",
174
+ content_type="application/json",
127
175
  )
128
176
  except Exception as exc:
129
177
  return web.Response(
@@ -164,7 +212,7 @@ async def profile_import_handler(request: web.Request) -> web.Response:
164
212
  payload = {"status": "ok", "profile_id": profile_id, "config_file": str(config_file)}
165
213
  return web.Response(
166
214
  body=json.dumps(payload),
167
- content_type="application/json; charset=utf-8",
215
+ content_type="application/json",
168
216
  )
169
217
  except Exception as exc:
170
218
  return web.Response(
@@ -182,8 +230,8 @@ async def websocket_handler(request: web.Request) -> web.WebSocketResponse:
182
230
  try:
183
231
  # Send initial snapshot
184
232
  payload = build_snapshot()
185
- encoded = json.dumps(payload, indent=2).encode("utf-8")
186
- await ws.send_bytes(encoded)
233
+ encoded = json.dumps(payload, indent=2)
234
+ await ws.send_str(encoded)
187
235
  # Keep connection alive, listen for client messages (ping/pong)
188
236
  async for msg in ws:
189
237
  if msg.type == WSMsgType.TEXT:
@@ -204,6 +252,7 @@ def build_app() -> web.Application:
204
252
  app.router.add_get("/api/snapshot.json", snapshot_handler)
205
253
  app.router.add_get("/api/doctor", doctor_handler)
206
254
  app.router.add_get("/api/profile/export", profile_export_handler)
255
+ app.router.add_get("/api/scheduler-status", scheduler_status_handler)
207
256
  app.router.add_post("/api/profile/import", profile_import_handler)
208
257
  app.router.add_get("/ws", websocket_handler)
209
258
  # Static files (dashboard HTML/CSS/JS)