Ordo commited on
Commit
6425ec7
·
0 Parent(s):

Initial public release

Browse files
.env.example ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ OPENCLAW_AGENTS_ROOT=~/.openclaw/agents
2
+ SESSION_AMPLIFIER_BASE_URL=http://localhost:8477
3
+ VITE_SESSION_AMPLIFIER_BASE_URL=/session-amplifier
.gitignore ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .env
2
+ .env.*
3
+ !.env.example
4
+ __pycache__/
5
+ *.py[cod]
6
+ .pytest_cache/
7
+ .venv/
8
+ venv/
9
+ node_modules/
10
+ react-dashboard/dist/
11
+ *.db
12
+ *.sqlite
13
+ *.log
14
+ *.bak*
Dockerfile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt /app/requirements.txt
6
+ RUN pip install --no-cache-dir -r /app/requirements.txt
7
+
8
+ COPY . /app
9
+
10
+ ENV STREAMLIT_SERVER_HEADLESS=true \
11
+ STREAMLIT_SERVER_PORT=8501 \
12
+ STREAMLIT_SERVER_ADDRESS=0.0.0.0
13
+
14
+ EXPOSE 8501
15
+
16
+ CMD ["streamlit", "run", "app.py"]
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Patrick
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # OpenClaw Ops Dashboard
2
+
3
+ Developer-facing dashboard experiments for OpenClaw operations.
4
+
5
+ This repo includes two dashboard surfaces:
6
+
7
+ - `react-dashboard/` — the current Vite/React dashboard for Session Amplifier data.
8
+ - `app.py` — the older Streamlit dashboard that reads local agent session JSON files.
9
+
10
+ ## React Dashboard
11
+
12
+ ```bash
13
+ cd react-dashboard
14
+ npm install
15
+ npm run build
16
+ npm run dev -- --port 5177
17
+ ```
18
+
19
+ The Vite dev server proxies `/session-amplifier/*` to `SESSION_AMPLIFIER_BASE_URL` or `http://localhost:8477`.
20
+
21
+ ## Streamlit Dashboard
22
+
23
+ The Streamlit app reads agent session JSON files from `~/.openclaw/agents/*/sessions` and displays:
24
+
25
+ - Agent list with last activity timestamps (best-effort)
26
+ - A simple timeline of recent cron runs (files with "cron" in the name under agents)
27
+
28
+ ```bash
29
+ python3 -m venv .venv
30
+ . .venv/bin/activate
31
+ pip install -r requirements.txt
32
+ streamlit run app.py
33
+ ```
34
+
35
+ Notes:
36
+ - The app is intentionally minimal and tolerant of malformed session files.
37
+ - Timestamps are parsed best-effort from common keys (last_activity, updated_at, timestamp).
SECURITY.md ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # Security
2
+
3
+ Do not commit session transcripts, generated dashboards containing private operational data, `.env` files, or screenshots showing private conversations or credentials.
app.py ADDED
@@ -0,0 +1,1084 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import datetime
2
+ import json
3
+ import os
4
+ import re
5
+ import time
6
+ from collections import Counter, deque
7
+ from pathlib import Path
8
+ from zoneinfo import ZoneInfo
9
+
10
+ import streamlit as st
11
+
12
+
13
+ if os.name == "nt" or Path("C:/openclaw-biz/state/openclaw").exists():
14
+ OPENCLAW_ROOT = Path("C:/openclaw-biz/state/openclaw")
15
+ else:
16
+ OPENCLAW_ROOT = Path("/home/node/.openclaw")
17
+
18
+ AGENTS_DIR = OPENCLAW_ROOT / "agents"
19
+ CRON_DIR = OPENCLAW_ROOT / "cron"
20
+ STATE_DIR = Path("/home/node/.openclaw/workspace/ops/state")
21
+ SESSION_AMPLIFIER_BASE = os.environ.get("SESSION_AMPLIFIER_BASE_URL", "http://session-amplifier:8477")
22
+ LOCAL_TZ = ZoneInfo("America/New_York")
23
+
24
+
25
+ def parse_ts_any(ts):
26
+ if ts is None:
27
+ return None
28
+ if isinstance(ts, datetime.datetime):
29
+ return ts if ts.tzinfo else ts.replace(tzinfo=datetime.timezone.utc)
30
+ if isinstance(ts, (int, float)):
31
+ try:
32
+ value = float(ts)
33
+ if value > 1e11:
34
+ value /= 1000.0
35
+ return datetime.datetime.fromtimestamp(value, tz=datetime.timezone.utc)
36
+ except Exception:
37
+ return None
38
+ if isinstance(ts, str):
39
+ text = ts.strip()
40
+ if not text:
41
+ return None
42
+ if text.endswith("Z"):
43
+ text = text[:-1] + "+00:00"
44
+ try:
45
+ dt = datetime.datetime.fromisoformat(text)
46
+ return dt if dt.tzinfo else dt.replace(tzinfo=datetime.timezone.utc)
47
+ except Exception:
48
+ return None
49
+ return None
50
+
51
+
52
+ def fmt_local(ts):
53
+ dt = parse_ts_any(ts)
54
+ if dt is None:
55
+ return ""
56
+ try:
57
+ return dt.astimezone(LOCAL_TZ).strftime("%Y-%m-%d %I:%M:%S %p %Z")
58
+ except Exception:
59
+ return dt.isoformat()
60
+
61
+
62
+ def fmt_duration(seconds):
63
+ """Format seconds into human-readable duration."""
64
+ if seconds < 60:
65
+ return f"{int(seconds)}s"
66
+ elif seconds < 3600:
67
+ return f"{int(seconds/60)}m"
68
+ elif seconds < 86400:
69
+ return f"{int(seconds/3600)}h"
70
+ else:
71
+ return f"{int(seconds/86400)}d"
72
+
73
+
74
+ def read_json(path: Path):
75
+ try:
76
+ return json.loads(path.read_text(encoding="utf-8"))
77
+ except Exception:
78
+ return None
79
+
80
+
81
+ def read_tail_lines(path: Path, count: int = 80):
82
+ try:
83
+ with path.open("r", encoding="utf-8", errors="ignore") as fh:
84
+ return list(deque(fh, maxlen=count))
85
+ except Exception:
86
+ return []
87
+
88
+
89
+ def clean_session_label(value: str, *, fallback: str = "") -> str:
90
+ text = (value or "").strip()
91
+ if not text:
92
+ return fallback
93
+ text = re.sub(r"^Discord thread\s+#?[^›]+›\s*", "", text)
94
+ text = re.sub(r"\s+channel id:\d+\s*$", "", text, flags=re.IGNORECASE)
95
+ text = text.strip(" #")
96
+ text = text.replace("-", " ")
97
+ text = re.sub(r"\s+", " ", text)
98
+ return text[:96] or fallback
99
+
100
+
101
+ def session_display_label(row: dict) -> str:
102
+ label = clean_session_label(row.get("label") or "")
103
+ if label:
104
+ return label
105
+ key = row.get("sessionKey") or row.get("sessionId") or "session"
106
+ kind = row.get("kind") or "session"
107
+ return f"{kind}: {str(key)[:32]}"
108
+
109
+
110
+ def normalize_exec_tool_label(content_block):
111
+ try:
112
+ name = content_block.get("name") or "unknown_tool"
113
+ if name != "exec":
114
+ return name
115
+ args = content_block.get("arguments")
116
+ cmd = None
117
+ if isinstance(args, dict):
118
+ cmd = args.get("command") or args.get("cmd")
119
+ elif isinstance(args, str):
120
+ cmd = args
121
+ if not isinstance(cmd, str):
122
+ return "exec"
123
+ if "mcporter" not in cmd.strip().lower():
124
+ return "exec"
125
+ import re
126
+
127
+ match = re.search(r"(?:^|\s)mcporter\s+call\s+([A-Za-z0-9_\-\.]+)", cmd)
128
+ if match:
129
+ return f"exec mcporter {match.group(1)}"
130
+ return "exec mcporter"
131
+ except Exception:
132
+ return content_block.get("name") or "unknown_tool"
133
+
134
+
135
+ def session_index_rows():
136
+ rows = []
137
+ session_key_by_agent_and_id = {}
138
+ if not AGENTS_DIR.exists():
139
+ return rows, session_key_by_agent_and_id
140
+
141
+ for agent_dir in sorted(AGENTS_DIR.iterdir()):
142
+ if not agent_dir.is_dir() or agent_dir.name.startswith("."):
143
+ continue
144
+ sessions_file = agent_dir / "sessions" / "sessions.json"
145
+ data = read_json(sessions_file)
146
+ if not isinstance(data, dict):
147
+ continue
148
+ for session_key, meta in data.items():
149
+ if not isinstance(meta, dict):
150
+ continue
151
+ updated_raw = meta.get("updatedAt") or meta.get("updatedAtMs")
152
+ session_id = meta.get("sessionId") or ""
153
+ delivery = meta.get("deliveryContext") if isinstance(meta.get("deliveryContext"), dict) else {}
154
+ kind = "session"
155
+ if ":subagent:" in session_key:
156
+ kind = "subagent"
157
+ elif ":cron:" in session_key:
158
+ kind = "cron"
159
+ elif ":discord:" in session_key:
160
+ kind = "discord"
161
+ origin = meta.get("origin") if isinstance(meta.get("origin"), dict) else {}
162
+ label = (
163
+ meta.get("displayName")
164
+ or meta.get("derivedTitle")
165
+ or meta.get("title")
166
+ or origin.get("label")
167
+ or meta.get("groupChannel")
168
+ or ""
169
+ )
170
+ row = {
171
+ "agent": agent_dir.name,
172
+ "sessionKey": session_key,
173
+ "sessionId": session_id,
174
+ "label": clean_session_label(str(label), fallback=session_key),
175
+ "updatedAt": parse_ts_any(updated_raw),
176
+ "updated": fmt_local(updated_raw),
177
+ "deliveryTo": delivery.get("to") or "",
178
+ "kind": kind,
179
+ }
180
+ rows.append(row)
181
+ if session_id:
182
+ session_key_by_agent_and_id[(agent_dir.name, session_id)] = row
183
+
184
+ rows.sort(
185
+ key=lambda row: row["updatedAt"] or datetime.datetime.min.replace(tzinfo=datetime.timezone.utc),
186
+ reverse=True,
187
+ )
188
+ return rows, session_key_by_agent_and_id
189
+
190
+
191
+ def transcript_activity_rows(session_lookup, max_files=80):
192
+ rows = []
193
+ if not AGENTS_DIR.exists():
194
+ return rows
195
+
196
+ jsonl_files = sorted(
197
+ AGENTS_DIR.rglob("sessions/*.jsonl"),
198
+ key=lambda path: path.stat().st_mtime if path.exists() else 0,
199
+ reverse=True,
200
+ )[:max_files]
201
+
202
+ for path in jsonl_files:
203
+ agent = path.parts[-3] if len(path.parts) >= 3 else "unknown"
204
+ session_id = path.stem
205
+ session_meta = session_lookup.get((agent, session_id))
206
+ session_key = session_meta.get("sessionKey") if isinstance(session_meta, dict) else session_meta
207
+ session_key = session_key or session_id
208
+ label = session_meta.get("label") if isinstance(session_meta, dict) else ""
209
+ last_ts = None
210
+ summary = ""
211
+ role = ""
212
+ for line in reversed(read_tail_lines(path, 120)):
213
+ line = line.strip()
214
+ if not line:
215
+ continue
216
+ try:
217
+ row = json.loads(line)
218
+ except Exception:
219
+ continue
220
+ msg = row.get("message") if isinstance(row.get("message"), dict) else {}
221
+ ts = parse_ts_any(row.get("timestamp") or row.get("time") or msg.get("timestamp") or msg.get("time"))
222
+ if ts and last_ts is None:
223
+ last_ts = ts
224
+ if not summary and isinstance(msg, dict):
225
+ role = msg.get("role") or row.get("type") or ""
226
+ content = msg.get("content") if isinstance(msg.get("content"), list) else []
227
+ for block in content:
228
+ if not isinstance(block, dict):
229
+ continue
230
+ if block.get("type") == "toolCall":
231
+ summary = f"tool: {normalize_exec_tool_label(block)}"
232
+ break
233
+ if block.get("type") == "text":
234
+ text = (block.get("text") or "").strip().replace("\n", " ")
235
+ if text:
236
+ summary = text[:160]
237
+ break
238
+ if not summary and msg.get("errorMessage"):
239
+ summary = str(msg.get("errorMessage"))[:160]
240
+ if last_ts and summary:
241
+ break
242
+ rows.append(
243
+ {
244
+ "agent": agent,
245
+ "sessionKey": session_key,
246
+ "sessionId": session_id,
247
+ "label": label or clean_session_label(session_key, fallback=session_id),
248
+ "updatedAt": last_ts,
249
+ "updated": fmt_local(last_ts),
250
+ "role": role,
251
+ "summary": summary or "(no recent summary)",
252
+ "file": str(path),
253
+ }
254
+ )
255
+
256
+ rows.sort(
257
+ key=lambda row: row["updatedAt"] or datetime.datetime.min.replace(tzinfo=datetime.timezone.utc),
258
+ reverse=True,
259
+ )
260
+ return rows
261
+
262
+
263
+ def list_cron_jobs(limit=100):
264
+ """Enhanced cron job listing with health status and staleness detection."""
265
+ jobs_file = CRON_DIR / "jobs.json"
266
+ data = read_json(jobs_file)
267
+ jobs = data.get("jobs", []) if isinstance(data, dict) else []
268
+ rows = []
269
+ now = datetime.datetime.now(datetime.timezone.utc)
270
+ now_ms = time.time() * 1000
271
+
272
+ for job in jobs[:limit]:
273
+ if not isinstance(job, dict):
274
+ continue
275
+ state = job.get("state") if isinstance(job.get("state"), dict) else {}
276
+ schedule = job.get("schedule") if isinstance(job.get("schedule"), dict) else {}
277
+ payload = job.get("payload") if isinstance(job.get("payload"), dict) else {}
278
+
279
+ last_run_ms = state.get("lastRunAtMs")
280
+ next_run_ms = state.get("nextRunAtMs")
281
+ consecutive_errors = state.get("consecutiveErrors", 0)
282
+ last_status = state.get("lastStatus") or state.get("lastRunStatus") or ""
283
+
284
+ # Calculate staleness
285
+ staleness = None
286
+ if last_run_ms and next_run_ms:
287
+ # If next run was scheduled but hasn't happened yet, check if we're past due
288
+ if now_ms > next_run_ms + 300000: # 5 min grace period
289
+ staleness = fmt_duration((now_ms - next_run_ms) / 1000)
290
+ elif last_run_ms and schedule.get("expr"):
291
+ # Estimate staleness based on schedule (rough: check if > 2x expected interval)
292
+ pass # Skip for now - would need cron parsing
293
+
294
+ # Health status
295
+ health = "healthy"
296
+ health_reason = ""
297
+ if not job.get("enabled", True):
298
+ health = "disabled"
299
+ elif consecutive_errors >= 3:
300
+ health = "critical"
301
+ health_reason = f"{consecutive_errors} consecutive errors"
302
+ elif consecutive_errors > 0:
303
+ health = "warning"
304
+ health_reason = f"{consecutive_errors} error(s)"
305
+ elif last_status == "error":
306
+ health = "error"
307
+ elif staleness:
308
+ health = "stale"
309
+ health_reason = f"overdue by {staleness}"
310
+ elif state.get("runningAtMs"):
311
+ health = "running"
312
+
313
+ rows.append(
314
+ {
315
+ "name": job.get("name") or job.get("id") or "unnamed",
316
+ "jobId": job.get("id") or "",
317
+ "enabled": bool(job.get("enabled", True)),
318
+ "kind": payload.get("kind") or "",
319
+ "model": payload.get("model") or "",
320
+ "schedule": schedule.get("expr") or schedule.get("kind") or "",
321
+ "tz": schedule.get("tz") or "",
322
+ "nextRunAt": parse_ts_any(state.get("nextRunAtMs") or state.get("nextRunAt")),
323
+ "nextRun": fmt_local(state.get("nextRunAtMs") or state.get("nextRunAt")),
324
+ "runningAt": parse_ts_any(state.get("runningAtMs") or state.get("runningAt")),
325
+ "running": fmt_local(state.get("runningAtMs") or state.get("runningAt")),
326
+ "lastRunAt": parse_ts_any(state.get("lastRunAtMs") or state.get("lastRunAt")),
327
+ "lastRun": fmt_local(state.get("lastRunAtMs") or state.get("lastRunAt")),
328
+ "lastStatus": last_status,
329
+ "sessionKey": job.get("sessionKey") or "",
330
+ "consecutiveErrors": consecutive_errors,
331
+ "health": health,
332
+ "healthReason": health_reason,
333
+ "staleness": staleness,
334
+ }
335
+ )
336
+ rows.sort(
337
+ key=lambda row: (
338
+ {"critical": 0, "error": 1, "warning": 2, "stale": 3, "running": 4, "healthy": 5, "disabled": 6}.get(row["health"], 7),
339
+ row["runningAt"] or row["nextRunAt"] or row["lastRunAt"] or datetime.datetime.min.replace(tzinfo=datetime.timezone.utc)
340
+ ),
341
+ reverse=False,
342
+ )
343
+ return rows
344
+
345
+
346
+ def collect_metrics_last_24h(max_files=500):
347
+ now = datetime.datetime.now(datetime.timezone.utc)
348
+ cutoff = now - datetime.timedelta(hours=24)
349
+ jsonl_files = sorted(
350
+ AGENTS_DIR.rglob("sessions/*.jsonl"),
351
+ key=lambda path: path.stat().st_mtime if path.exists() else 0,
352
+ reverse=True,
353
+ )[:max_files]
354
+
355
+ tool_counts = Counter()
356
+ error_count = 0
357
+ total_events = 0
358
+ token_total = 0
359
+ input_tokens = 0
360
+ output_tokens = 0
361
+ cost_total = 0.0
362
+ cache_hits = 0
363
+ cache_samples = 0
364
+ latest_per_agent = {}
365
+
366
+ for path in jsonl_files:
367
+ agent = path.parts[-3] if len(path.parts) >= 3 else "unknown"
368
+ try:
369
+ with path.open("r", encoding="utf-8", errors="ignore") as fh:
370
+ for line in fh:
371
+ line = line.strip()
372
+ if not line:
373
+ continue
374
+ try:
375
+ row = json.loads(line)
376
+ except Exception:
377
+ continue
378
+ msg = row.get("message") if isinstance(row.get("message"), dict) else {}
379
+ ts = parse_ts_any(row.get("timestamp") or msg.get("timestamp") or row.get("time") or msg.get("time"))
380
+ if ts is None or ts < cutoff:
381
+ continue
382
+ total_events += 1
383
+ if msg.get("errorMessage") or msg.get("stopReason") == "error":
384
+ error_count += 1
385
+ summary = None
386
+ content = msg.get("content") if isinstance(msg.get("content"), list) else []
387
+ for block in content:
388
+ if not isinstance(block, dict):
389
+ continue
390
+ if block.get("type") == "toolCall":
391
+ label = normalize_exec_tool_label(block)
392
+ tool_counts[label] += 1
393
+ summary = summary or f"tool: {label}"
394
+ elif block.get("type") == "text" and msg.get("role") == "assistant":
395
+ text = (block.get("text") or "").strip().replace("\n", " ")
396
+ if text and summary is None:
397
+ summary = text[:120]
398
+ usage = msg.get("usage") if isinstance(msg.get("usage"), dict) else {}
399
+ if usage:
400
+ # Token extraction - prefer explicit totals
401
+ total_tok = usage.get("total_tokens") or usage.get("totalTokens")
402
+ input_tok = usage.get("input_tokens") or usage.get("inputTokens") or usage.get("input")
403
+ output_tok = usage.get("output_tokens") or usage.get("outputTokens") or usage.get("output")
404
+
405
+ if total_tok:
406
+ token_total += int(total_tok)
407
+ elif input_tok or output_tok:
408
+ token_total += int(input_tok or 0) + int(output_tok or 0)
409
+
410
+ if input_tok:
411
+ input_tokens += int(input_tok)
412
+ if output_tok:
413
+ output_tokens += int(output_tok)
414
+
415
+ for key in ("cost", "total_cost", "usd", "costUsd"):
416
+ value = usage.get(key)
417
+ if isinstance(value, (int, float)):
418
+ cost_total += float(value)
419
+ cached = usage.get("cache_hit") or usage.get("cacheHit")
420
+ cached_tokens = usage.get("cached_tokens") or usage.get("cachedTokens") or usage.get("cache_read_tokens") or usage.get("cacheReadTokens")
421
+ if isinstance(cached, bool):
422
+ cache_samples += 1
423
+ if cached:
424
+ cache_hits += 1
425
+ elif isinstance(cached_tokens, (int, float)):
426
+ cache_samples += 1
427
+ if cached_tokens > 0:
428
+ cache_hits += 1
429
+ latest = latest_per_agent.get(agent)
430
+ if latest is None or ts > latest["ts"]:
431
+ latest_per_agent[agent] = {"ts": ts, "summary": summary or "No recent assistant/tool summary"}
432
+ except Exception:
433
+ continue
434
+
435
+ active_cutoff = now - datetime.timedelta(minutes=60)
436
+ active_agents = [
437
+ {"agent": agent, "ts": info["ts"], "updated": fmt_local(info["ts"]), "summary": info["summary"]}
438
+ for agent, info in latest_per_agent.items()
439
+ if info["ts"] >= active_cutoff
440
+ ]
441
+ active_agents.sort(key=lambda row: row["ts"], reverse=True)
442
+ return {
443
+ "tool_counts": tool_counts,
444
+ "error_rate": (error_count / total_events * 100.0) if total_events else 0.0,
445
+ "error_count": error_count,
446
+ "total_events": total_events,
447
+ "token_total": token_total,
448
+ "input_tokens": input_tokens,
449
+ "output_tokens": output_tokens,
450
+ "cost_total": cost_total,
451
+ "cache_hit_proxy": (cache_hits / cache_samples * 100.0) if cache_samples else None,
452
+ "cache_samples": cache_samples,
453
+ "active_summaries": active_agents,
454
+ }
455
+
456
+
457
+ def get_live_session_activity(session_key, agent_name, max_lines=100):
458
+ """Extract detailed recent activity from a specific session's JSONL."""
459
+ if not AGENTS_DIR.exists():
460
+ return []
461
+
462
+ # Find the JSONL file for this session
463
+ agent_dir = AGENTS_DIR / agent_name
464
+ if not agent_dir.exists():
465
+ return []
466
+
467
+ # session_key format: agent:name:uuid or just uuid
468
+ session_id = session_key.split(":")[-1] if ":" in session_key else session_key
469
+ jsonl_path = agent_dir / "sessions" / f"{session_id}.jsonl"
470
+
471
+ if not jsonl_path.exists():
472
+ return []
473
+
474
+ activities = []
475
+ lines = read_tail_lines(jsonl_path, max_lines)
476
+
477
+ for line in lines:
478
+ line = line.strip()
479
+ if not line:
480
+ continue
481
+ try:
482
+ row = json.loads(line)
483
+ except Exception:
484
+ continue
485
+
486
+ msg = row.get("message") if isinstance(row.get("message"), dict) else {}
487
+ ts = parse_ts_any(row.get("timestamp") or msg.get("timestamp") or row.get("time") or msg.get("time"))
488
+
489
+ entry = {
490
+ "timestamp": ts,
491
+ "time": fmt_local(ts),
492
+ "role": msg.get("role", ""),
493
+ "type": "",
494
+ "summary": "",
495
+ "details": "",
496
+ }
497
+
498
+ content = msg.get("content") if isinstance(msg.get("content"), list) else []
499
+ for block in content:
500
+ if not isinstance(block, dict):
501
+ continue
502
+
503
+ if block.get("type") == "thinking":
504
+ entry["type"] = "thinking"
505
+ thinking = block.get("thinking") or ""
506
+ entry["summary"] = "Thinking..." if thinking else "(empty thinking)"
507
+ entry["details"] = thinking[:500] if thinking else ""
508
+
509
+ elif block.get("type") == "toolCall":
510
+ entry["type"] = "tool"
511
+ label = normalize_exec_tool_label(block)
512
+ entry["summary"] = f"→ {label}"
513
+ args = block.get("arguments", {})
514
+ if isinstance(args, dict):
515
+ # Summarize key arguments
516
+ arg_summary = []
517
+ for k, v in list(args.items())[:3]:
518
+ v_str = str(v)[:50]
519
+ arg_summary.append(f"{k}={v_str}")
520
+ entry["details"] = ", ".join(arg_summary)
521
+ elif isinstance(args, str):
522
+ entry["details"] = args[:100]
523
+
524
+ elif block.get("type") == "toolResult":
525
+ entry["type"] = "result"
526
+ content_data = block.get("content")
527
+ is_error = block.get("isError") or block.get("error")
528
+ entry["summary"] = "✗ Error" if is_error else "✓ Result"
529
+ if isinstance(content_data, str):
530
+ entry["details"] = content_data[:200]
531
+ elif isinstance(content_data, list) and content_data:
532
+ entry["details"] = str(content_data[0])[:200]
533
+
534
+ elif block.get("type") == "text":
535
+ entry["type"] = "text"
536
+ text = (block.get("text") or "").strip()
537
+ entry["summary"] = text[:80].replace("\n", " ")
538
+ entry["details"] = text[:300]
539
+
540
+ if entry["type"] or entry["summary"]:
541
+ activities.append(entry)
542
+
543
+ return activities
544
+
545
+
546
+ def _load_sidecar_artifact(name: str):
547
+ """Load a Session Amplifier artifact from the state directory. Returns {} if unavailable."""
548
+ path = STATE_DIR / "session_amplifier" / name
549
+ if not path.exists():
550
+ return None
551
+ try:
552
+ return json.loads(path.read_text(encoding="utf-8"))
553
+ except Exception:
554
+ return None
555
+
556
+
557
+ def _sidecar_reachable() -> bool:
558
+ try:
559
+ import urllib.request
560
+ req = urllib.request.Request(f"{SESSION_AMPLIFIER_BASE}/health", method="GET")
561
+ urllib.request.urlopen(req, timeout=2)
562
+ return True
563
+ except Exception:
564
+ return False
565
+
566
+
567
+ def _fetch_sidecar_json(path: str, default=None):
568
+ """GET a path from the sidecar API. Returns parsed JSON or default on failure."""
569
+ try:
570
+ import urllib.request
571
+ req = urllib.request.Request(f"{SESSION_AMPLIFIER_BASE}{path}", method="GET")
572
+ with urllib.request.urlopen(req, timeout=5) as resp:
573
+ return json.loads(resp.read().decode())
574
+ except Exception:
575
+ return default
576
+
577
+
578
+ def _load_ops_json(name: str, default=None):
579
+ path = STATE_DIR / name
580
+ try:
581
+ return json.loads(path.read_text(encoding="utf-8"))
582
+ except Exception:
583
+ return default
584
+
585
+
586
+ def render_model_ops_tab(session_rows):
587
+ snapshot = _load_ops_json("model-ops-snapshot-latest.json", {}) or {}
588
+ merged = _load_ops_json("model-benchmarks-merged-latest.json", {}) or {}
589
+ cost_rows = _load_ops_json("session-cost-top50-latest.json", []) or []
590
+
591
+ st.subheader("Model Ops")
592
+ c1, c2, c3, c4 = st.columns(4)
593
+ c1.metric("OpenRouter models", snapshot.get("market_model_count", 0))
594
+ c2.metric("Merged benchmark models", len(merged.get("models", [])))
595
+ c3.metric("HF models", len((_load_ops_json("hf-benchmarks-latest.json", {}) or {}).get("models", [])))
596
+ c4.metric("Top cost rows", len(cost_rows))
597
+
598
+ st.markdown("### Benchmark sources")
599
+ src_rows = []
600
+ for src in snapshot.get("benchmark_source_status", []):
601
+ if isinstance(src, dict):
602
+ src_rows.append({"source": src.get("name"), "status": src.get("status")})
603
+ if src_rows:
604
+ st.dataframe(src_rows, use_container_width=True, hide_index=True)
605
+ else:
606
+ st.caption("No benchmark source metadata available")
607
+
608
+ st.markdown("### Cost coverage quality")
609
+ coverage = Counter((row.get("cost_source") or "unknown") for row in cost_rows)
610
+ cc1, cc2, cc3, cc4 = st.columns(4)
611
+ cc1.metric("Observed", coverage.get("observed", 0))
612
+ cc2.metric("Estimated", coverage.get("estimated", 0))
613
+ cc3.metric("Unknown", coverage.get("unknown", 0) + coverage.get("", 0))
614
+ cc4.metric("Trust score", f"{(snapshot.get('cost_trust_score', 0.0) * 100):.1f}%")
615
+
616
+ st.markdown("### Highest current observed session cost")
617
+ if cost_rows:
618
+ agent_filter = st.selectbox("Cost rows filter by agent", ["all"] + sorted({r.get('agent_id') or '' for r in cost_rows if r.get('agent_id')}), index=0)
619
+ filtered_cost = [r for r in cost_rows if agent_filter == 'all' or r.get('agent_id') == agent_filter]
620
+ st.dataframe(filtered_cost[:15], use_container_width=True, hide_index=True)
621
+
622
+ st.markdown("#### Inspect costly session")
623
+ lookup = {f"{r.get('agent_id')}/{r.get('session_id')} — {r.get('primary_model')} — ${r.get('total_estimated_usd')}": r for r in filtered_cost[:25]}
624
+ if lookup:
625
+ selected = st.selectbox("Jump to session details", list(lookup.keys()), index=0)
626
+ chosen = lookup[selected]
627
+ agent = chosen.get('agent_id')
628
+ session_id = chosen.get('session_id')
629
+ matching = [r for r in session_rows if r.get('agent') == agent and (r.get('sessionId') == session_id or session_id in (r.get('sessionKey') or ''))]
630
+ if matching:
631
+ match = matching[0]
632
+ st.caption(f"Session key: {match.get('sessionKey')}")
633
+ st.caption(f"Last updated: {match.get('updated')}")
634
+ st.caption(f"Delivery: {match.get('deliveryTo')}")
635
+ recent = get_live_session_activity(match.get('sessionKey'), agent, max_lines=40)
636
+ if recent:
637
+ st.dataframe([
638
+ {
639
+ 'time': a.get('time'),
640
+ 'type': a.get('type'),
641
+ 'summary': a.get('summary'),
642
+ 'details': (a.get('details') or '')[:120],
643
+ } for a in recent[-15:]
644
+ ], use_container_width=True, hide_index=True)
645
+ else:
646
+ st.caption('No recent activity extracted from transcript.')
647
+ else:
648
+ st.caption('No matching session found in current session index.')
649
+ else:
650
+ st.caption("No session cost artifacts found")
651
+
652
+ show_legacy = st.checkbox("Show legacy / non-orchestration benchmark leaders", value=False)
653
+ st.markdown("### Top merged Arena Elo")
654
+ arena_elo = snapshot.get("top_arena_elo", [])
655
+ if show_legacy:
656
+ arena_elo = sorted([m for m in merged.get('models', []) if (m.get('arena') or {}).get('elo') is not None], key=lambda m: (m.get('arena') or {}).get('elo') or 0, reverse=True)[:10]
657
+ if arena_elo:
658
+ st.dataframe([
659
+ {"model": r.get("model"), "elo": (r.get("arena") or {}).get("elo"), "mapping": (r.get("mapping") or {}).get("confidence")}
660
+ for r in arena_elo
661
+ ], use_container_width=True, hide_index=True)
662
+ else:
663
+ st.caption("No merged arena elo rows")
664
+
665
+ st.markdown("### Top merged Arena winrate")
666
+ arena_wr = snapshot.get("top_arena_winrate", [])
667
+ if show_legacy:
668
+ arena_wr = sorted([m for m in merged.get('models', []) if (m.get('arena') or {}).get('winrate') is not None], key=lambda m: (m.get('arena') or {}).get('winrate') or 0, reverse=True)[:10]
669
+ if arena_wr:
670
+ st.dataframe([
671
+ {
672
+ "model": r.get("model"),
673
+ "winrate": (r.get("arena") or {}).get("winrate"),
674
+ "appearances": (r.get("arena") or {}).get("appearances"),
675
+ "mapping": (r.get("mapping") or {}).get("confidence"),
676
+ }
677
+ for r in arena_wr
678
+ ], use_container_width=True, hide_index=True)
679
+ else:
680
+ st.caption("No merged arena winrate rows")
681
+
682
+ st.markdown("### Highest heuristic efficiency")
683
+ eff = snapshot.get("top_efficiency", [])
684
+ if eff:
685
+ st.dataframe([
686
+ {
687
+ "model": r.get("id"),
688
+ "efficiency": r.get("efficiency_score"),
689
+ "input_per_1m": (r.get("pricing_per_1m") or {}).get("input"),
690
+ "context": r.get("context_length"),
691
+ }
692
+ for r in eff
693
+ ], use_container_width=True, hide_index=True)
694
+
695
+ st.markdown("### Mapping confidence breakdown")
696
+ counts = Counter(((m.get("mapping") or {}).get("confidence") or "unknown") for m in merged.get("models", []))
697
+ st.json(dict(counts))
698
+
699
+
700
+ def main():
701
+ st.set_page_config(page_title="OpenClaw Ops Dashboard", layout="wide")
702
+ st.title("OpenClaw Ops Dashboard")
703
+ st.caption(f"Root: {OPENCLAW_ROOT}")
704
+
705
+ # Load Session Amplifier artifacts if available
706
+ sidecar_status = "unavailable"
707
+ sidecar_report = _load_sidecar_artifact("review-latest.json")
708
+ sidecar_skills = _load_sidecar_artifact("skills-latest.json")
709
+ if sidecar_report or sidecar_skills:
710
+ sidecar_status = "ok"
711
+ elif _sidecar_reachable():
712
+ sidecar_status = "reachable"
713
+
714
+ session_rows, session_lookup = session_index_rows()
715
+ activity_rows = transcript_activity_rows(session_lookup)
716
+ cron_rows = list_cron_jobs()
717
+ metrics = collect_metrics_last_24h()
718
+
719
+ running_cron = [row for row in cron_rows if row["runningAt"]]
720
+ subagent_rows = [row for row in session_rows if row["kind"] == "subagent"]
721
+
722
+ # Health summary
723
+ critical_jobs = [r for r in cron_rows if r["health"] == "critical"]
724
+ warning_jobs = [r for r in cron_rows if r["health"] == "warning"]
725
+ stale_jobs = [r for r in cron_rows if r["health"] == "stale"]
726
+
727
+ c1, c2, c3, c4, c5 = st.columns(5)
728
+ c1.metric("Agents", len({row["agent"] for row in session_rows}))
729
+ c2.metric("Indexed sessions", len(session_rows))
730
+ c3.metric("Running cron", len(running_cron))
731
+ c4.metric("Sidecar", sidecar_status.title(), help="Session Amplifier sidecar availability")
732
+ health_display = "✓" if not (critical_jobs or warning_jobs or stale_jobs) else f"⚠ {len(critical_jobs)}/{len(warning_jobs)}/{len(stale_jobs)}"
733
+ c5.metric("Health", health_display, help="Critical/Warning/Stale cron jobs")
734
+
735
+ tab_overview, tab_sessions, tab_cron, tab_activity, tab_live, tab_model_ops = st.tabs(
736
+ ["Overview", "Sessions", "Cron", "Activity", "Live Session", "Model Ops"]
737
+ )
738
+
739
+ with tab_overview:
740
+ left, right = st.columns([1, 1])
741
+ with left:
742
+ st.subheader("Active agent summaries (last 60m)")
743
+ if metrics["active_summaries"]:
744
+ st.dataframe(metrics["active_summaries"], use_container_width=True, hide_index=True)
745
+ else:
746
+ st.info("No active agent summaries in the last 60 minutes.")
747
+
748
+ st.subheader("Recent sessions")
749
+ if session_rows:
750
+ st.dataframe(
751
+ [
752
+ {
753
+ "agent": row["agent"],
754
+ "kind": row["kind"],
755
+ "updated": row["updated"],
756
+ "sessionKey": row["sessionKey"],
757
+ "deliveryTo": row["deliveryTo"],
758
+ }
759
+ for row in session_rows[:20]
760
+ ],
761
+ use_container_width=True,
762
+ hide_index=True,
763
+ )
764
+ else:
765
+ st.info("No indexed sessions found.")
766
+
767
+ with right:
768
+ st.subheader("Last 24h metrics")
769
+ m1, m2, m3, m4 = st.columns(4)
770
+ m1.metric("Error rate", f"{metrics['error_rate']:.2f}%")
771
+ m2.metric("Errors / events", f"{metrics['error_count']} / {metrics['total_events']}")
772
+
773
+ # Show tokens instead of cost (more reliable)
774
+ token_display = f"{metrics['token_total']:,}" if metrics['token_total'] else "0"
775
+ m3.metric("Tokens", token_display)
776
+ m4.metric("Cost", f"${metrics['cost_total']:.4f}")
777
+
778
+ st.caption(f"Input: {metrics['input_tokens']:,} | Output: {metrics['output_tokens']:,}")
779
+
780
+ if metrics["cache_hit_proxy"] is None:
781
+ st.caption("Cache-hit proxy unavailable in recent usage fields.")
782
+ else:
783
+ st.caption(f"Cache-hit proxy: {metrics['cache_hit_proxy']:.2f}% across {metrics['cache_samples']} samples.")
784
+
785
+ st.subheader("Top tools (last 24h)")
786
+ top_tools = [{"tool": tool, "count": count} for tool, count in metrics["tool_counts"].most_common(20)]
787
+ if top_tools:
788
+ st.dataframe(top_tools, use_container_width=True, hide_index=True)
789
+ else:
790
+ st.info("No tool invocation events found in the last 24 hours.")
791
+
792
+ # Session Amplifier findings
793
+ st.subheader("Session Amplifier")
794
+ if sidecar_report:
795
+ patterns = sidecar_report.get("failure_patterns", [])
796
+ if patterns:
797
+ st.warning(f"{len(patterns)} failure pattern(s) detected — see Activity tab for details")
798
+ for p in patterns[:5]:
799
+ st.caption(f"• {p.get('description', p.get('pattern', '?'))} ({p.get('count', 0)})")
800
+ else:
801
+ st.success("No failure patterns detected")
802
+ if sidecar_report.get("sessions_reviewed"):
803
+ st.caption(f"Reviewed {sidecar_report['sessions_reviewed']} sessions")
804
+ elif sidecar_status == "reachable":
805
+ st.info("Sidecar reachable but not yet warmed up — run spool to populate")
806
+ else:
807
+ st.caption("Session Amplifier unavailable — see docs to deploy sidecar/session-amplifier/")
808
+
809
+ if sidecar_skills:
810
+ missing = sidecar_skills.get("mcps_missing_skill_surface", [])
811
+ if missing:
812
+ st.warning(f"{len(missing)} MCP(s) without skill surface: {', '.join(missing[:5])}")
813
+ else:
814
+ st.caption("All registered MCPs have skill surfaces ✓")
815
+
816
+ with tab_sessions:
817
+ # Prefer sidecar session list when available
818
+ sidecar_sessions = _fetch_sidecar_json("/sessions/recent?limit=50")
819
+ if sidecar_sessions and sidecar_sessions.get("sessions"):
820
+ st.subheader("Recent sessions (Session Amplifier)")
821
+ st.caption(f"Fetched at {fmt_local(datetime.datetime.now(datetime.timezone.utc).isoformat())}")
822
+ sess_rows = []
823
+ for sr in sidecar_sessions["sessions"]:
824
+ health = sr.get("health", "ok")
825
+ hints = sr.get("hints", [])
826
+ hint_str = "; ".join(hints) if hints else ""
827
+ sess_rows.append({
828
+ "agent": sr.get("agent_id", "?"),
829
+ "session_id": sr.get("session_id", "?")[:32],
830
+ "events": sr.get("event_count", 0),
831
+ "tools": sr.get("tool_result_count", 0),
832
+ "errors": sr.get("error_count", 0),
833
+ "health": health.upper(),
834
+ "hints": hint_str,
835
+ "last_event": fmt_local(sr.get("last_event_at")),
836
+ })
837
+ st.dataframe(sess_rows, use_container_width=True, hide_index=True)
838
+ else:
839
+ st.subheader("Session index")
840
+ agent_names = ["all"] + sorted({row["agent"] for row in session_rows})
841
+ selected_agent = st.selectbox("Filter agent", agent_names, index=0)
842
+ selected_kind = st.selectbox("Filter kind", ["all", "session", "discord", "cron", "subagent"], index=0)
843
+ filtered_rows = []
844
+ for row in session_rows:
845
+ if selected_agent != "all" and row["agent"] != selected_agent:
846
+ continue
847
+ if selected_kind != "all" and row["kind"] != selected_kind:
848
+ continue
849
+ filtered_rows.append(
850
+ {
851
+ "agent": row["agent"],
852
+ "kind": row["kind"],
853
+ "updated": row["updated"],
854
+ "sessionKey": row["sessionKey"],
855
+ "sessionId": row["sessionId"],
856
+ "deliveryTo": row["deliveryTo"],
857
+ }
858
+ )
859
+ st.dataframe(filtered_rows, use_container_width=True, hide_index=True)
860
+
861
+ st.subheader("Recent child/subagent sessions")
862
+ st.dataframe(
863
+ [
864
+ {
865
+ "agent": row["agent"],
866
+ "updated": row["updated"],
867
+ "sessionKey": row["sessionKey"],
868
+ "sessionId": row["sessionId"],
869
+ }
870
+ for row in subagent_rows[:50]
871
+ ],
872
+ use_container_width=True,
873
+ hide_index=True,
874
+ )
875
+
876
+ with tab_cron:
877
+ # Health filter
878
+ health_filter = st.selectbox(
879
+ "Filter by health",
880
+ ["all", "critical", "error", "warning", "stale", "running", "healthy", "disabled"],
881
+ index=0
882
+ )
883
+
884
+ filtered_cron = cron_rows
885
+ if health_filter != "all":
886
+ filtered_cron = [r for r in cron_rows if r["health"] == health_filter]
887
+
888
+ st.subheader(f"Cron jobs ({len(filtered_cron)} shown)")
889
+
890
+ # Color-coded health display
891
+ def health_color(health):
892
+ return {
893
+ "critical": "🔴",
894
+ "error": "🟠",
895
+ "warning": "🟡",
896
+ "stale": "⚪",
897
+ "running": "🟢",
898
+ "healthy": "✓",
899
+ "disabled": "⊘",
900
+ }.get(health, "?")
901
+
902
+ display_rows = []
903
+ for row in filtered_cron:
904
+ display_rows.append({
905
+ "health": f"{health_color(row['health'])} {row['health']}",
906
+ "name": row["name"],
907
+ "enabled": "✓" if row["enabled"] else "✗",
908
+ "schedule": row["schedule"],
909
+ "lastRun": row["lastRun"],
910
+ "lastStatus": row["lastStatus"],
911
+ "nextRun": row["nextRun"],
912
+ "errors": row["consecutiveErrors"] if row["consecutiveErrors"] > 0 else "",
913
+ "reason": row["healthReason"],
914
+ })
915
+
916
+ st.dataframe(display_rows, use_container_width=True, hide_index=True)
917
+
918
+ # Quick stats
919
+ if critical_jobs or warning_jobs or stale_jobs:
920
+ st.error(f"**{len(critical_jobs)}** critical, **{len(warning_jobs)}** warning, **{len(stale_jobs)}** stale jobs need attention")
921
+ else:
922
+ st.success("All monitored cron jobs are healthy")
923
+
924
+ st.subheader("Running now")
925
+ if running_cron:
926
+ st.dataframe(running_cron, use_container_width=True, hide_index=True)
927
+ else:
928
+ st.info("No cron jobs currently marked running.")
929
+
930
+ with tab_activity:
931
+ st.subheader("Recent transcript activity")
932
+ st.dataframe(
933
+ [
934
+ {
935
+ "agent": row["agent"],
936
+ "updated": row["updated"],
937
+ "role": row["role"],
938
+ "sessionKey": row["sessionKey"],
939
+ "summary": row["summary"],
940
+ "file": row["file"],
941
+ }
942
+ for row in activity_rows[:50]
943
+ ],
944
+ use_container_width=True,
945
+ hide_index=True,
946
+ )
947
+
948
+ with tab_live:
949
+ st.subheader("Live Session Activity Stream")
950
+
951
+ # Prefer sidecar-based session list for selection when available
952
+ sidecar_sessions = _fetch_sidecar_json("/sessions/recent?limit=30") or {}
953
+ sidecar_session_list = sidecar_sessions.get("sessions") or []
954
+
955
+ # Show spooler health / readiness
956
+ if sidecar_status == "reachable":
957
+ st.info("Session Amplifier reachable but not yet warmed up — spool may be empty")
958
+ elif sidecar_status == "unavailable":
959
+ st.warning("Session Amplifier unavailable. Deploy: cd sidecar/session-amplifier && docker compose up -d")
960
+
961
+ st.caption("Normalized activity feed from Session Amplifier. "
962
+ "For continuous streaming, use: python scripts/session_amplifier_live_monitor.py")
963
+
964
+ # Build unified session options: sidecar sessions first, then fallback to dashboard sessions
965
+ if sidecar_session_list:
966
+ sidecar_opts = [
967
+ f"[SA] {s.get('agent_id','?')}: {clean_session_label(s.get('display_title') or '', fallback=s.get('session_id','?')[:32])}"
968
+ for s in sidecar_session_list[:20]
969
+ ]
970
+ sidecar_map = {opt: s for opt, s in zip(sidecar_opts, sidecar_session_list[:20])}
971
+ use_sidecar = True
972
+ else:
973
+ recent = [r for r in session_rows if r["updatedAt"] and r["updatedAt"] > datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(hours=24)]
974
+ sidecar_opts = []
975
+ sidecar_map = {}
976
+ use_sidecar = False
977
+
978
+ # Fallback: dashboard-based session list
979
+ recent_sessions = [r for r in session_rows if r["updatedAt"] and r["updatedAt"] > datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(hours=24)]
980
+ dash_opts = [f"{r['agent']}: {session_display_label(r)}" for r in recent_sessions[:20]]
981
+ dash_map = {opt: r for opt, r in zip(dash_opts, recent_sessions[:20])}
982
+
983
+ # Combine: sidecar sessions first, then dashboard sessions
984
+ all_opts = sidecar_opts + dash_opts
985
+ all_map = {**sidecar_map, **dash_map}
986
+
987
+ if all_opts:
988
+ selected = st.selectbox("Select session to monitor", all_opts, index=0)
989
+ selected_item = all_map[selected]
990
+
991
+ if use_sidecar and selected in sidecar_map:
992
+ sel_session_id = selected_item["session_id"]
993
+ sel_agent = selected_item["agent_id"]
994
+ sel_title = clean_session_label(selected_item.get("display_title") or "", fallback=sel_session_id)
995
+ sel_updated = fmt_local(selected_item.get("last_event_at"))
996
+ sel_health = selected_item.get("health", "ok").upper()
997
+ sel_hints = "; ".join(selected_item.get("hints", []) or [])
998
+ st.caption(f"Health: {sel_health} | Events: {selected_item.get('event_count',0)} | Tools: {selected_item.get('tool_result_count',0)} | Errors: {selected_item.get('error_count',0)}")
999
+ if sel_hints:
1000
+ st.caption(f"Hints: {sel_hints}")
1001
+
1002
+ # Fetch activity from sidecar
1003
+ activity_data = _fetch_sidecar_json(f"/session/{sel_session_id}/activity?limit=200")
1004
+ activities = activity_data.get("activity", []) if activity_data else []
1005
+ else:
1006
+ if isinstance(selected_item, dict) and "sessionKey" in selected_item:
1007
+ sel_session_id = selected_item["sessionKey"]
1008
+ sel_agent = selected_item["agent"]
1009
+ sel_title = session_display_label(selected_item)
1010
+ sel_updated = selected_item["updated"]
1011
+ else:
1012
+ sel_session_id = "?"
1013
+ sel_agent = "?"
1014
+ sel_title = "?"
1015
+ sel_updated = "?"
1016
+ st.caption(f"Dashboard session — may not appear in Session Amplifier spool")
1017
+ activities = get_live_session_activity(sel_session_id, sel_agent, max_lines=200)
1018
+
1019
+ col1, col2, col3 = st.columns([1, 1, 1])
1020
+ with col1:
1021
+ st.write(f"**Agent:** {sel_agent}")
1022
+ with col2:
1023
+ st.write(f"**Session:** {sel_title}")
1024
+ with col3:
1025
+ st.write(f"**Last event:** {sel_updated}")
1026
+
1027
+ if activities:
1028
+ current_bucket = None
1029
+ for act in reversed(activities[-50:]):
1030
+ ts = act.get("timestamp")
1031
+ if ts:
1032
+ try:
1033
+ bucket = ts.replace(minute=(ts.minute // 5) * 5, second=0, microsecond=0)
1034
+ if bucket != current_bucket:
1035
+ current_bucket = bucket
1036
+ st.divider()
1037
+ st.caption(f"📍 {fmt_local(bucket)}")
1038
+ except Exception:
1039
+ pass
1040
+
1041
+ evtype = act.get("type") or act.get("event_type", "")
1042
+ ts_str = act.get("time") or ""
1043
+
1044
+ if evtype == "thinking":
1045
+ with st.expander(f"🧠 {ts_str} — Thinking", expanded=False):
1046
+ st.text(act.get("details", "")[:1000] or "(no details)")
1047
+ elif evtype == "tool":
1048
+ st.markdown(f"**🔧 {ts_str}** — {act.get('summary', '')}")
1049
+ if act.get("details"):
1050
+ st.code(act["details"][:500], language="bash")
1051
+ elif evtype == "tool_call":
1052
+ st.markdown(f"**⚙ {ts_str}** → {act.get('tool_name', '')} — {act.get('summary', '')}")
1053
+ elif evtype == "tool_result" or evtype == "result":
1054
+ icon = "❌" if act.get("is_error") else "✅"
1055
+ st.markdown(f"**{icon} {ts_str}** — {act.get('summary', '')}")
1056
+ if act.get("details"):
1057
+ st.text(act["details"][:300])
1058
+ elif evtype == "tool_error":
1059
+ st.error(f"⚠ {ts_str} — {act.get('summary', 'tool error')}")
1060
+ if act.get("details"):
1061
+ st.text(act["details"][:300])
1062
+ elif evtype == "assistant_meta":
1063
+ st.markdown(f"**💡 {ts_str}** — {act.get('summary', '')[:120]}")
1064
+ elif evtype == "assistant_thinking":
1065
+ with st.expander(f"🧠 {ts_str} — Thinking", expanded=False):
1066
+ st.text(act.get("details", "")[:500] or "(no details)")
1067
+ elif evtype == "assistant_text":
1068
+ st.markdown(f"**💬 {ts_str}** — {act.get('summary', '')[:120]}")
1069
+ elif evtype == "user_message":
1070
+ with st.expander(f"👤 {ts_str} — User message", expanded=False):
1071
+ st.text(act.get("details", "")[:300])
1072
+ else:
1073
+ st.caption(f"[{evtype or '?'}] {ts_str} — {act.get('summary', '')[:80]}")
1074
+ else:
1075
+ st.info("No activity found for this session (may be archived or not yet written)")
1076
+ else:
1077
+ st.info("No recent sessions found in the last 24 hours")
1078
+
1079
+ with tab_model_ops:
1080
+ render_model_ops_tab(session_rows)
1081
+
1082
+
1083
+ if __name__ == "__main__":
1084
+ main()
docker-compose.yml ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ ops-dashboard:
5
+ image: ops-dashboard:latest
6
+ container_name: ops-dashboard
7
+ restart: unless-stopped
8
+ working_dir: /app
9
+ command: [ "streamlit", "run", "app.py" ]
10
+ volumes:
11
+ - ./app.py:/app/app.py
12
+ - ${OPENCLAW_HOME:-~/.openclaw}:/openclaw:ro
13
+ environment:
14
+ OPENCLAW_AGENTS_ROOT: /openclaw/agents
15
+ networks:
16
+ - librechat_default
17
+
18
+ ops-dashboard-react:
19
+ build:
20
+ context: ./react-dashboard
21
+ args:
22
+ VITE_BASE_PATH: /react/
23
+ image: ops-dashboard-react:latest
24
+ container_name: ops-dashboard-react
25
+ restart: unless-stopped
26
+ networks:
27
+ - librechat_default
28
+
29
+ networks:
30
+ librechat_default:
31
+ external: true
32
+ name: librechat_default
react-dashboard/.dockerignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ node_modules
2
+ dist
3
+ .git
4
+ .cache
5
+ npm-debug.log*
6
+
react-dashboard/.gitignore ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ node_modules/
2
+ dist/
3
+ .env
4
+ .env.*
react-dashboard/Dockerfile ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:22-alpine AS build
2
+
3
+ WORKDIR /app
4
+
5
+ COPY package*.json ./
6
+ RUN npm ci
7
+
8
+ COPY . .
9
+ ARG VITE_BASE_PATH=/react/
10
+ ENV VITE_BASE_PATH=$VITE_BASE_PATH
11
+ RUN npm run build
12
+
13
+ FROM nginx:1.27-alpine
14
+
15
+ COPY nginx.conf /etc/nginx/conf.d/default.conf
16
+ COPY --from=build /app/dist /usr/share/nginx/html
17
+
18
+ EXPOSE 8080
19
+
react-dashboard/README.md ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # OpenClaw Ops React Dashboard
2
+
3
+ Paperclip-style action dashboard prototype for Session Amplifier data.
4
+
5
+ ## Run locally
6
+
7
+ npm install
8
+ npm run dev -- --port 5177
9
+
10
+ The Vite dev server proxies /session-amplifier/* to SESSION_AMPLIFIER_BASE_URL or http://localhost:8477.
11
+
12
+ For browser access from another host, set the API URL directly:
13
+
14
+ VITE_SESSION_AMPLIFIER_BASE_URL=http://session-amplifier:8477 npm run dev -- --port 5177
15
+
16
+ ## Status
17
+
18
+ - Additive prototype beside the existing Streamlit dashboard.
19
+ - Uses /health, /sessions/active-bulk, and /review/skills.
20
+ - Run/pause/open controls are intentionally disabled until first-class action endpoints exist.
react-dashboard/index.html ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
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.0" />
6
+ <title>OpenClaw Ops</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.jsx"></script>
11
+ </body>
12
+ </html>
react-dashboard/nginx.conf ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ server {
2
+ listen 8080;
3
+ server_name _;
4
+ root /usr/share/nginx/html;
5
+ index index.html;
6
+
7
+ location /healthz {
8
+ access_log off;
9
+ add_header Content-Type text/plain;
10
+ return 200 "ok\n";
11
+ }
12
+
13
+ location / {
14
+ try_files $uri $uri/ /index.html;
15
+ }
16
+ }
17
+
react-dashboard/package-lock.json ADDED
@@ -0,0 +1,1673 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "openclaw-ops-react-dashboard",
3
+ "version": "0.1.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "openclaw-ops-react-dashboard",
9
+ "version": "0.1.0",
10
+ "dependencies": {
11
+ "@vitejs/plugin-react": "^5.0.4",
12
+ "lucide-react": "^0.468.0",
13
+ "react": "^19.2.0",
14
+ "react-dom": "^19.2.0",
15
+ "vite": "^7.2.4"
16
+ }
17
+ },
18
+ "node_modules/@babel/code-frame": {
19
+ "version": "7.29.0",
20
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
21
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
22
+ "license": "MIT",
23
+ "dependencies": {
24
+ "@babel/helper-validator-identifier": "^7.28.5",
25
+ "js-tokens": "^4.0.0",
26
+ "picocolors": "^1.1.1"
27
+ },
28
+ "engines": {
29
+ "node": ">=6.9.0"
30
+ }
31
+ },
32
+ "node_modules/@babel/compat-data": {
33
+ "version": "7.29.3",
34
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz",
35
+ "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==",
36
+ "license": "MIT",
37
+ "engines": {
38
+ "node": ">=6.9.0"
39
+ }
40
+ },
41
+ "node_modules/@babel/core": {
42
+ "version": "7.29.0",
43
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
44
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
45
+ "license": "MIT",
46
+ "dependencies": {
47
+ "@babel/code-frame": "^7.29.0",
48
+ "@babel/generator": "^7.29.0",
49
+ "@babel/helper-compilation-targets": "^7.28.6",
50
+ "@babel/helper-module-transforms": "^7.28.6",
51
+ "@babel/helpers": "^7.28.6",
52
+ "@babel/parser": "^7.29.0",
53
+ "@babel/template": "^7.28.6",
54
+ "@babel/traverse": "^7.29.0",
55
+ "@babel/types": "^7.29.0",
56
+ "@jridgewell/remapping": "^2.3.5",
57
+ "convert-source-map": "^2.0.0",
58
+ "debug": "^4.1.0",
59
+ "gensync": "^1.0.0-beta.2",
60
+ "json5": "^2.2.3",
61
+ "semver": "^6.3.1"
62
+ },
63
+ "engines": {
64
+ "node": ">=6.9.0"
65
+ },
66
+ "funding": {
67
+ "type": "opencollective",
68
+ "url": "https://opencollective.com/babel"
69
+ }
70
+ },
71
+ "node_modules/@babel/generator": {
72
+ "version": "7.29.1",
73
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
74
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
75
+ "license": "MIT",
76
+ "dependencies": {
77
+ "@babel/parser": "^7.29.0",
78
+ "@babel/types": "^7.29.0",
79
+ "@jridgewell/gen-mapping": "^0.3.12",
80
+ "@jridgewell/trace-mapping": "^0.3.28",
81
+ "jsesc": "^3.0.2"
82
+ },
83
+ "engines": {
84
+ "node": ">=6.9.0"
85
+ }
86
+ },
87
+ "node_modules/@babel/helper-compilation-targets": {
88
+ "version": "7.28.6",
89
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
90
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
91
+ "license": "MIT",
92
+ "dependencies": {
93
+ "@babel/compat-data": "^7.28.6",
94
+ "@babel/helper-validator-option": "^7.27.1",
95
+ "browserslist": "^4.24.0",
96
+ "lru-cache": "^5.1.1",
97
+ "semver": "^6.3.1"
98
+ },
99
+ "engines": {
100
+ "node": ">=6.9.0"
101
+ }
102
+ },
103
+ "node_modules/@babel/helper-globals": {
104
+ "version": "7.28.0",
105
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
106
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
107
+ "license": "MIT",
108
+ "engines": {
109
+ "node": ">=6.9.0"
110
+ }
111
+ },
112
+ "node_modules/@babel/helper-module-imports": {
113
+ "version": "7.28.6",
114
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
115
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
116
+ "license": "MIT",
117
+ "dependencies": {
118
+ "@babel/traverse": "^7.28.6",
119
+ "@babel/types": "^7.28.6"
120
+ },
121
+ "engines": {
122
+ "node": ">=6.9.0"
123
+ }
124
+ },
125
+ "node_modules/@babel/helper-module-transforms": {
126
+ "version": "7.28.6",
127
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
128
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
129
+ "license": "MIT",
130
+ "dependencies": {
131
+ "@babel/helper-module-imports": "^7.28.6",
132
+ "@babel/helper-validator-identifier": "^7.28.5",
133
+ "@babel/traverse": "^7.28.6"
134
+ },
135
+ "engines": {
136
+ "node": ">=6.9.0"
137
+ },
138
+ "peerDependencies": {
139
+ "@babel/core": "^7.0.0"
140
+ }
141
+ },
142
+ "node_modules/@babel/helper-plugin-utils": {
143
+ "version": "7.28.6",
144
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
145
+ "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
146
+ "license": "MIT",
147
+ "engines": {
148
+ "node": ">=6.9.0"
149
+ }
150
+ },
151
+ "node_modules/@babel/helper-string-parser": {
152
+ "version": "7.27.1",
153
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
154
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
155
+ "license": "MIT",
156
+ "engines": {
157
+ "node": ">=6.9.0"
158
+ }
159
+ },
160
+ "node_modules/@babel/helper-validator-identifier": {
161
+ "version": "7.28.5",
162
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
163
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
164
+ "license": "MIT",
165
+ "engines": {
166
+ "node": ">=6.9.0"
167
+ }
168
+ },
169
+ "node_modules/@babel/helper-validator-option": {
170
+ "version": "7.27.1",
171
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
172
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
173
+ "license": "MIT",
174
+ "engines": {
175
+ "node": ">=6.9.0"
176
+ }
177
+ },
178
+ "node_modules/@babel/helpers": {
179
+ "version": "7.29.2",
180
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
181
+ "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
182
+ "license": "MIT",
183
+ "dependencies": {
184
+ "@babel/template": "^7.28.6",
185
+ "@babel/types": "^7.29.0"
186
+ },
187
+ "engines": {
188
+ "node": ">=6.9.0"
189
+ }
190
+ },
191
+ "node_modules/@babel/parser": {
192
+ "version": "7.29.3",
193
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz",
194
+ "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==",
195
+ "license": "MIT",
196
+ "dependencies": {
197
+ "@babel/types": "^7.29.0"
198
+ },
199
+ "bin": {
200
+ "parser": "bin/babel-parser.js"
201
+ },
202
+ "engines": {
203
+ "node": ">=6.0.0"
204
+ }
205
+ },
206
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
207
+ "version": "7.27.1",
208
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
209
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
210
+ "license": "MIT",
211
+ "dependencies": {
212
+ "@babel/helper-plugin-utils": "^7.27.1"
213
+ },
214
+ "engines": {
215
+ "node": ">=6.9.0"
216
+ },
217
+ "peerDependencies": {
218
+ "@babel/core": "^7.0.0-0"
219
+ }
220
+ },
221
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
222
+ "version": "7.27.1",
223
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
224
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
225
+ "license": "MIT",
226
+ "dependencies": {
227
+ "@babel/helper-plugin-utils": "^7.27.1"
228
+ },
229
+ "engines": {
230
+ "node": ">=6.9.0"
231
+ },
232
+ "peerDependencies": {
233
+ "@babel/core": "^7.0.0-0"
234
+ }
235
+ },
236
+ "node_modules/@babel/template": {
237
+ "version": "7.28.6",
238
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
239
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
240
+ "license": "MIT",
241
+ "dependencies": {
242
+ "@babel/code-frame": "^7.28.6",
243
+ "@babel/parser": "^7.28.6",
244
+ "@babel/types": "^7.28.6"
245
+ },
246
+ "engines": {
247
+ "node": ">=6.9.0"
248
+ }
249
+ },
250
+ "node_modules/@babel/traverse": {
251
+ "version": "7.29.0",
252
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
253
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
254
+ "license": "MIT",
255
+ "dependencies": {
256
+ "@babel/code-frame": "^7.29.0",
257
+ "@babel/generator": "^7.29.0",
258
+ "@babel/helper-globals": "^7.28.0",
259
+ "@babel/parser": "^7.29.0",
260
+ "@babel/template": "^7.28.6",
261
+ "@babel/types": "^7.29.0",
262
+ "debug": "^4.3.1"
263
+ },
264
+ "engines": {
265
+ "node": ">=6.9.0"
266
+ }
267
+ },
268
+ "node_modules/@babel/types": {
269
+ "version": "7.29.0",
270
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
271
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
272
+ "license": "MIT",
273
+ "dependencies": {
274
+ "@babel/helper-string-parser": "^7.27.1",
275
+ "@babel/helper-validator-identifier": "^7.28.5"
276
+ },
277
+ "engines": {
278
+ "node": ">=6.9.0"
279
+ }
280
+ },
281
+ "node_modules/@esbuild/aix-ppc64": {
282
+ "version": "0.27.7",
283
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
284
+ "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
285
+ "cpu": [
286
+ "ppc64"
287
+ ],
288
+ "license": "MIT",
289
+ "optional": true,
290
+ "os": [
291
+ "aix"
292
+ ],
293
+ "engines": {
294
+ "node": ">=18"
295
+ }
296
+ },
297
+ "node_modules/@esbuild/android-arm": {
298
+ "version": "0.27.7",
299
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
300
+ "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
301
+ "cpu": [
302
+ "arm"
303
+ ],
304
+ "license": "MIT",
305
+ "optional": true,
306
+ "os": [
307
+ "android"
308
+ ],
309
+ "engines": {
310
+ "node": ">=18"
311
+ }
312
+ },
313
+ "node_modules/@esbuild/android-arm64": {
314
+ "version": "0.27.7",
315
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
316
+ "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
317
+ "cpu": [
318
+ "arm64"
319
+ ],
320
+ "license": "MIT",
321
+ "optional": true,
322
+ "os": [
323
+ "android"
324
+ ],
325
+ "engines": {
326
+ "node": ">=18"
327
+ }
328
+ },
329
+ "node_modules/@esbuild/android-x64": {
330
+ "version": "0.27.7",
331
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
332
+ "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
333
+ "cpu": [
334
+ "x64"
335
+ ],
336
+ "license": "MIT",
337
+ "optional": true,
338
+ "os": [
339
+ "android"
340
+ ],
341
+ "engines": {
342
+ "node": ">=18"
343
+ }
344
+ },
345
+ "node_modules/@esbuild/darwin-arm64": {
346
+ "version": "0.27.7",
347
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
348
+ "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
349
+ "cpu": [
350
+ "arm64"
351
+ ],
352
+ "license": "MIT",
353
+ "optional": true,
354
+ "os": [
355
+ "darwin"
356
+ ],
357
+ "engines": {
358
+ "node": ">=18"
359
+ }
360
+ },
361
+ "node_modules/@esbuild/darwin-x64": {
362
+ "version": "0.27.7",
363
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
364
+ "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
365
+ "cpu": [
366
+ "x64"
367
+ ],
368
+ "license": "MIT",
369
+ "optional": true,
370
+ "os": [
371
+ "darwin"
372
+ ],
373
+ "engines": {
374
+ "node": ">=18"
375
+ }
376
+ },
377
+ "node_modules/@esbuild/freebsd-arm64": {
378
+ "version": "0.27.7",
379
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
380
+ "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
381
+ "cpu": [
382
+ "arm64"
383
+ ],
384
+ "license": "MIT",
385
+ "optional": true,
386
+ "os": [
387
+ "freebsd"
388
+ ],
389
+ "engines": {
390
+ "node": ">=18"
391
+ }
392
+ },
393
+ "node_modules/@esbuild/freebsd-x64": {
394
+ "version": "0.27.7",
395
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
396
+ "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
397
+ "cpu": [
398
+ "x64"
399
+ ],
400
+ "license": "MIT",
401
+ "optional": true,
402
+ "os": [
403
+ "freebsd"
404
+ ],
405
+ "engines": {
406
+ "node": ">=18"
407
+ }
408
+ },
409
+ "node_modules/@esbuild/linux-arm": {
410
+ "version": "0.27.7",
411
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
412
+ "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
413
+ "cpu": [
414
+ "arm"
415
+ ],
416
+ "license": "MIT",
417
+ "optional": true,
418
+ "os": [
419
+ "linux"
420
+ ],
421
+ "engines": {
422
+ "node": ">=18"
423
+ }
424
+ },
425
+ "node_modules/@esbuild/linux-arm64": {
426
+ "version": "0.27.7",
427
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
428
+ "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
429
+ "cpu": [
430
+ "arm64"
431
+ ],
432
+ "license": "MIT",
433
+ "optional": true,
434
+ "os": [
435
+ "linux"
436
+ ],
437
+ "engines": {
438
+ "node": ">=18"
439
+ }
440
+ },
441
+ "node_modules/@esbuild/linux-ia32": {
442
+ "version": "0.27.7",
443
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
444
+ "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
445
+ "cpu": [
446
+ "ia32"
447
+ ],
448
+ "license": "MIT",
449
+ "optional": true,
450
+ "os": [
451
+ "linux"
452
+ ],
453
+ "engines": {
454
+ "node": ">=18"
455
+ }
456
+ },
457
+ "node_modules/@esbuild/linux-loong64": {
458
+ "version": "0.27.7",
459
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
460
+ "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
461
+ "cpu": [
462
+ "loong64"
463
+ ],
464
+ "license": "MIT",
465
+ "optional": true,
466
+ "os": [
467
+ "linux"
468
+ ],
469
+ "engines": {
470
+ "node": ">=18"
471
+ }
472
+ },
473
+ "node_modules/@esbuild/linux-mips64el": {
474
+ "version": "0.27.7",
475
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
476
+ "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
477
+ "cpu": [
478
+ "mips64el"
479
+ ],
480
+ "license": "MIT",
481
+ "optional": true,
482
+ "os": [
483
+ "linux"
484
+ ],
485
+ "engines": {
486
+ "node": ">=18"
487
+ }
488
+ },
489
+ "node_modules/@esbuild/linux-ppc64": {
490
+ "version": "0.27.7",
491
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
492
+ "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
493
+ "cpu": [
494
+ "ppc64"
495
+ ],
496
+ "license": "MIT",
497
+ "optional": true,
498
+ "os": [
499
+ "linux"
500
+ ],
501
+ "engines": {
502
+ "node": ">=18"
503
+ }
504
+ },
505
+ "node_modules/@esbuild/linux-riscv64": {
506
+ "version": "0.27.7",
507
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
508
+ "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
509
+ "cpu": [
510
+ "riscv64"
511
+ ],
512
+ "license": "MIT",
513
+ "optional": true,
514
+ "os": [
515
+ "linux"
516
+ ],
517
+ "engines": {
518
+ "node": ">=18"
519
+ }
520
+ },
521
+ "node_modules/@esbuild/linux-s390x": {
522
+ "version": "0.27.7",
523
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
524
+ "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
525
+ "cpu": [
526
+ "s390x"
527
+ ],
528
+ "license": "MIT",
529
+ "optional": true,
530
+ "os": [
531
+ "linux"
532
+ ],
533
+ "engines": {
534
+ "node": ">=18"
535
+ }
536
+ },
537
+ "node_modules/@esbuild/linux-x64": {
538
+ "version": "0.27.7",
539
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
540
+ "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
541
+ "cpu": [
542
+ "x64"
543
+ ],
544
+ "license": "MIT",
545
+ "optional": true,
546
+ "os": [
547
+ "linux"
548
+ ],
549
+ "engines": {
550
+ "node": ">=18"
551
+ }
552
+ },
553
+ "node_modules/@esbuild/netbsd-arm64": {
554
+ "version": "0.27.7",
555
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
556
+ "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
557
+ "cpu": [
558
+ "arm64"
559
+ ],
560
+ "license": "MIT",
561
+ "optional": true,
562
+ "os": [
563
+ "netbsd"
564
+ ],
565
+ "engines": {
566
+ "node": ">=18"
567
+ }
568
+ },
569
+ "node_modules/@esbuild/netbsd-x64": {
570
+ "version": "0.27.7",
571
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
572
+ "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
573
+ "cpu": [
574
+ "x64"
575
+ ],
576
+ "license": "MIT",
577
+ "optional": true,
578
+ "os": [
579
+ "netbsd"
580
+ ],
581
+ "engines": {
582
+ "node": ">=18"
583
+ }
584
+ },
585
+ "node_modules/@esbuild/openbsd-arm64": {
586
+ "version": "0.27.7",
587
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
588
+ "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
589
+ "cpu": [
590
+ "arm64"
591
+ ],
592
+ "license": "MIT",
593
+ "optional": true,
594
+ "os": [
595
+ "openbsd"
596
+ ],
597
+ "engines": {
598
+ "node": ">=18"
599
+ }
600
+ },
601
+ "node_modules/@esbuild/openbsd-x64": {
602
+ "version": "0.27.7",
603
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
604
+ "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
605
+ "cpu": [
606
+ "x64"
607
+ ],
608
+ "license": "MIT",
609
+ "optional": true,
610
+ "os": [
611
+ "openbsd"
612
+ ],
613
+ "engines": {
614
+ "node": ">=18"
615
+ }
616
+ },
617
+ "node_modules/@esbuild/openharmony-arm64": {
618
+ "version": "0.27.7",
619
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
620
+ "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
621
+ "cpu": [
622
+ "arm64"
623
+ ],
624
+ "license": "MIT",
625
+ "optional": true,
626
+ "os": [
627
+ "openharmony"
628
+ ],
629
+ "engines": {
630
+ "node": ">=18"
631
+ }
632
+ },
633
+ "node_modules/@esbuild/sunos-x64": {
634
+ "version": "0.27.7",
635
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
636
+ "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
637
+ "cpu": [
638
+ "x64"
639
+ ],
640
+ "license": "MIT",
641
+ "optional": true,
642
+ "os": [
643
+ "sunos"
644
+ ],
645
+ "engines": {
646
+ "node": ">=18"
647
+ }
648
+ },
649
+ "node_modules/@esbuild/win32-arm64": {
650
+ "version": "0.27.7",
651
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
652
+ "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
653
+ "cpu": [
654
+ "arm64"
655
+ ],
656
+ "license": "MIT",
657
+ "optional": true,
658
+ "os": [
659
+ "win32"
660
+ ],
661
+ "engines": {
662
+ "node": ">=18"
663
+ }
664
+ },
665
+ "node_modules/@esbuild/win32-ia32": {
666
+ "version": "0.27.7",
667
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
668
+ "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
669
+ "cpu": [
670
+ "ia32"
671
+ ],
672
+ "license": "MIT",
673
+ "optional": true,
674
+ "os": [
675
+ "win32"
676
+ ],
677
+ "engines": {
678
+ "node": ">=18"
679
+ }
680
+ },
681
+ "node_modules/@esbuild/win32-x64": {
682
+ "version": "0.27.7",
683
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
684
+ "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
685
+ "cpu": [
686
+ "x64"
687
+ ],
688
+ "license": "MIT",
689
+ "optional": true,
690
+ "os": [
691
+ "win32"
692
+ ],
693
+ "engines": {
694
+ "node": ">=18"
695
+ }
696
+ },
697
+ "node_modules/@jridgewell/gen-mapping": {
698
+ "version": "0.3.13",
699
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
700
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
701
+ "license": "MIT",
702
+ "dependencies": {
703
+ "@jridgewell/sourcemap-codec": "^1.5.0",
704
+ "@jridgewell/trace-mapping": "^0.3.24"
705
+ }
706
+ },
707
+ "node_modules/@jridgewell/remapping": {
708
+ "version": "2.3.5",
709
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
710
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
711
+ "license": "MIT",
712
+ "dependencies": {
713
+ "@jridgewell/gen-mapping": "^0.3.5",
714
+ "@jridgewell/trace-mapping": "^0.3.24"
715
+ }
716
+ },
717
+ "node_modules/@jridgewell/resolve-uri": {
718
+ "version": "3.1.2",
719
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
720
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
721
+ "license": "MIT",
722
+ "engines": {
723
+ "node": ">=6.0.0"
724
+ }
725
+ },
726
+ "node_modules/@jridgewell/sourcemap-codec": {
727
+ "version": "1.5.5",
728
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
729
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
730
+ "license": "MIT"
731
+ },
732
+ "node_modules/@jridgewell/trace-mapping": {
733
+ "version": "0.3.31",
734
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
735
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
736
+ "license": "MIT",
737
+ "dependencies": {
738
+ "@jridgewell/resolve-uri": "^3.1.0",
739
+ "@jridgewell/sourcemap-codec": "^1.4.14"
740
+ }
741
+ },
742
+ "node_modules/@rolldown/pluginutils": {
743
+ "version": "1.0.0-rc.3",
744
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
745
+ "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==",
746
+ "license": "MIT"
747
+ },
748
+ "node_modules/@rollup/rollup-android-arm-eabi": {
749
+ "version": "4.60.4",
750
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz",
751
+ "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==",
752
+ "cpu": [
753
+ "arm"
754
+ ],
755
+ "license": "MIT",
756
+ "optional": true,
757
+ "os": [
758
+ "android"
759
+ ]
760
+ },
761
+ "node_modules/@rollup/rollup-android-arm64": {
762
+ "version": "4.60.4",
763
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz",
764
+ "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==",
765
+ "cpu": [
766
+ "arm64"
767
+ ],
768
+ "license": "MIT",
769
+ "optional": true,
770
+ "os": [
771
+ "android"
772
+ ]
773
+ },
774
+ "node_modules/@rollup/rollup-darwin-arm64": {
775
+ "version": "4.60.4",
776
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz",
777
+ "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==",
778
+ "cpu": [
779
+ "arm64"
780
+ ],
781
+ "license": "MIT",
782
+ "optional": true,
783
+ "os": [
784
+ "darwin"
785
+ ]
786
+ },
787
+ "node_modules/@rollup/rollup-darwin-x64": {
788
+ "version": "4.60.4",
789
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz",
790
+ "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==",
791
+ "cpu": [
792
+ "x64"
793
+ ],
794
+ "license": "MIT",
795
+ "optional": true,
796
+ "os": [
797
+ "darwin"
798
+ ]
799
+ },
800
+ "node_modules/@rollup/rollup-freebsd-arm64": {
801
+ "version": "4.60.4",
802
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz",
803
+ "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==",
804
+ "cpu": [
805
+ "arm64"
806
+ ],
807
+ "license": "MIT",
808
+ "optional": true,
809
+ "os": [
810
+ "freebsd"
811
+ ]
812
+ },
813
+ "node_modules/@rollup/rollup-freebsd-x64": {
814
+ "version": "4.60.4",
815
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz",
816
+ "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==",
817
+ "cpu": [
818
+ "x64"
819
+ ],
820
+ "license": "MIT",
821
+ "optional": true,
822
+ "os": [
823
+ "freebsd"
824
+ ]
825
+ },
826
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
827
+ "version": "4.60.4",
828
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz",
829
+ "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==",
830
+ "cpu": [
831
+ "arm"
832
+ ],
833
+ "license": "MIT",
834
+ "optional": true,
835
+ "os": [
836
+ "linux"
837
+ ]
838
+ },
839
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
840
+ "version": "4.60.4",
841
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz",
842
+ "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==",
843
+ "cpu": [
844
+ "arm"
845
+ ],
846
+ "license": "MIT",
847
+ "optional": true,
848
+ "os": [
849
+ "linux"
850
+ ]
851
+ },
852
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
853
+ "version": "4.60.4",
854
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz",
855
+ "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==",
856
+ "cpu": [
857
+ "arm64"
858
+ ],
859
+ "license": "MIT",
860
+ "optional": true,
861
+ "os": [
862
+ "linux"
863
+ ]
864
+ },
865
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
866
+ "version": "4.60.4",
867
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz",
868
+ "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==",
869
+ "cpu": [
870
+ "arm64"
871
+ ],
872
+ "license": "MIT",
873
+ "optional": true,
874
+ "os": [
875
+ "linux"
876
+ ]
877
+ },
878
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
879
+ "version": "4.60.4",
880
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz",
881
+ "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==",
882
+ "cpu": [
883
+ "loong64"
884
+ ],
885
+ "license": "MIT",
886
+ "optional": true,
887
+ "os": [
888
+ "linux"
889
+ ]
890
+ },
891
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
892
+ "version": "4.60.4",
893
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz",
894
+ "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==",
895
+ "cpu": [
896
+ "loong64"
897
+ ],
898
+ "license": "MIT",
899
+ "optional": true,
900
+ "os": [
901
+ "linux"
902
+ ]
903
+ },
904
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
905
+ "version": "4.60.4",
906
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz",
907
+ "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==",
908
+ "cpu": [
909
+ "ppc64"
910
+ ],
911
+ "license": "MIT",
912
+ "optional": true,
913
+ "os": [
914
+ "linux"
915
+ ]
916
+ },
917
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
918
+ "version": "4.60.4",
919
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz",
920
+ "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==",
921
+ "cpu": [
922
+ "ppc64"
923
+ ],
924
+ "license": "MIT",
925
+ "optional": true,
926
+ "os": [
927
+ "linux"
928
+ ]
929
+ },
930
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
931
+ "version": "4.60.4",
932
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz",
933
+ "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==",
934
+ "cpu": [
935
+ "riscv64"
936
+ ],
937
+ "license": "MIT",
938
+ "optional": true,
939
+ "os": [
940
+ "linux"
941
+ ]
942
+ },
943
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
944
+ "version": "4.60.4",
945
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz",
946
+ "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==",
947
+ "cpu": [
948
+ "riscv64"
949
+ ],
950
+ "license": "MIT",
951
+ "optional": true,
952
+ "os": [
953
+ "linux"
954
+ ]
955
+ },
956
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
957
+ "version": "4.60.4",
958
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz",
959
+ "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==",
960
+ "cpu": [
961
+ "s390x"
962
+ ],
963
+ "license": "MIT",
964
+ "optional": true,
965
+ "os": [
966
+ "linux"
967
+ ]
968
+ },
969
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
970
+ "version": "4.60.4",
971
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz",
972
+ "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==",
973
+ "cpu": [
974
+ "x64"
975
+ ],
976
+ "license": "MIT",
977
+ "optional": true,
978
+ "os": [
979
+ "linux"
980
+ ]
981
+ },
982
+ "node_modules/@rollup/rollup-linux-x64-musl": {
983
+ "version": "4.60.4",
984
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz",
985
+ "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==",
986
+ "cpu": [
987
+ "x64"
988
+ ],
989
+ "license": "MIT",
990
+ "optional": true,
991
+ "os": [
992
+ "linux"
993
+ ]
994
+ },
995
+ "node_modules/@rollup/rollup-openbsd-x64": {
996
+ "version": "4.60.4",
997
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz",
998
+ "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==",
999
+ "cpu": [
1000
+ "x64"
1001
+ ],
1002
+ "license": "MIT",
1003
+ "optional": true,
1004
+ "os": [
1005
+ "openbsd"
1006
+ ]
1007
+ },
1008
+ "node_modules/@rollup/rollup-openharmony-arm64": {
1009
+ "version": "4.60.4",
1010
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz",
1011
+ "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==",
1012
+ "cpu": [
1013
+ "arm64"
1014
+ ],
1015
+ "license": "MIT",
1016
+ "optional": true,
1017
+ "os": [
1018
+ "openharmony"
1019
+ ]
1020
+ },
1021
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
1022
+ "version": "4.60.4",
1023
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz",
1024
+ "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==",
1025
+ "cpu": [
1026
+ "arm64"
1027
+ ],
1028
+ "license": "MIT",
1029
+ "optional": true,
1030
+ "os": [
1031
+ "win32"
1032
+ ]
1033
+ },
1034
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
1035
+ "version": "4.60.4",
1036
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz",
1037
+ "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==",
1038
+ "cpu": [
1039
+ "ia32"
1040
+ ],
1041
+ "license": "MIT",
1042
+ "optional": true,
1043
+ "os": [
1044
+ "win32"
1045
+ ]
1046
+ },
1047
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
1048
+ "version": "4.60.4",
1049
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz",
1050
+ "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==",
1051
+ "cpu": [
1052
+ "x64"
1053
+ ],
1054
+ "license": "MIT",
1055
+ "optional": true,
1056
+ "os": [
1057
+ "win32"
1058
+ ]
1059
+ },
1060
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
1061
+ "version": "4.60.4",
1062
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz",
1063
+ "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==",
1064
+ "cpu": [
1065
+ "x64"
1066
+ ],
1067
+ "license": "MIT",
1068
+ "optional": true,
1069
+ "os": [
1070
+ "win32"
1071
+ ]
1072
+ },
1073
+ "node_modules/@types/babel__core": {
1074
+ "version": "7.20.5",
1075
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
1076
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
1077
+ "license": "MIT",
1078
+ "dependencies": {
1079
+ "@babel/parser": "^7.20.7",
1080
+ "@babel/types": "^7.20.7",
1081
+ "@types/babel__generator": "*",
1082
+ "@types/babel__template": "*",
1083
+ "@types/babel__traverse": "*"
1084
+ }
1085
+ },
1086
+ "node_modules/@types/babel__generator": {
1087
+ "version": "7.27.0",
1088
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
1089
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
1090
+ "license": "MIT",
1091
+ "dependencies": {
1092
+ "@babel/types": "^7.0.0"
1093
+ }
1094
+ },
1095
+ "node_modules/@types/babel__template": {
1096
+ "version": "7.4.4",
1097
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
1098
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
1099
+ "license": "MIT",
1100
+ "dependencies": {
1101
+ "@babel/parser": "^7.1.0",
1102
+ "@babel/types": "^7.0.0"
1103
+ }
1104
+ },
1105
+ "node_modules/@types/babel__traverse": {
1106
+ "version": "7.28.0",
1107
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
1108
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
1109
+ "license": "MIT",
1110
+ "dependencies": {
1111
+ "@babel/types": "^7.28.2"
1112
+ }
1113
+ },
1114
+ "node_modules/@types/estree": {
1115
+ "version": "1.0.8",
1116
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
1117
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
1118
+ "license": "MIT"
1119
+ },
1120
+ "node_modules/@vitejs/plugin-react": {
1121
+ "version": "5.2.0",
1122
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz",
1123
+ "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==",
1124
+ "license": "MIT",
1125
+ "dependencies": {
1126
+ "@babel/core": "^7.29.0",
1127
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
1128
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
1129
+ "@rolldown/pluginutils": "1.0.0-rc.3",
1130
+ "@types/babel__core": "^7.20.5",
1131
+ "react-refresh": "^0.18.0"
1132
+ },
1133
+ "engines": {
1134
+ "node": "^20.19.0 || >=22.12.0"
1135
+ },
1136
+ "peerDependencies": {
1137
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
1138
+ }
1139
+ },
1140
+ "node_modules/baseline-browser-mapping": {
1141
+ "version": "2.10.31",
1142
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz",
1143
+ "integrity": "sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==",
1144
+ "license": "Apache-2.0",
1145
+ "bin": {
1146
+ "baseline-browser-mapping": "dist/cli.cjs"
1147
+ },
1148
+ "engines": {
1149
+ "node": ">=6.0.0"
1150
+ }
1151
+ },
1152
+ "node_modules/browserslist": {
1153
+ "version": "4.28.2",
1154
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
1155
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
1156
+ "funding": [
1157
+ {
1158
+ "type": "opencollective",
1159
+ "url": "https://opencollective.com/browserslist"
1160
+ },
1161
+ {
1162
+ "type": "tidelift",
1163
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1164
+ },
1165
+ {
1166
+ "type": "github",
1167
+ "url": "https://github.com/sponsors/ai"
1168
+ }
1169
+ ],
1170
+ "license": "MIT",
1171
+ "dependencies": {
1172
+ "baseline-browser-mapping": "^2.10.12",
1173
+ "caniuse-lite": "^1.0.30001782",
1174
+ "electron-to-chromium": "^1.5.328",
1175
+ "node-releases": "^2.0.36",
1176
+ "update-browserslist-db": "^1.2.3"
1177
+ },
1178
+ "bin": {
1179
+ "browserslist": "cli.js"
1180
+ },
1181
+ "engines": {
1182
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
1183
+ }
1184
+ },
1185
+ "node_modules/caniuse-lite": {
1186
+ "version": "1.0.30001793",
1187
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz",
1188
+ "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==",
1189
+ "funding": [
1190
+ {
1191
+ "type": "opencollective",
1192
+ "url": "https://opencollective.com/browserslist"
1193
+ },
1194
+ {
1195
+ "type": "tidelift",
1196
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
1197
+ },
1198
+ {
1199
+ "type": "github",
1200
+ "url": "https://github.com/sponsors/ai"
1201
+ }
1202
+ ],
1203
+ "license": "CC-BY-4.0"
1204
+ },
1205
+ "node_modules/convert-source-map": {
1206
+ "version": "2.0.0",
1207
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
1208
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
1209
+ "license": "MIT"
1210
+ },
1211
+ "node_modules/debug": {
1212
+ "version": "4.4.3",
1213
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
1214
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
1215
+ "license": "MIT",
1216
+ "dependencies": {
1217
+ "ms": "^2.1.3"
1218
+ },
1219
+ "engines": {
1220
+ "node": ">=6.0"
1221
+ },
1222
+ "peerDependenciesMeta": {
1223
+ "supports-color": {
1224
+ "optional": true
1225
+ }
1226
+ }
1227
+ },
1228
+ "node_modules/electron-to-chromium": {
1229
+ "version": "1.5.359",
1230
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.359.tgz",
1231
+ "integrity": "sha512-8lPELWuYZIWk7NDvCNthtmMw/7Q5Wu25NpM4djFMHBmk8DubPAtL4YTOp7ou0e7HyJtwkVlWv8XMLURnrtgJQw==",
1232
+ "license": "ISC"
1233
+ },
1234
+ "node_modules/esbuild": {
1235
+ "version": "0.27.7",
1236
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
1237
+ "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
1238
+ "hasInstallScript": true,
1239
+ "license": "MIT",
1240
+ "bin": {
1241
+ "esbuild": "bin/esbuild"
1242
+ },
1243
+ "engines": {
1244
+ "node": ">=18"
1245
+ },
1246
+ "optionalDependencies": {
1247
+ "@esbuild/aix-ppc64": "0.27.7",
1248
+ "@esbuild/android-arm": "0.27.7",
1249
+ "@esbuild/android-arm64": "0.27.7",
1250
+ "@esbuild/android-x64": "0.27.7",
1251
+ "@esbuild/darwin-arm64": "0.27.7",
1252
+ "@esbuild/darwin-x64": "0.27.7",
1253
+ "@esbuild/freebsd-arm64": "0.27.7",
1254
+ "@esbuild/freebsd-x64": "0.27.7",
1255
+ "@esbuild/linux-arm": "0.27.7",
1256
+ "@esbuild/linux-arm64": "0.27.7",
1257
+ "@esbuild/linux-ia32": "0.27.7",
1258
+ "@esbuild/linux-loong64": "0.27.7",
1259
+ "@esbuild/linux-mips64el": "0.27.7",
1260
+ "@esbuild/linux-ppc64": "0.27.7",
1261
+ "@esbuild/linux-riscv64": "0.27.7",
1262
+ "@esbuild/linux-s390x": "0.27.7",
1263
+ "@esbuild/linux-x64": "0.27.7",
1264
+ "@esbuild/netbsd-arm64": "0.27.7",
1265
+ "@esbuild/netbsd-x64": "0.27.7",
1266
+ "@esbuild/openbsd-arm64": "0.27.7",
1267
+ "@esbuild/openbsd-x64": "0.27.7",
1268
+ "@esbuild/openharmony-arm64": "0.27.7",
1269
+ "@esbuild/sunos-x64": "0.27.7",
1270
+ "@esbuild/win32-arm64": "0.27.7",
1271
+ "@esbuild/win32-ia32": "0.27.7",
1272
+ "@esbuild/win32-x64": "0.27.7"
1273
+ }
1274
+ },
1275
+ "node_modules/escalade": {
1276
+ "version": "3.2.0",
1277
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
1278
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
1279
+ "license": "MIT",
1280
+ "engines": {
1281
+ "node": ">=6"
1282
+ }
1283
+ },
1284
+ "node_modules/fdir": {
1285
+ "version": "6.5.0",
1286
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
1287
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
1288
+ "license": "MIT",
1289
+ "engines": {
1290
+ "node": ">=12.0.0"
1291
+ },
1292
+ "peerDependencies": {
1293
+ "picomatch": "^3 || ^4"
1294
+ },
1295
+ "peerDependenciesMeta": {
1296
+ "picomatch": {
1297
+ "optional": true
1298
+ }
1299
+ }
1300
+ },
1301
+ "node_modules/fsevents": {
1302
+ "version": "2.3.3",
1303
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1304
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1305
+ "hasInstallScript": true,
1306
+ "license": "MIT",
1307
+ "optional": true,
1308
+ "os": [
1309
+ "darwin"
1310
+ ],
1311
+ "engines": {
1312
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1313
+ }
1314
+ },
1315
+ "node_modules/gensync": {
1316
+ "version": "1.0.0-beta.2",
1317
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
1318
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
1319
+ "license": "MIT",
1320
+ "engines": {
1321
+ "node": ">=6.9.0"
1322
+ }
1323
+ },
1324
+ "node_modules/js-tokens": {
1325
+ "version": "4.0.0",
1326
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
1327
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
1328
+ "license": "MIT"
1329
+ },
1330
+ "node_modules/jsesc": {
1331
+ "version": "3.1.0",
1332
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
1333
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
1334
+ "license": "MIT",
1335
+ "bin": {
1336
+ "jsesc": "bin/jsesc"
1337
+ },
1338
+ "engines": {
1339
+ "node": ">=6"
1340
+ }
1341
+ },
1342
+ "node_modules/json5": {
1343
+ "version": "2.2.3",
1344
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
1345
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
1346
+ "license": "MIT",
1347
+ "bin": {
1348
+ "json5": "lib/cli.js"
1349
+ },
1350
+ "engines": {
1351
+ "node": ">=6"
1352
+ }
1353
+ },
1354
+ "node_modules/lru-cache": {
1355
+ "version": "5.1.1",
1356
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
1357
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
1358
+ "license": "ISC",
1359
+ "dependencies": {
1360
+ "yallist": "^3.0.2"
1361
+ }
1362
+ },
1363
+ "node_modules/lucide-react": {
1364
+ "version": "0.468.0",
1365
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.468.0.tgz",
1366
+ "integrity": "sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==",
1367
+ "license": "ISC",
1368
+ "peerDependencies": {
1369
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc"
1370
+ }
1371
+ },
1372
+ "node_modules/ms": {
1373
+ "version": "2.1.3",
1374
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1375
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1376
+ "license": "MIT"
1377
+ },
1378
+ "node_modules/nanoid": {
1379
+ "version": "3.3.12",
1380
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
1381
+ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
1382
+ "funding": [
1383
+ {
1384
+ "type": "github",
1385
+ "url": "https://github.com/sponsors/ai"
1386
+ }
1387
+ ],
1388
+ "license": "MIT",
1389
+ "bin": {
1390
+ "nanoid": "bin/nanoid.cjs"
1391
+ },
1392
+ "engines": {
1393
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1394
+ }
1395
+ },
1396
+ "node_modules/node-releases": {
1397
+ "version": "2.0.44",
1398
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz",
1399
+ "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==",
1400
+ "license": "MIT"
1401
+ },
1402
+ "node_modules/picocolors": {
1403
+ "version": "1.1.1",
1404
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1405
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1406
+ "license": "ISC"
1407
+ },
1408
+ "node_modules/picomatch": {
1409
+ "version": "4.0.4",
1410
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
1411
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
1412
+ "license": "MIT",
1413
+ "engines": {
1414
+ "node": ">=12"
1415
+ },
1416
+ "funding": {
1417
+ "url": "https://github.com/sponsors/jonschlinkert"
1418
+ }
1419
+ },
1420
+ "node_modules/postcss": {
1421
+ "version": "8.5.15",
1422
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
1423
+ "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
1424
+ "funding": [
1425
+ {
1426
+ "type": "opencollective",
1427
+ "url": "https://opencollective.com/postcss/"
1428
+ },
1429
+ {
1430
+ "type": "tidelift",
1431
+ "url": "https://tidelift.com/funding/github/npm/postcss"
1432
+ },
1433
+ {
1434
+ "type": "github",
1435
+ "url": "https://github.com/sponsors/ai"
1436
+ }
1437
+ ],
1438
+ "license": "MIT",
1439
+ "dependencies": {
1440
+ "nanoid": "^3.3.12",
1441
+ "picocolors": "^1.1.1",
1442
+ "source-map-js": "^1.2.1"
1443
+ },
1444
+ "engines": {
1445
+ "node": "^10 || ^12 || >=14"
1446
+ }
1447
+ },
1448
+ "node_modules/react": {
1449
+ "version": "19.2.6",
1450
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz",
1451
+ "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==",
1452
+ "license": "MIT",
1453
+ "engines": {
1454
+ "node": ">=0.10.0"
1455
+ }
1456
+ },
1457
+ "node_modules/react-dom": {
1458
+ "version": "19.2.6",
1459
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz",
1460
+ "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==",
1461
+ "license": "MIT",
1462
+ "dependencies": {
1463
+ "scheduler": "^0.27.0"
1464
+ },
1465
+ "peerDependencies": {
1466
+ "react": "^19.2.6"
1467
+ }
1468
+ },
1469
+ "node_modules/react-refresh": {
1470
+ "version": "0.18.0",
1471
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
1472
+ "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
1473
+ "license": "MIT",
1474
+ "engines": {
1475
+ "node": ">=0.10.0"
1476
+ }
1477
+ },
1478
+ "node_modules/rollup": {
1479
+ "version": "4.60.4",
1480
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz",
1481
+ "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==",
1482
+ "license": "MIT",
1483
+ "dependencies": {
1484
+ "@types/estree": "1.0.8"
1485
+ },
1486
+ "bin": {
1487
+ "rollup": "dist/bin/rollup"
1488
+ },
1489
+ "engines": {
1490
+ "node": ">=18.0.0",
1491
+ "npm": ">=8.0.0"
1492
+ },
1493
+ "optionalDependencies": {
1494
+ "@rollup/rollup-android-arm-eabi": "4.60.4",
1495
+ "@rollup/rollup-android-arm64": "4.60.4",
1496
+ "@rollup/rollup-darwin-arm64": "4.60.4",
1497
+ "@rollup/rollup-darwin-x64": "4.60.4",
1498
+ "@rollup/rollup-freebsd-arm64": "4.60.4",
1499
+ "@rollup/rollup-freebsd-x64": "4.60.4",
1500
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.4",
1501
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.4",
1502
+ "@rollup/rollup-linux-arm64-gnu": "4.60.4",
1503
+ "@rollup/rollup-linux-arm64-musl": "4.60.4",
1504
+ "@rollup/rollup-linux-loong64-gnu": "4.60.4",
1505
+ "@rollup/rollup-linux-loong64-musl": "4.60.4",
1506
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.4",
1507
+ "@rollup/rollup-linux-ppc64-musl": "4.60.4",
1508
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.4",
1509
+ "@rollup/rollup-linux-riscv64-musl": "4.60.4",
1510
+ "@rollup/rollup-linux-s390x-gnu": "4.60.4",
1511
+ "@rollup/rollup-linux-x64-gnu": "4.60.4",
1512
+ "@rollup/rollup-linux-x64-musl": "4.60.4",
1513
+ "@rollup/rollup-openbsd-x64": "4.60.4",
1514
+ "@rollup/rollup-openharmony-arm64": "4.60.4",
1515
+ "@rollup/rollup-win32-arm64-msvc": "4.60.4",
1516
+ "@rollup/rollup-win32-ia32-msvc": "4.60.4",
1517
+ "@rollup/rollup-win32-x64-gnu": "4.60.4",
1518
+ "@rollup/rollup-win32-x64-msvc": "4.60.4",
1519
+ "fsevents": "~2.3.2"
1520
+ }
1521
+ },
1522
+ "node_modules/scheduler": {
1523
+ "version": "0.27.0",
1524
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
1525
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
1526
+ "license": "MIT"
1527
+ },
1528
+ "node_modules/semver": {
1529
+ "version": "6.3.1",
1530
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
1531
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
1532
+ "license": "ISC",
1533
+ "bin": {
1534
+ "semver": "bin/semver.js"
1535
+ }
1536
+ },
1537
+ "node_modules/source-map-js": {
1538
+ "version": "1.2.1",
1539
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
1540
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
1541
+ "license": "BSD-3-Clause",
1542
+ "engines": {
1543
+ "node": ">=0.10.0"
1544
+ }
1545
+ },
1546
+ "node_modules/tinyglobby": {
1547
+ "version": "0.2.16",
1548
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
1549
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
1550
+ "license": "MIT",
1551
+ "dependencies": {
1552
+ "fdir": "^6.5.0",
1553
+ "picomatch": "^4.0.4"
1554
+ },
1555
+ "engines": {
1556
+ "node": ">=12.0.0"
1557
+ },
1558
+ "funding": {
1559
+ "url": "https://github.com/sponsors/SuperchupuDev"
1560
+ }
1561
+ },
1562
+ "node_modules/update-browserslist-db": {
1563
+ "version": "1.2.3",
1564
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
1565
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
1566
+ "funding": [
1567
+ {
1568
+ "type": "opencollective",
1569
+ "url": "https://opencollective.com/browserslist"
1570
+ },
1571
+ {
1572
+ "type": "tidelift",
1573
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1574
+ },
1575
+ {
1576
+ "type": "github",
1577
+ "url": "https://github.com/sponsors/ai"
1578
+ }
1579
+ ],
1580
+ "license": "MIT",
1581
+ "dependencies": {
1582
+ "escalade": "^3.2.0",
1583
+ "picocolors": "^1.1.1"
1584
+ },
1585
+ "bin": {
1586
+ "update-browserslist-db": "cli.js"
1587
+ },
1588
+ "peerDependencies": {
1589
+ "browserslist": ">= 4.21.0"
1590
+ }
1591
+ },
1592
+ "node_modules/vite": {
1593
+ "version": "7.3.3",
1594
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.3.tgz",
1595
+ "integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==",
1596
+ "license": "MIT",
1597
+ "dependencies": {
1598
+ "esbuild": "^0.27.0",
1599
+ "fdir": "^6.5.0",
1600
+ "picomatch": "^4.0.3",
1601
+ "postcss": "^8.5.6",
1602
+ "rollup": "^4.43.0",
1603
+ "tinyglobby": "^0.2.15"
1604
+ },
1605
+ "bin": {
1606
+ "vite": "bin/vite.js"
1607
+ },
1608
+ "engines": {
1609
+ "node": "^20.19.0 || >=22.12.0"
1610
+ },
1611
+ "funding": {
1612
+ "url": "https://github.com/vitejs/vite?sponsor=1"
1613
+ },
1614
+ "optionalDependencies": {
1615
+ "fsevents": "~2.3.3"
1616
+ },
1617
+ "peerDependencies": {
1618
+ "@types/node": "^20.19.0 || >=22.12.0",
1619
+ "jiti": ">=1.21.0",
1620
+ "less": "^4.0.0",
1621
+ "lightningcss": "^1.21.0",
1622
+ "sass": "^1.70.0",
1623
+ "sass-embedded": "^1.70.0",
1624
+ "stylus": ">=0.54.8",
1625
+ "sugarss": "^5.0.0",
1626
+ "terser": "^5.16.0",
1627
+ "tsx": "^4.8.1",
1628
+ "yaml": "^2.4.2"
1629
+ },
1630
+ "peerDependenciesMeta": {
1631
+ "@types/node": {
1632
+ "optional": true
1633
+ },
1634
+ "jiti": {
1635
+ "optional": true
1636
+ },
1637
+ "less": {
1638
+ "optional": true
1639
+ },
1640
+ "lightningcss": {
1641
+ "optional": true
1642
+ },
1643
+ "sass": {
1644
+ "optional": true
1645
+ },
1646
+ "sass-embedded": {
1647
+ "optional": true
1648
+ },
1649
+ "stylus": {
1650
+ "optional": true
1651
+ },
1652
+ "sugarss": {
1653
+ "optional": true
1654
+ },
1655
+ "terser": {
1656
+ "optional": true
1657
+ },
1658
+ "tsx": {
1659
+ "optional": true
1660
+ },
1661
+ "yaml": {
1662
+ "optional": true
1663
+ }
1664
+ }
1665
+ },
1666
+ "node_modules/yallist": {
1667
+ "version": "3.1.1",
1668
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
1669
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
1670
+ "license": "ISC"
1671
+ }
1672
+ }
1673
+ }
react-dashboard/package.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"name":"openclaw-ops-react-dashboard","version":"0.1.0","private":false,"type":"module","scripts":{"dev":"vite --host 0.0.0.0","build":"vite build","preview":"vite preview --host 0.0.0.0","test":"npm run build"},"dependencies":{"@vitejs/plugin-react":"^5.0.4","vite":"^7.2.4","react":"^19.2.0","react-dom":"^19.2.0","lucide-react":"^0.468.0"}}
react-dashboard/src/App.jsx ADDED
@@ -0,0 +1,310 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useMemo, useState } from 'react';
2
+ import {
3
+ Activity,
4
+ AlertTriangle,
5
+ Bot,
6
+ CheckCircle2,
7
+ CirclePause,
8
+ CirclePlay,
9
+ ClipboardCheck,
10
+ ExternalLink,
11
+ RefreshCw,
12
+ ShieldCheck,
13
+ Sparkles,
14
+ TerminalSquare,
15
+ } from 'lucide-react';
16
+
17
+ const API_BASE = import.meta.env.VITE_SESSION_AMPLIFIER_BASE_URL || '/session-amplifier';
18
+ const POLL_MS = 15000;
19
+
20
+ async function fetchJson(path) {
21
+ const response = await fetch(API_BASE + path);
22
+ if (!response.ok) {
23
+ throw new Error(response.status + ' ' + response.statusText);
24
+ }
25
+ return response.json();
26
+ }
27
+
28
+ function formatAge(value) {
29
+ if (!value) return 'unknown';
30
+ const at = new Date(value);
31
+ if (Number.isNaN(at.getTime())) return 'unknown';
32
+ const seconds = Math.max(0, Math.floor((Date.now() - at.getTime()) / 1000));
33
+ if (seconds < 60) return seconds + 's';
34
+ const minutes = Math.floor(seconds / 60);
35
+ if (minutes < 60) return minutes + 'm';
36
+ const hours = Math.floor(minutes / 60);
37
+ if (hours < 36) return hours + 'h';
38
+ return Math.floor(hours / 24) + 'd';
39
+ }
40
+
41
+ function compactText(value, limit = 110) {
42
+ const text = String(value || '').replace(/\s+/g, ' ').trim();
43
+ if (!text) return '';
44
+ return text.length > limit ? text.slice(0, limit - 1).trimEnd() + '…' : text;
45
+ }
46
+
47
+ function looksLikeOpaqueId(value) {
48
+ return /^[0-9a-f]{8}-[0-9a-f-]{27,}$/i.test(String(value || '').trim());
49
+ }
50
+
51
+ function firstMeaningfulEvent(events = []) {
52
+ return events.find((event) => {
53
+ const text = event?.details || event?.summary || event?.clean_text || event?.preview || '';
54
+ return text && text !== 'assistant' && (event.role === 'user' || event.event_type === 'user_message');
55
+ }) || events.find((event) => {
56
+ const text = event?.details || event?.summary || event?.clean_text || event?.preview || '';
57
+ return text && text !== 'assistant';
58
+ });
59
+ }
60
+
61
+ function sessionTextLabel(event) {
62
+ if (!event) return '';
63
+ const raw = event.details || event.summary || event.clean_text || event.preview || '';
64
+ const firstLine = String(raw).split('\n').find((line) => line.trim()) || '';
65
+ const cronMatch = firstLine.match(/^\[cron:[^\s\]]+\s+([^\]]+)\]\s*(.*)$/);
66
+ if (cronMatch) {
67
+ return compactText(cronMatch[1] + ': ' + cronMatch[2], 96);
68
+ }
69
+ return compactText(firstLine.replace(/^#+\s*/, ''), 96);
70
+ }
71
+
72
+ function quotedHint(event) {
73
+ if (!event) return '';
74
+ const raw = event.details || event.summary || event.clean_text || event.preview || '';
75
+ const quote = String(raw).match(/["“']([^"”']{8,130})["”']/);
76
+ if (quote?.[1]) return compactText('"' + quote[1] + '"', 120);
77
+ const label = sessionTextLabel(event);
78
+ return label ? compactText('"' + label + '"', 120) : '';
79
+ }
80
+
81
+ function shortSessionId(session) {
82
+ return String(session.session_id || '').slice(0, 8) || 'unknown';
83
+ }
84
+
85
+ function pickTitle(session, events = []) {
86
+ const configuredTitle = (
87
+ session.display_title ||
88
+ session.displayTitle ||
89
+ session.display_name ||
90
+ session.displayName ||
91
+ session.origin_label
92
+ );
93
+ if (configuredTitle && !looksLikeOpaqueId(configuredTitle)) {
94
+ return { title: configuredTitle, hint: quotedHint(firstMeaningfulEvent(events)) };
95
+ }
96
+ const firstEvent = firstMeaningfulEvent(events);
97
+ const derivedTitle = sessionTextLabel(firstEvent);
98
+ return {
99
+ title: derivedTitle || 'Session ' + shortSessionId(session),
100
+ hint: quotedHint(firstEvent),
101
+ };
102
+ }
103
+
104
+ function deriveState(session, events = []) {
105
+ if (session.derived_state) return session.derived_state;
106
+ if (session.health === 'error') return 'error';
107
+ const latest = [...events].reverse().find(Boolean);
108
+ if (!latest) return 'idle';
109
+ if (latest.is_error) return 'error';
110
+ if (latest.tool_name) return 'active';
111
+ if ((latest.preview || latest.clean_text || '').toLowerCase().includes('permission')) return 'waiting';
112
+ return latest.role === 'assistant' || latest.role === 'user' ? 'active' : 'idle';
113
+ }
114
+
115
+ function stateMeta(state, needsPermission) {
116
+ if (needsPermission || state === 'waiting') {
117
+ return { label: 'Waiting', tone: 'waiting', Icon: CirclePause };
118
+ }
119
+ if (state === 'active') return { label: 'Active', tone: 'active', Icon: Activity };
120
+ if (state === 'error') return { label: 'Error', tone: 'error', Icon: AlertTriangle };
121
+ return { label: 'Idle', tone: 'idle', Icon: CheckCircle2 };
122
+ }
123
+
124
+ function MetricCard({ icon: Icon, label, value, sublabel, tone = 'neutral' }) {
125
+ return (
126
+ <section className={'metric metric--' + tone}>
127
+ <div className="metric__icon"><Icon size={18} /></div>
128
+ <div>
129
+ <div className="metric__value">{value}</div>
130
+ <div className="metric__label">{label}</div>
131
+ {sublabel ? <div className="metric__sublabel">{sublabel}</div> : null}
132
+ </div>
133
+ </section>
134
+ );
135
+ }
136
+
137
+ function SessionCard({ session, events }) {
138
+ const state = deriveState(session, events);
139
+ const meta = stateMeta(state, session.needs_permission);
140
+ const latest = [...events].reverse().find(Boolean);
141
+ const currentTool = session.current_tool_name || latest?.tool_name || 'none';
142
+ const { title, hint } = pickTitle(session, events);
143
+
144
+ return (
145
+ <article className={'session-card session-card--' + meta.tone}>
146
+ <header className="session-card__head">
147
+ <div className="session-card__identity">
148
+ <span className={'status-pill status-pill--' + meta.tone}>
149
+ <meta.Icon size={14} />
150
+ {meta.label}
151
+ </span>
152
+ <h2 title={title}>{title}</h2>
153
+ {hint ? <p className="session-card__quote">{hint}</p> : null}
154
+ </div>
155
+ <span className="session-card__age">{formatAge(session.last_activity_at || session.updated_at || latest?.timestamp)}</span>
156
+ </header>
157
+
158
+ <div className="session-card__meta">
159
+ <span><Bot size={14} />{session.agent_id || 'unknown'}</span>
160
+ <span><TerminalSquare size={14} />{currentTool}</span>
161
+ <span><Activity size={14} />{shortSessionId(session)} · {events.length} events</span>
162
+ </div>
163
+
164
+ <p className="session-card__preview">
165
+ {latest?.preview || latest?.clean_text || session.active_reason || 'No recent transcript preview'}
166
+ </p>
167
+
168
+ <footer className="session-card__actions">
169
+ <button type="button" title="Open transcript when a route is wired" disabled>
170
+ <ExternalLink size={15} />
171
+ Open
172
+ </button>
173
+ <button type="button" title="Run control requires an action endpoint" disabled>
174
+ <CirclePlay size={15} />
175
+ Run
176
+ </button>
177
+ <button type="button" title="Pause/resume requires an action endpoint" disabled>
178
+ <CirclePause size={15} />
179
+ Pause
180
+ </button>
181
+ </footer>
182
+ </article>
183
+ );
184
+ }
185
+
186
+ function App() {
187
+ const [health, setHealth] = useState(null);
188
+ const [bulk, setBulk] = useState({ sessions: [], activity: {} });
189
+ const [skills, setSkills] = useState(null);
190
+ const [error, setError] = useState('');
191
+ const [loading, setLoading] = useState(true);
192
+
193
+ async function refresh() {
194
+ setError('');
195
+ try {
196
+ const results = await Promise.allSettled([
197
+ fetchJson('/health'),
198
+ fetchJson('/sessions/active-bulk?limit=30&activity_limit=80'),
199
+ fetchJson('/review/skills'),
200
+ ]);
201
+ const [healthResult, bulkResult, skillsResult] = results;
202
+ if (healthResult.status === 'fulfilled') setHealth(healthResult.value);
203
+ if (bulkResult.status === 'fulfilled') setBulk(bulkResult.value);
204
+ if (skillsResult.status === 'fulfilled') setSkills(skillsResult.value);
205
+ const rejected = [healthResult, bulkResult].find((result) => result.status === 'rejected');
206
+ if (rejected) throw rejected.reason;
207
+ } catch (err) {
208
+ setError(err.message || String(err));
209
+ } finally {
210
+ setLoading(false);
211
+ }
212
+ }
213
+
214
+ useEffect(() => {
215
+ refresh();
216
+ const timer = window.setInterval(refresh, POLL_MS);
217
+ return () => window.clearInterval(timer);
218
+ }, []);
219
+
220
+ const sessions = bulk.sessions || [];
221
+ const activity = bulk.activity || {};
222
+ const metrics = useMemo(() => {
223
+ const states = sessions.map((session) => deriveState(session, activity[session.session_id] || []));
224
+ const active = states.filter((state) => state === 'active').length;
225
+ const waiting = sessions.filter((session, index) => session.needs_permission || states[index] === 'waiting').length;
226
+ const errors = states.filter((state) => state === 'error').length;
227
+ const toolHeavy = sessions.filter((session) => (activity[session.session_id] || []).filter((row) => row.tool_name).length >= 5).length;
228
+ const candidateCount = Array.isArray(skills?.candidates) ? skills.candidates.length : Array.isArray(skills?.recommendations) ? skills.recommendations.length : 0;
229
+ return { active, waiting, errors, toolHeavy, candidateCount };
230
+ }, [sessions, activity, skills]);
231
+
232
+ return (
233
+ <main className="app-shell">
234
+ <header className="topbar">
235
+ <div>
236
+ <p className="eyebrow">OpenClaw Ops</p>
237
+ <h1>Action Dashboard</h1>
238
+ </div>
239
+ <button className="refresh-button" type="button" onClick={refresh}>
240
+ <RefreshCw size={16} className={loading ? 'spin' : ''} />
241
+ Refresh
242
+ </button>
243
+ </header>
244
+
245
+ {error ? (
246
+ <section className="alert">
247
+ <AlertTriangle size={16} />
248
+ <span>{error}</span>
249
+ </section>
250
+ ) : null}
251
+
252
+ <section className="metrics-grid">
253
+ <MetricCard icon={ShieldCheck} label="Amplifier" value={health?.status || 'unknown'} sublabel={(health?.sessions ?? 0) + ' indexed sessions'} tone={health?.status === 'ok' ? 'good' : 'warn'} />
254
+ <MetricCard icon={Activity} label="Active" value={metrics.active} sublabel={sessions.length + ' recent sessions'} tone="info" />
255
+ <MetricCard icon={CirclePause} label="Waiting" value={metrics.waiting} sublabel="permission or stalled state" tone={metrics.waiting ? 'warn' : 'neutral'} />
256
+ <MetricCard icon={AlertTriangle} label="Errors" value={metrics.errors} sublabel="recent session state" tone={metrics.errors ? 'bad' : 'neutral'} />
257
+ <MetricCard icon={TerminalSquare} label="Tool Heavy" value={metrics.toolHeavy} sublabel="5+ tool events" tone="neutral" />
258
+ <MetricCard icon={ClipboardCheck} label="Skill Signals" value={metrics.candidateCount || 'review'} sublabel="from skill review API" tone="neutral" />
259
+ </section>
260
+
261
+ <section className="workspace-grid">
262
+ <section className="panel panel--wide">
263
+ <div className="panel__head">
264
+ <h2>Live Sessions</h2>
265
+ <span>{bulk.generated_at ? 'Updated ' + formatAge(bulk.generated_at) + ' ago' : 'Not loaded'}</span>
266
+ </div>
267
+ <div className="sessions-grid">
268
+ {sessions.length ? (
269
+ sessions.map((session) => (
270
+ <SessionCard
271
+ key={session.session_id}
272
+ session={session}
273
+ events={activity[session.session_id] || []}
274
+ />
275
+ ))
276
+ ) : (
277
+ <div className="empty-state">
278
+ <Sparkles size={18} />
279
+ <span>No recent sessions returned.</span>
280
+ </div>
281
+ )}
282
+ </div>
283
+ </section>
284
+
285
+ <aside className="panel">
286
+ <div className="panel__head">
287
+ <h2>Promotion Lane</h2>
288
+ <span>advisory</span>
289
+ </div>
290
+ <div className="lane-list">
291
+ <div className="lane-row">
292
+ <ShieldCheck size={16} />
293
+ <span>Installed skills may be used automatically when the match is safe.</span>
294
+ </div>
295
+ <div className="lane-row">
296
+ <ClipboardCheck size={16} />
297
+ <span>New installs, agent allowlists, and config changes stay approval-gated.</span>
298
+ </div>
299
+ <div className="lane-row">
300
+ <ExternalLink size={16} />
301
+ <span>Approval packets are summarized by scripts/openclaw_skill_approval_status.py.</span>
302
+ </div>
303
+ </div>
304
+ </aside>
305
+ </section>
306
+ </main>
307
+ );
308
+ }
309
+
310
+ export default App;
react-dashboard/src/main.jsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import App from './App.jsx';
4
+ import './styles.css';
5
+
6
+ createRoot(document.getElementById('root')).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>,
10
+ );
react-dashboard/src/styles.css ADDED
@@ -0,0 +1,331 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ color-scheme: dark;
3
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
4
+ background: #111318;
5
+ color: #edf1f7;
6
+ font-synthesis: none;
7
+ text-rendering: optimizeLegibility;
8
+ }
9
+
10
+ * { box-sizing: border-box; }
11
+
12
+ body {
13
+ margin: 0;
14
+ min-width: 320px;
15
+ background: #111318;
16
+ }
17
+
18
+ button { font: inherit; }
19
+
20
+ .app-shell {
21
+ width: min(1480px, calc(100vw - 32px));
22
+ margin: 0 auto;
23
+ padding: 24px 0 36px;
24
+ }
25
+
26
+ .topbar {
27
+ display: flex;
28
+ align-items: center;
29
+ justify-content: space-between;
30
+ gap: 16px;
31
+ margin-bottom: 20px;
32
+ }
33
+
34
+ .eyebrow {
35
+ margin: 0 0 4px;
36
+ color: #9aa8bb;
37
+ font-size: 0.78rem;
38
+ font-weight: 700;
39
+ letter-spacing: 0;
40
+ text-transform: uppercase;
41
+ }
42
+
43
+ h1, h2, p { margin: 0; }
44
+
45
+ h1 {
46
+ font-size: clamp(1.7rem, 3vw, 2.35rem);
47
+ line-height: 1.05;
48
+ letter-spacing: 0;
49
+ }
50
+
51
+ .refresh-button,
52
+ .session-card__actions button {
53
+ display: inline-flex;
54
+ align-items: center;
55
+ justify-content: center;
56
+ gap: 7px;
57
+ min-height: 36px;
58
+ border: 1px solid #303847;
59
+ border-radius: 7px;
60
+ background: #191d25;
61
+ color: #edf1f7;
62
+ cursor: pointer;
63
+ }
64
+
65
+ .refresh-button { padding: 0 14px; }
66
+
67
+ .session-card__actions button {
68
+ width: 100%;
69
+ padding: 0 10px;
70
+ }
71
+
72
+ .session-card__actions button:disabled {
73
+ cursor: not-allowed;
74
+ color: #7f8ca0;
75
+ background: #151922;
76
+ }
77
+
78
+ .alert {
79
+ display: flex;
80
+ align-items: center;
81
+ gap: 10px;
82
+ min-height: 42px;
83
+ margin-bottom: 16px;
84
+ padding: 0 12px;
85
+ border: 1px solid #8a5630;
86
+ border-radius: 7px;
87
+ background: #231a14;
88
+ color: #ffbf80;
89
+ }
90
+
91
+ .metrics-grid {
92
+ display: grid;
93
+ grid-template-columns: repeat(6, minmax(0, 1fr));
94
+ gap: 12px;
95
+ margin-bottom: 16px;
96
+ }
97
+
98
+ .metric,
99
+ .panel,
100
+ .session-card {
101
+ border: 1px solid #2a313f;
102
+ border-radius: 8px;
103
+ background: #171b23;
104
+ }
105
+
106
+ .metric {
107
+ display: grid;
108
+ grid-template-columns: 32px minmax(0, 1fr);
109
+ gap: 10px;
110
+ min-height: 96px;
111
+ padding: 14px;
112
+ }
113
+
114
+ .metric__icon {
115
+ display: grid;
116
+ width: 32px;
117
+ height: 32px;
118
+ place-items: center;
119
+ border-radius: 7px;
120
+ background: #202733;
121
+ color: #9fb9ff;
122
+ }
123
+
124
+ .metric__value {
125
+ font-size: 1.45rem;
126
+ font-weight: 800;
127
+ line-height: 1.05;
128
+ }
129
+
130
+ .metric__label {
131
+ margin-top: 4px;
132
+ color: #d6deeb;
133
+ font-weight: 700;
134
+ }
135
+
136
+ .metric__sublabel {
137
+ margin-top: 5px;
138
+ color: #8e9caf;
139
+ font-size: 0.82rem;
140
+ line-height: 1.25;
141
+ }
142
+
143
+ .metric--good .metric__icon,
144
+ .status-pill--idle { color: #6ee7a8; }
145
+
146
+ .metric--warn .metric__icon,
147
+ .status-pill--waiting { color: #ffd166; }
148
+
149
+ .metric--bad .metric__icon,
150
+ .status-pill--error { color: #ff8f8f; }
151
+
152
+ .metric--info .metric__icon,
153
+ .status-pill--active { color: #8ec7ff; }
154
+
155
+ .workspace-grid {
156
+ display: grid;
157
+ grid-template-columns: minmax(0, 1fr) 360px;
158
+ gap: 16px;
159
+ align-items: start;
160
+ }
161
+
162
+ .panel { padding: 16px; }
163
+
164
+ .panel__head {
165
+ display: flex;
166
+ align-items: center;
167
+ justify-content: space-between;
168
+ gap: 12px;
169
+ margin-bottom: 14px;
170
+ }
171
+
172
+ .panel__head h2,
173
+ .session-card h2 {
174
+ font-size: 1rem;
175
+ letter-spacing: 0;
176
+ }
177
+
178
+ .panel__head span,
179
+ .session-card__age {
180
+ color: #8794a8;
181
+ font-size: 0.82rem;
182
+ white-space: nowrap;
183
+ }
184
+
185
+ .sessions-grid {
186
+ display: grid;
187
+ grid-template-columns: repeat(2, minmax(0, 1fr));
188
+ gap: 12px;
189
+ }
190
+
191
+ .session-card {
192
+ display: flex;
193
+ min-height: 214px;
194
+ flex-direction: column;
195
+ padding: 14px;
196
+ }
197
+
198
+ .session-card--active { border-color: #315c88; }
199
+ .session-card--waiting { border-color: #816632; }
200
+ .session-card--error { border-color: #8a3f3f; }
201
+
202
+ .session-card__head {
203
+ display: flex;
204
+ justify-content: space-between;
205
+ gap: 12px;
206
+ margin-bottom: 12px;
207
+ }
208
+
209
+ .session-card__identity { min-width: 0; }
210
+
211
+ .session-card h2 {
212
+ margin-top: 8px;
213
+ overflow: hidden;
214
+ text-overflow: ellipsis;
215
+ white-space: nowrap;
216
+ }
217
+
218
+ .session-card__quote {
219
+ margin-top: 5px;
220
+ overflow: hidden;
221
+ color: #9faabd;
222
+ font-size: 0.82rem;
223
+ line-height: 1.3;
224
+ text-overflow: ellipsis;
225
+ white-space: nowrap;
226
+ }
227
+
228
+ .status-pill {
229
+ display: inline-flex;
230
+ align-items: center;
231
+ gap: 6px;
232
+ min-height: 24px;
233
+ padding: 0 8px;
234
+ border-radius: 999px;
235
+ background: #202733;
236
+ font-size: 0.78rem;
237
+ font-weight: 800;
238
+ }
239
+
240
+ .session-card__meta {
241
+ display: grid;
242
+ grid-template-columns: repeat(3, minmax(0, 1fr));
243
+ gap: 8px;
244
+ margin-bottom: 12px;
245
+ }
246
+
247
+ .session-card__meta span {
248
+ display: inline-flex;
249
+ min-width: 0;
250
+ align-items: center;
251
+ gap: 6px;
252
+ overflow: hidden;
253
+ color: #a9b5c7;
254
+ font-size: 0.82rem;
255
+ text-overflow: ellipsis;
256
+ white-space: nowrap;
257
+ }
258
+
259
+ .session-card__preview {
260
+ display: -webkit-box;
261
+ flex: 1;
262
+ overflow: hidden;
263
+ color: #c4ccda;
264
+ font-size: 0.9rem;
265
+ line-height: 1.42;
266
+ -webkit-box-orient: vertical;
267
+ -webkit-line-clamp: 3;
268
+ }
269
+
270
+ .session-card__actions {
271
+ display: grid;
272
+ grid-template-columns: repeat(3, minmax(0, 1fr));
273
+ gap: 8px;
274
+ margin-top: 14px;
275
+ }
276
+
277
+ .lane-list { display: grid; gap: 10px; }
278
+
279
+ .lane-row,
280
+ .empty-state {
281
+ display: flex;
282
+ align-items: center;
283
+ gap: 10px;
284
+ color: #c4ccda;
285
+ line-height: 1.35;
286
+ }
287
+
288
+ .lane-row {
289
+ min-height: 46px;
290
+ padding: 10px;
291
+ border: 1px solid #2a313f;
292
+ border-radius: 7px;
293
+ background: #141820;
294
+ }
295
+
296
+ .empty-state {
297
+ min-height: 130px;
298
+ justify-content: center;
299
+ border: 1px dashed #364154;
300
+ border-radius: 8px;
301
+ }
302
+
303
+ .spin { animation: spin 0.9s linear infinite; }
304
+
305
+ @keyframes spin {
306
+ from { transform: rotate(0deg); }
307
+ to { transform: rotate(360deg); }
308
+ }
309
+
310
+ @media (max-width: 1120px) {
311
+ .metrics-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
312
+ .workspace-grid { grid-template-columns: 1fr; }
313
+ }
314
+
315
+ @media (max-width: 760px) {
316
+ .app-shell {
317
+ width: min(100vw - 20px, 720px);
318
+ padding-top: 16px;
319
+ }
320
+
321
+ .topbar {
322
+ align-items: flex-start;
323
+ flex-direction: column;
324
+ }
325
+
326
+ .metrics-grid,
327
+ .sessions-grid { grid-template-columns: 1fr; }
328
+
329
+ .session-card__meta,
330
+ .session-card__actions { grid-template-columns: 1fr; }
331
+ }
react-dashboard/vite.config.js ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ base: process.env.VITE_BASE_PATH || '/',
7
+ server: {
8
+ port: 5177,
9
+ strictPort: false,
10
+ proxy: {
11
+ '/session-amplifier': {
12
+ target: process.env.SESSION_AMPLIFIER_BASE_URL || 'http://localhost:8477',
13
+ changeOrigin: true,
14
+ rewrite: (path) => path.replace(/^\/session-amplifier/, ''),
15
+ },
16
+ },
17
+ },
18
+ });
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ streamlit==1.26.0
2
+