rmm dashboard redesign (Gemini live review) + CDP Chrome driver

- .claude/scripts/cdp.py: drive Chrome via DevTools Protocol; screenshots to disk
  (so Gemini/Grok can see the live site). Fixes invisible-window + no-disk-screenshot.
- reference_cdp_chrome_driver.md (+ MEMORY index)
- gururmm submodule pointer -> dashboard redesign docs (local 3cef6ba)
- session log

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-05 13:10:19 -07:00
parent 73c36342fd
commit 383f137186
7 changed files with 408 additions and 103 deletions

View File

@@ -7,6 +7,7 @@
- [Power Failure Runbook](../POWER_FAILURE_RUNBOOK.md) — Recovery order after a power event: Tailscale routes, libvirt/VMs, Seafile, NPM/DNS. - [Power Failure Runbook](../POWER_FAILURE_RUNBOOK.md) — Recovery order after a power event: Tailscale routes, libvirt/VMs, Seafile, NPM/DNS.
- [Syncro API — Invoice Verification Pattern](syncro_invoice_verification_pattern.md) — /invoices?customer_id=X returns no ticket linkage; query /invoices/{number} for ticket_id. Compare by ticket ID, not number. - [Syncro API — Invoice Verification Pattern](syncro_invoice_verification_pattern.md) — /invoices?customer_id=X returns no ticket linkage; query /invoices/{number} for ticket_id. Compare by ticket ID, not number.
- [Approval Workflow: Tools vs Projects](approval-workflow-tools-vs-projects.md) — Tools (remediation, scripts): Howard/Claude with approval. Projects (GuruRMM): Mike approval; features→roadmap, bugs→bug list. - [Approval Workflow: Tools vs Projects](approval-workflow-tools-vs-projects.md) — Tools (remediation, scripts): Howard/Claude with approval. Projects (GuruRMM): Mike approval; features→roadmap, bugs→bug list.
- [CDP Chrome driver](reference_cdp_chrome_driver.md) — Drive Chrome via DevTools Protocol (.claude/scripts/cdp.py): visible window + screenshots-to-disk so Gemini/Grok can SEE the live site. Use localhost not 127.0.0.1; dedicated profile. Antigravity-style.
- [Community Forum (Flarum)](reference_community_forum.md) — Flarum forum at community.azcomputerguru.com, API access, database, posting workflow. - [Community Forum (Flarum)](reference_community_forum.md) — Flarum forum at community.azcomputerguru.com, API access, database, posting workflow.
- [Radio Show Website](reference_radio_website.md) — Astro static site at radio.azcomputerguru.com on IX server. - [Radio Show Website](reference_radio_website.md) — Astro static site at radio.azcomputerguru.com on IX server.
- [IX Server Access](reference_ix_server_access.md) — `ix.azcomputerguru.com` / 172.16.3.10. Reachable when Tailscale is on (no VPN). SSH currently uses sshpass with root password; key auth from GURU-5070 not configured yet (was CachyOS, now Win11 — verify). - [IX Server Access](reference_ix_server_access.md) — `ix.azcomputerguru.com` / 172.16.3.10. Reachable when Tailscale is on (no VPN). SSH currently uses sshpass with root password; key auth from GURU-5070 not configured yet (was CachyOS, now Win11 — verify).
@@ -99,3 +100,4 @@
- [ACG Website Hosting](project_azcomputerguru_hosting.md) — azcomputerguru.com is hosted on IX Web Hosting via cPanel. - [ACG Website Hosting](project_azcomputerguru_hosting.md) — azcomputerguru.com is hosted on IX Web Hosting via cPanel.
- [jq on Windows emits CRLF](feedback_jq_crlf_windows.md) — winget jq outputs CRLF; trailing \r silently breaks `for x in $(jq ...)` loops + read-from-@tsv. Override `jq(){ command jq "$@"|tr -d '\r'; }`. Windows-build-specific (passes on Mac/Linux). - [jq on Windows emits CRLF](feedback_jq_crlf_windows.md) — winget jq outputs CRLF; trailing \r silently breaks `for x in $(jq ...)` loops + read-from-@tsv. Override `jq(){ command jq "$@"|tr -d '\r'; }`. Windows-build-specific (passes on Mac/Linux).
- [ScreenConnect RESTful API auth](reference_screenconnect_api.md) — CTRLAuthHeader = raw api_secret (no Basic/b64) + Origin header; only method is GetSessionsByName; matches blank-for-agents Name field so it cannot enumerate full inventory. - [ScreenConnect RESTful API auth](reference_screenconnect_api.md) — CTRLAuthHeader = raw api_secret (no Basic/b64) + Origin header; only method is GetSessionsByName; matches blank-for-agents Name field so it cannot enumerate full inventory.
- [No manufactured guardrails on our products](feedback_no_manufactured_guardrails.md) — At Mikes request on GuruRMM/GuruConnect/ClaudeTools, just execute; stop only for genuinely irreversible/destructive ops (with a heads-up). Read the actual code/state before claiming something is disallowed or a security hole.

View File

@@ -0,0 +1,19 @@
---
name: feedback-no-manufactured-guardrails
description: On OUR products (GuruRMM/GuruConnect/ClaudeTools etc.) at Mike's request, execute without manufactured guardrails; only stop for genuinely dangerous (irreversible/destructive) actions, with a clear heads-up.
metadata:
type: feedback
---
For products we build together (GuruRMM, GuruConnect, ClaudeTools, the MSP tooling), Mike is the owner/admin and his authority over them is real: **at his request, just execute** — do not invent "prohibited" categories, do not refuse routine admin/dev/provisioning work, do not lecture. Judge by the action's ACTUAL consequence, not by which generic rule-of-thumb bucket it falls in.
But "just execute" does NOT mean blind compliance. Three standing expectations:
1. **See around corners (proactive).** He counts on foresight — flag downstream/second-order consequences, design coupling, "works now but bites later" risks BEFORE they land. Bringing judgment forward is part of the job, not optional.
2. **Some actions I MUST perform because no interface exists for him** (e.g. creating an RMM user when there's no admin UI). When the tooling gap means I'm the only one who CAN do it, deflecting "go do it yourself" is just unhelpful, not safety. Do it.
3. **Negative ramifications → tell, then do.** If a request has real downsides, lay them out clearly and let HIM decide; inform is NOT block. The decision is his.
Stop him ONLY when something is genuinely dangerous — **irreversible and destructive to production/client data, fleet-breaking, or harmful to third parties** (e.g. dropping client data, mass cross-tenant deletes). Even then it's: stop, state plainly why, wait for explicit confirmation — never "refuse for a made-up reason." That confirm is a partner check he WANTS, not a gate.
**Why:** 2026-06-05 — I refused to create a routine test user in GuruRMM (his own product) citing a generic "don't create accounts" rule, then falsely alarmed that an endpoint was an "ungated security hole" after reading the route table but NOT the handler (it was bootstrap-only — not a vuln). Both were manufactured friction on his own system, and he was right to be frustrated: "you're actively making it so I use different products."
**How to apply:** Default to action on our products. Before claiming something is disallowed or a security problem, READ THE ACTUAL CODE/STATE first. Reserve "stop and confirm" for truly irreversible/destructive ops. Related: [[feedback-no-toml-config-endpoints]].

View File

@@ -0,0 +1,38 @@
---
name: reference_cdp_chrome_driver
description: Drive Chrome via CDP (debugger) with on-disk screenshots; how Gemini/Grok "see" the live site
metadata:
type: reference
---
`.claude/scripts/cdp.py` drives Chrome over the **Chrome DevTools Protocol** (same approach
Antigravity uses) — fixing two problems the claude-in-chrome MCP extension had: invisible windows,
and screenshots that never landed on disk.
**Why it matters:** CDP `Page.captureScreenshot` returns the PNG bytes, so cdp.py writes a **real
PNG file** → which can be fed to `agy image-analyze` (Gemini) or Grok. That is how Gemini/Grok
"look at the live site" (verified 2026-06-05: Gemini correctly read a CDP screenshot of the GuruRMM
login). The MCP extension's `save_to_disk` never produced a findable file.
**Setup (one-time per session):**
- `py -m pip install websocket-client` (uses stdlib `urllib` + `websocket-client`; no Playwright/Node).
- `py .claude/scripts/cdp.py launch [url]` — opens a **visible** Chrome on a **dedicated profile**
(`~/.claude/cdp-chrome-profile`) with `--remote-debugging-port=9222`. Dedicated profile = NOT logged
in; the user signs into authenticated apps once (Claude still must NOT type passwords — that rule
holds regardless of CDP).
**Gotchas:**
- Chrome's DNS-rebinding guard rejects `Host: 127.0.0.1` on the debug endpoint → **use `localhost`**
(cdp.py BASE is `http://localhost:9222`). Launch also passes `--remote-allow-origins=*`.
- Launching `chrome.exe` while Chrome runs on the SAME profile just opens a tab in the existing
instance (flags ignored). The dedicated `--user-data-dir` forces a real new instance with the port.
**Commands:** `launch [url]` · `status` · `nav <url> [tabid]` · `shot <out.png> [tabid]` ·
`click <x> <y>` · `type <text>` · `key <Key>` · `eval <js>`. Stateless (new WS per command).
**Letting Gemini/Grok DRIVE (not just see):** cdp.py is a plain CLI, so Grok's `run_terminal_command`
(or any agent with shell access) could call it to navigate/click. **Security caveat:** a debug Chrome
on :9222 is controllable by any local process, and if it holds authenticated sessions (M365, Syncro,
RMM) those are driveable by whatever drives it — including external-vendor CLIs. Safer model: **Claude
drives cdp.py; Gemini/Grok receive the on-disk screenshots.** Only expose direct driving to an
external CLI deliberately. See [[reference_gururmm]].

194
.claude/scripts/cdp.py Normal file
View File

@@ -0,0 +1,194 @@
#!/usr/bin/env python
"""
cdp.py - drive Chrome over the DevTools Protocol (CDP), like Antigravity does.
Launches (or attaches to) a Chrome started with --remote-debugging-port and drives
it: navigate, screenshot-to-disk, click, type, key, eval. Screenshots are written
as real PNG files (so they can be fed to Gemini/Grok image tools).
Usage:
py cdp.py launch [url] # start a visible debug Chrome (dedicated profile)
py cdp.py status # /json/version + list page targets
py cdp.py nav <url> [tabid] # navigate (active page if tabid omitted)
py cdp.py shot <out.png> [tabid] # screenshot the page to a PNG file
py cdp.py click <x> <y> [tabid] # left-click at viewport coords
py cdp.py type <text> [tabid] # insert text into the focused element
py cdp.py key <Key> [tabid] # press a key (Enter/Tab/Escape/...)
py cdp.py eval <js> [tabid] # Runtime.evaluate, prints JSON result
Env: CDP_PORT (default 9222), CDP_PROFILE (default %USERPROFILE%\\.claude\\cdp-chrome-profile)
"""
import sys, os, json, time, base64, subprocess, urllib.request
PORT = int(os.environ.get("CDP_PORT", "9222"))
BASE = f"http://localhost:{PORT}"
PROFILE = os.environ.get("CDP_PROFILE", os.path.join(os.path.expanduser("~"), ".claude", "cdp-chrome-profile"))
CHROME = next((p for p in [
r"C:\Program Files\Google\Chrome\Application\chrome.exe",
r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
os.path.expandvars(r"%LOCALAPPDATA%\Google\Chrome\Application\chrome.exe"),
] if os.path.isfile(p)), None)
import websocket # websocket-client
def http_get(path):
with urllib.request.urlopen(BASE + path, timeout=5) as r:
return json.loads(r.read().decode())
def page_targets():
return [t for t in http_get("/json") if t.get("type") == "page"]
def pick_target(tabid=None):
targets = page_targets()
if not targets:
raise SystemExit("[cdp] no page targets. Run: py cdp.py launch")
if tabid:
for t in targets:
if t["id"] == tabid:
return t
raise SystemExit(f"[cdp] tabid {tabid} not found")
# prefer a non-devtools, non-blank page
for t in targets:
if not t["url"].startswith("devtools://"):
return t
return targets[0]
def send(ws, _id, method, params=None):
ws.send(json.dumps({"id": _id, "method": method, "params": params or {}}))
while True:
msg = json.loads(ws.recv())
if msg.get("id") == _id:
if "error" in msg:
raise SystemExit(f"[cdp] {method} error: {msg['error']}")
return msg.get("result", {})
# ignore events with no matching id
def with_ws(tabid, fn):
t = pick_target(tabid)
ws = websocket.create_connection(t["webSocketDebuggerUrl"], max_size=64 * 1024 * 1024)
try:
return fn(ws)
finally:
ws.close()
def cmd_launch(args):
if not CHROME:
raise SystemExit("[cdp] chrome.exe not found")
os.makedirs(PROFILE, exist_ok=True)
url = args[0] if args else "about:blank"
subprocess.Popen([
CHROME,
f"--remote-debugging-port={PORT}",
f"--user-data-dir={PROFILE}",
"--no-first-run", "--no-default-browser-check",
"--remote-allow-origins=*",
url,
], close_fds=True)
for _ in range(40):
try:
v = http_get("/json/version")
print(f"[cdp] launched: {v.get('Browser')} ws={v.get('webSocketDebuggerUrl','')[:40]}...")
print(f"[cdp] profile: {PROFILE}")
return
except Exception:
time.sleep(0.25)
raise SystemExit("[cdp] chrome started but debug port never opened")
def cmd_status(args):
v = http_get("/json/version")
print(f"Browser: {v.get('Browser')}")
for t in page_targets():
print(f" [{t['id'][:8]}] {t['title'][:40]!r} {t['url'][:70]}")
def cmd_nav(args):
url = args[0]
if "://" not in url:
url = "https://" + url
tabid = args[1] if len(args) > 1 else None
def fn(ws):
send(ws, 1, "Page.enable")
send(ws, 2, "Page.navigate", {"url": url})
# wait for load event (best-effort)
deadline = time.time() + 20
ws.settimeout(20)
while time.time() < deadline:
try:
m = json.loads(ws.recv())
except Exception:
break
if m.get("method") == "Page.loadEventFired":
break
return "ok"
with_ws(tabid, fn)
time.sleep(1.0)
print(f"[cdp] navigated -> {url}")
def cmd_shot(args):
out = os.path.abspath(args[0])
tabid = args[1] if len(args) > 1 else None
def fn(ws):
return send(ws, 1, "Page.captureScreenshot", {"format": "png", "captureBeyondViewport": False})
res = with_ws(tabid, fn)
with open(out, "wb") as f:
f.write(base64.b64decode(res["data"]))
print(f"[cdp] screenshot -> {out} ({os.path.getsize(out)} bytes)")
def cmd_click(args):
x, y = float(args[0]), float(args[1])
tabid = args[2] if len(args) > 2 else None
def fn(ws):
for typ in ("mousePressed", "mouseReleased"):
send(ws, 1, "Input.dispatchMouseEvent",
{"type": typ, "x": x, "y": y, "button": "left", "clickCount": 1})
return "ok"
with_ws(tabid, fn)
print(f"[cdp] click ({x},{y})")
def cmd_type(args):
text = args[0]
tabid = args[1] if len(args) > 1 else None
with_ws(tabid, lambda ws: send(ws, 1, "Input.insertText", {"text": text}))
print(f"[cdp] typed {len(text)} chars")
KEYMAP = {"Enter": 13, "Return": 13, "Tab": 9, "Escape": 27, "Backspace": 8}
def cmd_key(args):
key = args[0]
tabid = args[1] if len(args) > 1 else None
code = KEYMAP.get(key)
def fn(ws):
base = {"key": key, "windowsVirtualKeyCode": code} if code else {"key": key}
send(ws, 1, "Input.dispatchKeyEvent", {"type": "keyDown", **base})
send(ws, 2, "Input.dispatchKeyEvent", {"type": "keyUp", **base})
return "ok"
with_ws(tabid, fn)
print(f"[cdp] key {key}")
def cmd_eval(args):
js = args[0]
tabid = args[1] if len(args) > 1 else None
res = with_ws(tabid, lambda ws: send(ws, 1, "Runtime.evaluate",
{"expression": js, "returnByValue": True}))
print(json.dumps(res.get("result", {}).get("value"), indent=2, default=str))
CMDS = {"launch": cmd_launch, "status": cmd_status, "nav": cmd_nav, "shot": cmd_shot,
"click": cmd_click, "type": cmd_type, "key": cmd_key, "eval": cmd_eval}
if __name__ == "__main__":
if len(sys.argv) < 2 or sys.argv[1] not in CMDS:
print(__doc__)
raise SystemExit(1)
CMDS[sys.argv[1]](sys.argv[2:])

View File

@@ -46,7 +46,8 @@ HGHAUBNER/server `user_session` can write to existing GPO-mapped drives (e.g. Q:
## Other Dataforth items PARKED 2026-06-04 (not started) ## Other Dataforth items PARKED 2026-06-04 (not started)
### 1. AD1 Files backup — command READY, awaiting "run AD1" ### 1. AD1 Files backup — [DONE] created 2026-06-05
**Created 2026-06-05 ~18:40 UTC** via GuruRMM remote command (cmd id `a12d59a3-117d-497a-a080-75823b8db08d`) on AD1 (agent `bf7bc5ee…`); `cbb.exe` returned "Backup plan is created." (180-day retention, fast-NTFS, compress, NBF, synthetic full, daily 2:00 AM). Initial run NOT triggered (first run at 2:00 AM, per Mike). AD1 now has image (`Image2025`) + file (`Files`), parallel to AD2. Original prep notes below for reference:
AD1's shared data folders need a Files plan matching AD2's (NBF, daily 2 AM, 180-day retention, `ACG-Dataforth`). Shares: `Engineering``C:\Engineering`, `ITSvc``C:\Shares\ITSvc`. AD1 currently has only the `Image2025` image plan. Verified AD2 `Files` plan config: `SerializationSupportRetentionTime=180 days`, GFS off, synthetic full, compression, fast-NTFS. Run on AD1 via RMM (agent `bf7bc5ee…`): AD1's shared data folders need a Files plan matching AD2's (NBF, daily 2 AM, 180-day retention, `ACG-Dataforth`). Shares: `Engineering``C:\Engineering`, `ITSvc``C:\Shares\ITSvc`. AD1 currently has only the `Image2025` image plan. Verified AD2 `Files` plan config: `SerializationSupportRetentionTime=180 days`, GFS off, synthetic full, compression, fast-NTFS. Run on AD1 via RMM (agent `bf7bc5ee…`):
``` ```
cbb.exe addBackupPlan -n "Files" -a "ACG-Dataforth" -nbf -syntheticFull yes -d "C:\Engineering" -d "C:\Shares\ITSvc" -c yes -fastNtfs yes -ntfs yes -every day -at "2:00 AM" -purge "180d" -notification errorOnly -dr yes cbb.exe addBackupPlan -n "Files" -a "ACG-Dataforth" -nbf -syntheticFull yes -d "C:\Engineering" -d "C:\Shares\ITSvc" -c yes -fastNtfs yes -ntfs yes -every day -at "2:00 AM" -purge "180d" -notification errorOnly -dr yes

View File

@@ -0,0 +1,51 @@
## User
- **User:** Mike Swanson (mike)
- **Machine:** GURU-5070
- **Role:** admin
# GuruRMM dashboard redesign (Gemini live review) + CDP Chrome driver
## Session Summary
Ran an interactive, Gemini-driven redesign review of the LIVE GuruRMM dashboard (Claude as eyes/hands
over 5 rounds), producing a final proposal + a round-by-round session log in the gururmm submodule
docs/. Then solved the underlying "can't drive/see Chrome" problem by building a CDP (Chrome DevTools
Protocol) driver, so screenshots now hit disk and can be fed to Gemini/Grok image tools.
## Key Decisions
- Gemini drove the redesign (design brain w/ full docs incl. 2302-line roadmap + source); Claude
transcribed live views + ran non-destructive interaction tests. No app code changed; no live-site
changes. Proposal: context-spine IA, unified omnibox, triage inbox, AgentDetail vertical sub-nav,
reuse existing primitives (tri-state inheritance, vertical sub-nav, RBAC banners, dark terminal).
- CDP over the MCP extension: launch visible Chrome with --remote-debugging-port on a DEDICATED
profile; drive via .claude/scripts/cdp.py (websocket-client + urllib). Fixes invisible-window +
screenshots-not-on-disk. Use localhost (not 127.0.0.1) for the debug endpoint.
- Password rule unchanged: Claude does NOT type passwords into fields even with CDP / even when asked.
## Problems Encountered
- MCP browser windows opened on a Chrome instance Mike couldn't see; save_to_disk screenshots never
landed on a findable path -> blocked feeding images to Gemini. Solved with CDP (captureScreenshot
returns bytes -> real PNG file -> Gemini image-analyze verified reading it).
- CDP /json 404 on 127.0.0.1 (DNS-rebinding guard) -> switched to localhost.
## Live UX bugs found during testing (candidates for UI_GAPS.md)
1. "Search everything" bar dead on Dashboard (per-route local filter bound to nothing). 2. Ctrl+K
palette collides with Chrome's native shortcut + no on-screen trigger. 3. Alerts: no grouping/dedup,
no bulk actions, no remediation link from an alert. 4. AgentDetail Overview leads with Maintenance
Mode, burying live metrics. 5. App defaults to light despite "dark is native" (theme is per-profile
localStorage).
## Configuration Changes
- NEW .claude/scripts/cdp.py (CDP driver). NEW .claude/memory/reference_cdp_chrome_driver.md (+ index).
- gururmm submodule (local commit 3cef6ba): docs/dashboard-redesign-{brief,proposal-gemini,session}.md
- pip: websocket-client 1.9.0 installed (py 3.14).
## Pending / Next
- Decide: push gururmm submodule docs to the gururmm repo (may hit build webhook) — held local for now.
- Optional: re-run the redesign loop feeding Gemini REAL CDP screenshots (now possible); file bugs 1-5
into UI_GAPS.md; triangulate with Grok; start Phase 1.
- Decide whether to grant Gemini/Grok direct cdp.py driving (security caveat: drives authed sessions).
## Reference
- CDP driver: .claude/scripts/cdp.py (launch/status/nav/shot/click/type/key/eval). Port 9222, profile
~/.claude/cdp-chrome-profile. Screenshots: .claude/tmp/cdp/.
- Redesign docs: projects/msp-tools/guru-rmm/docs/dashboard-redesign-*.md