Executor-Tyrant-Framework commited on
Commit
b4efd7d
·
1 Parent(s): 081db53

feat: BLK-GUI-198 spec — NeuroGraph GUI 2.0 Phase 2 Dashboard tab

Browse files
Files changed (1) hide show
  1. specs/BLK-GUI-198.json +230 -0
specs/BLK-GUI-198.json ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "spec_version": "1.0.0",
3
+ "block": {
4
+ "id": "BLK-GUI-198",
5
+ "name": "NeuroGraph GUI 2.0 — Phase 2: Ecosystem Dashboard Tab",
6
+ "agent": "QB",
7
+ "scope": "NeuroGraph — add Dashboard tab to neurograph_gui.py with 5 auto-refreshing ecosystem panels",
8
+ "acceptance_criteria": [
9
+ "EcosystemPoller class present in neurograph_gui.py",
10
+ "self.ecosystem_poller initialized in NeuroGraphGUI.__init__",
11
+ "_build_dashboard_tab() called in _build_tabs() after _build_modules_tab()",
12
+ "Dashboard tab renders with 5 LabelFrame panels: Syl/NeuroGraph, TID Routing, Bunyan Narrative, Elmer Health, THC Repairs",
13
+ "_refresh_dashboard(), _apply_dashboard_panel(), _format_panel_data(), _start_dashboard_autorefresh() methods present",
14
+ "python3 -m py_compile neurograph_work/neurograph_gui.py passes with exit code 0",
15
+ "Changes committed and pushed to NeuroGraph repo"
16
+ ],
17
+ "depends_on": [
18
+ "BLK-GUI-197"
19
+ ]
20
+ },
21
+ "snap_interface": {
22
+ "inputs": [
23
+ {
24
+ "name": "neurograph_gui.py",
25
+ "type": "file",
26
+ "source_block": "BLK-GUI-197",
27
+ "path": "neurograph_work/neurograph_gui.py"
28
+ }
29
+ ],
30
+ "outputs": [
31
+ {
32
+ "name": "neurograph_gui.py",
33
+ "type": "file",
34
+ "path": "neurograph_work/neurograph_gui.py",
35
+ "contract": "Dashboard tab added; EcosystemPoller class present; 5 panels wired; py_compile passes"
36
+ }
37
+ ]
38
+ },
39
+ "constraints": {
40
+ "never": [
41
+ "Do not modify any vendored file: ng_lite.py, ng_peer_bridge.py, ng_tract_bridge.py, ng_ecosystem.py, openclaw_adapter.py, ng_autonomic.py, ng_embed.py",
42
+ "Do not instantiate NeuroGraphMemory or any module class directly — HTTP endpoints or file reads only (Law 1)",
43
+ "Do not touch data/checkpoints/ or any msgpack file"
44
+ ],
45
+ "anti_drift": [
46
+ "All new classes go inside neurograph_gui.py, not in new files",
47
+ "EcosystemPoller uses threading.Thread with daemon=True for each panel fetch",
48
+ "In-flight guard per panel: skip spawn if _in_flight[panel] is True",
49
+ "All panel fetches degrade gracefully with offline:True dict on any error"
50
+ ],
51
+ "tool_allowlist": [
52
+ "write_file",
53
+ "edit_file",
54
+ "shell_execute",
55
+ "read_file"
56
+ ],
57
+ "shell_allowlist": [
58
+ "rm",
59
+ "echo"
60
+ ]
61
+ },
62
+ "steps": [
63
+ {
64
+ "id": "check_token",
65
+ "type": "action",
66
+ "description": "Verify GITHUB_TOKEN is set",
67
+ "tool": "shell_execute",
68
+ "params": {
69
+ "command": "python3 -c \"import os; t=os.environ.get('GITHUB_TOKEN',''); print('TOKEN_OK' if t else 'TOKEN_MISSING')\""
70
+ },
71
+ "validation": {
72
+ "checks": [
73
+ {
74
+ "operator": "contains",
75
+ "value": "TOKEN_OK"
76
+ }
77
+ ]
78
+ },
79
+ "on_failure": "abort"
80
+ },
81
+ {
82
+ "id": "cleanup_workspace",
83
+ "type": "action",
84
+ "description": "Remove stale partial clone from any previous run",
85
+ "tool": "shell_execute",
86
+ "params": {
87
+ "command": "rm -rf neurograph_work && echo CLEANED"
88
+ },
89
+ "validation": {
90
+ "checks": [
91
+ {
92
+ "operator": "contains",
93
+ "value": "CLEANED"
94
+ }
95
+ ]
96
+ },
97
+ "on_failure": "abort"
98
+ },
99
+ {
100
+ "id": "clone_repo",
101
+ "type": "action",
102
+ "description": "Clone NeuroGraph repo (includes Phase 1 changes from BLK-GUI-197)",
103
+ "tool": "shell_execute",
104
+ "params": {
105
+ "command": "git clone --depth=1 https://${GITHUB_TOKEN}@github.com/greatnorthernfishguy-hub/NeuroGraph.git neurograph_work && echo CLONE_OK"
106
+ },
107
+ "validation": {
108
+ "checks": [
109
+ {
110
+ "operator": "contains",
111
+ "value": "CLONE_OK"
112
+ }
113
+ ]
114
+ },
115
+ "on_failure": "abort"
116
+ },
117
+ {
118
+ "id": "add_ecosystem_poller_class",
119
+ "type": "action",
120
+ "description": "Insert EcosystemPoller class before the IngestionManager section separator",
121
+ "tool": "edit_file",
122
+ "params": {
123
+ "path": "neurograph_work/neurograph_gui.py",
124
+ "old_text": "\n\n# ---------------------------------------------------------------------------\n# 5. IngestionManager\n# ---------------------------------------------------------------------------",
125
+ "new_text": "\n\n# ---------------------------------------------------------------------------\n# 4b. EcosystemPoller\n# ---------------------------------------------------------------------------\n\n\nclass EcosystemPoller:\n \"\"\"Fetches live data for the 5 Dashboard panels.\n\n Each panel runs its fetch on a daemon thread. An in-flight guard\n per panel prevents overlapping fetches when auto-refresh fires\n faster than the fetch completes.\n \"\"\"\n\n PANELS = (\"neurograph\", \"bunyan\", \"tid\", \"elmer\", \"thc\")\n\n def __init__(self, on_panel_ready: Callable[[str, Dict[str, Any]], None]) -> None:\n self._on_panel_ready = on_panel_ready\n self._in_flight: Dict[str, bool] = {p: False for p in self.PANELS}\n\n def fetch_all(self) -> None:\n for panel in self.PANELS:\n self._fetch_panel(panel)\n\n def _fetch_panel(self, panel: str) -> None:\n if self._in_flight.get(panel):\n return\n self._in_flight[panel] = True\n threading.Thread(target=self._worker, args=(panel,), daemon=True).start()\n\n def _worker(self, panel: str) -> None:\n try:\n data = self._fetch(panel)\n except Exception as exc:\n data = {\"offline\": True, \"reason\": str(exc)}\n finally:\n self._in_flight[panel] = False\n self._on_panel_ready(panel, data)\n\n def _fetch(self, panel: str) -> Dict[str, Any]:\n if panel == \"neurograph\":\n return self._fetch_http(\"http://127.0.0.1:8847/stats\")\n if panel == \"tid\":\n return self._fetch_http(\"http://127.0.0.1:7437/stats\")\n if panel == \"bunyan\":\n return self._fetch_bunyan()\n if panel == \"elmer\":\n return self._fetch_json(\n Path.home() / \".et_modules\" / \"elmer\" / \"ng_lite_state.json\"\n )\n if panel == \"thc\":\n return self._fetch_json(\n Path.home() / \".et_modules\" / \"healing_collective\" / \"detection_calibrator.json\"\n )\n return {\"offline\": True, \"reason\": \"unknown panel\"}\n\n def _fetch_http(self, url: str) -> Dict[str, Any]:\n import urllib.request\n try:\n with urllib.request.urlopen(url, timeout=5) as resp:\n return json.loads(resp.read().decode())\n except Exception as exc:\n return {\"offline\": True, \"reason\": str(exc)}\n\n def _fetch_bunyan(self) -> Dict[str, Any]:\n import glob as _glob\n pattern = str(Path.home() / \".et_modules\" / \"shared_learning\" / \"bunyan*.jsonl\")\n files = sorted(_glob.glob(pattern))\n if not files:\n return {\"offline\": True, \"reason\": \"no bunyan files\"}\n lines: List[str] = []\n for fpath in reversed(files):\n try:\n with open(fpath, \"r\", encoding=\"utf-8\") as fh:\n chunk = [ln.strip() for ln in fh.readlines() if ln.strip()]\n lines = chunk + lines\n if len(lines) >= 5:\n break\n except OSError:\n continue\n events: List[Any] = []\n for ln in lines[-5:]:\n try:\n events.append(json.loads(ln))\n except json.JSONDecodeError:\n events.append({\"raw\": ln})\n return {\"events\": events}\n\n def _fetch_json(self, path: Path) -> Dict[str, Any]:\n try:\n with open(path, \"r\", encoding=\"utf-8\") as fh:\n return json.load(fh)\n except FileNotFoundError:\n return {\"offline\": True, \"reason\": \"file not found\"}\n except Exception as exc:\n return {\"offline\": True, \"reason\": str(exc)}\n\n\n\n# ---------------------------------------------------------------------------\n# 5. IngestionManager\n# ---------------------------------------------------------------------------"
126
+ },
127
+ "validation": {
128
+ "checks": [
129
+ {
130
+ "operator": "result_is_string"
131
+ }
132
+ ]
133
+ },
134
+ "on_failure": "abort"
135
+ },
136
+ {
137
+ "id": "add_dashboard_tab_call",
138
+ "type": "action",
139
+ "description": "Wire _build_dashboard_tab() into _build_tabs() after _build_modules_tab()",
140
+ "tool": "edit_file",
141
+ "params": {
142
+ "path": "neurograph_work/neurograph_gui.py",
143
+ "old_text": " self._build_modules_tab()\n self._build_logs_tab()",
144
+ "new_text": " self._build_modules_tab()\n self._build_dashboard_tab()\n self._build_logs_tab()"
145
+ },
146
+ "validation": {
147
+ "checks": [
148
+ {
149
+ "operator": "result_is_string"
150
+ }
151
+ ]
152
+ },
153
+ "on_failure": "abort"
154
+ },
155
+ {
156
+ "id": "init_ecosystem_poller",
157
+ "type": "action",
158
+ "description": "Initialize self.ecosystem_poller in NeuroGraphGUI.__init__ before file_watcher",
159
+ "tool": "edit_file",
160
+ "params": {
161
+ "path": "neurograph_work/neurograph_gui.py",
162
+ "old_text": " self.file_watcher: Optional[FileWatcher] = None\n self._ingest_history: List[Dict[str, Any]] = []",
163
+ "new_text": " self.ecosystem_poller = EcosystemPoller(\n on_panel_ready=lambda panel, data: self.msg_queue.put(\n self._apply_dashboard_panel, panel, data\n ),\n )\n self.file_watcher: Optional[FileWatcher] = None\n self._ingest_history: List[Dict[str, Any]] = []"
164
+ },
165
+ "validation": {
166
+ "checks": [
167
+ {
168
+ "operator": "result_is_string"
169
+ }
170
+ ]
171
+ },
172
+ "on_failure": "abort"
173
+ },
174
+ {
175
+ "id": "add_dashboard_methods",
176
+ "type": "action",
177
+ "description": "Add all Dashboard tab methods to NeuroGraphGUI before Settings Dialog section",
178
+ "tool": "edit_file",
179
+ "params": {
180
+ "path": "neurograph_work/neurograph_gui.py",
181
+ "old_text": " # ---- Settings Dialog ----",
182
+ "new_text": " # ---- Dashboard Tab ----\n\n def _build_dashboard_tab(self) -> None:\n frame = ttk.Frame(self.notebook, padding=10)\n self.notebook.add(frame, text=\" Dashboard \")\n\n hdr = ttk.Frame(frame)\n hdr.pack(fill=tk.X, pady=(0, 8))\n ttk.Label(hdr, text=\"Ecosystem Dashboard\", font=(\"\", 13, \"bold\")).pack(side=tk.LEFT)\n ttk.Button(hdr, text=\"Refresh Now\", command=self._refresh_dashboard).pack(\n side=tk.RIGHT, padx=(4, 0)\n )\n ttk.Label(hdr, text=\"Interval (s):\").pack(side=tk.RIGHT)\n self._dashboard_interval_var = tk.StringVar(\n value=str(self.config.get(\"dashboard_refresh_seconds\", 30))\n )\n ttk.Entry(\n hdr, textvariable=self._dashboard_interval_var, width=5\n ).pack(side=tk.RIGHT, padx=(0, 4))\n ttk.Button(\n hdr, text=\"Apply\", command=self._start_dashboard_autorefresh\n ).pack(side=tk.RIGHT, padx=(0, 2))\n\n grid = ttk.Frame(frame)\n grid.pack(fill=tk.BOTH, expand=True)\n grid.columnconfigure(0, weight=1)\n grid.columnconfigure(1, weight=1)\n\n panel_defs = [\n (\"neurograph\", \"Syl / NeuroGraph\", 0, 0),\n (\"tid\", \"TID Routing\", 0, 1),\n (\"bunyan\", \"Bunyan Narrative\", 1, 0),\n (\"elmer\", \"Elmer Health\", 1, 1),\n (\"thc\", \"THC Repairs\", 2, 0),\n ]\n self._dashboard_texts: Dict[str, tk.Text] = {}\n for panel_id, title, row, col in panel_defs:\n lf = ttk.LabelFrame(grid, text=title, padding=6)\n lf.grid(row=row, column=col, sticky=tk.NSEW, padx=4, pady=4)\n grid.rowconfigure(row, weight=1)\n txt = tk.Text(\n lf, height=6, wrap=tk.WORD, state=tk.DISABLED, font=(\"Courier\", 9)\n )\n txt.pack(fill=tk.BOTH, expand=True)\n self._dashboard_texts[panel_id] = txt\n\n self._dashboard_after_id: Optional[str] = None\n self._refresh_dashboard()\n self._start_dashboard_autorefresh()\n\n def _refresh_dashboard(self) -> None:\n self.ecosystem_poller.fetch_all()\n\n def _apply_dashboard_panel(self, panel: str, data: Dict[str, Any]) -> None:\n txt = self._dashboard_texts.get(panel)\n if txt is None:\n return\n txt.config(state=tk.NORMAL)\n txt.delete(\"1.0\", tk.END)\n if data.get(\"offline\"):\n txt.insert(tk.END, f\"Offline \\u2014 {data.get('reason', 'unavailable')}\")\n else:\n txt.insert(tk.END, self._format_panel_data(panel, data))\n txt.config(state=tk.DISABLED)\n\n def _format_panel_data(self, panel: str, data: Dict[str, Any]) -> str:\n if panel == \"neurograph\":\n lines = []\n for k in (\"total_nodes\", \"total_synapses\", \"firing_rate\",\n \"active_nodes\", \"avg_voltage\"):\n if k in data:\n lines.append(f\"{k}: {data[k]}\")\n return \"\\n\".join(lines) if lines else json.dumps(data, indent=2)[:400]\n if panel == \"tid\":\n lines = []\n models = data.get(\"models\") or data.get(\"routing\") or {}\n if isinstance(models, dict):\n ranked = sorted(\n models.items(),\n key=lambda kv: kv[1].get(\"usage\", 0) if isinstance(kv[1], dict) else 0,\n reverse=True,\n )[:3]\n for name, stats in ranked:\n usage = stats.get(\"usage\", \"?\") if isinstance(stats, dict) else \"?\"\n quality = stats.get(\"quality\", \"?\") if isinstance(stats, dict) else \"?\"\n lines.append(f\"{name}: usage={usage} quality={quality}\")\n return \"\\n\".join(lines) if lines else json.dumps(data, indent=2)[:400]\n if panel == \"bunyan\":\n events = data.get(\"events\", [])\n lines = []\n for ev in events[-5:]:\n if isinstance(ev, dict):\n msg = ev.get(\"message\") or ev.get(\"event\") or ev.get(\"raw\") or str(ev)\n lines.append(str(msg)[:120])\n else:\n lines.append(str(ev)[:120])\n return \"\\n\".join(lines) if lines else \"No events\"\n if panel == \"elmer\":\n lines = []\n for k in (\"health_score\", \"maintenance_target\", \"last_action\",\n \"coherence\", \"total_nodes\"):\n if k in data:\n lines.append(f\"{k}: {data[k]}\")\n return \"\\n\".join(lines) if lines else json.dumps(data, indent=2)[:400]\n if panel == \"thc\":\n repairs = (\n data.get(\"repair_events\")\n or data.get(\"events\")\n or data.get(\"calibration_events\")\n or []\n )\n if isinstance(repairs, list):\n lines = []\n for ev in repairs[-5:]:\n if isinstance(ev, dict):\n lines.append(\n str(ev.get(\"event\") or ev.get(\"action\") or ev)[:120]\n )\n else:\n lines.append(str(ev)[:120])\n return \"\\n\".join(lines) if lines else json.dumps(data, indent=2)[:400]\n return json.dumps(data, indent=2)[:400]\n return json.dumps(data, indent=2)[:400]\n\n def _start_dashboard_autorefresh(self) -> None:\n if self._dashboard_after_id is not None:\n self.root.after_cancel(self._dashboard_after_id)\n try:\n interval_s = max(5, int(self._dashboard_interval_var.get()))\n except (ValueError, AttributeError):\n interval_s = 30\n self.config.set(\"dashboard_refresh_seconds\", interval_s)\n\n def _tick() -> None:\n self._refresh_dashboard()\n self._dashboard_after_id = self.root.after(interval_s * 1000, _tick)\n\n self._dashboard_after_id = self.root.after(interval_s * 1000, _tick)\n\n # ---- Settings Dialog ----"
183
+ },
184
+ "validation": {
185
+ "checks": [
186
+ {
187
+ "operator": "result_is_string"
188
+ }
189
+ ]
190
+ },
191
+ "on_failure": "abort"
192
+ },
193
+ {
194
+ "id": "syntax_check",
195
+ "type": "action",
196
+ "description": "Verify neurograph_gui.py has no syntax errors",
197
+ "tool": "shell_execute",
198
+ "params": {
199
+ "command": "python3 -m py_compile neurograph_work/neurograph_gui.py && echo SYNTAX_OK"
200
+ },
201
+ "validation": {
202
+ "checks": [
203
+ {
204
+ "operator": "contains",
205
+ "value": "SYNTAX_OK"
206
+ }
207
+ ]
208
+ },
209
+ "on_failure": "abort"
210
+ },
211
+ {
212
+ "id": "commit_and_push",
213
+ "type": "action",
214
+ "description": "Commit Phase 2 changes and push to NeuroGraph main",
215
+ "tool": "shell_execute",
216
+ "params": {
217
+ "command": "git -C neurograph_work config user.email \"codemine@et-systems.ai\" && git -C neurograph_work config user.name \"Codemine\" && git -C neurograph_work add neurograph_gui.py && git -C neurograph_work diff --cached --quiet && echo ALREADY_COMMITTED || (git -C neurograph_work commit -m \"feat: GUI 2.0 Phase 2 — Ecosystem Dashboard tab (BLK-GUI-198)\" && git -C neurograph_work push origin HEAD && echo PUSH_OK)"
218
+ },
219
+ "validation": {
220
+ "checks": [
221
+ {
222
+ "operator": "contains",
223
+ "value": "COMMIT"
224
+ }
225
+ ]
226
+ },
227
+ "on_failure": "abort"
228
+ }
229
+ ]
230
+ }