← Commit history

Publish 1.0.104

John Lauer ·aac6bee098 ·1mo ago ·parent 48755fa
4 files changed +152−62
dashboard/kicad_dashboard.py+79−4
@@ -49,7 +49,7 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path from urllib.parse import unquote, urlparse -VERSION = "4.7.2"+VERSION = "4.7.3" AI_THREAD = "kicad-dashboard" DEFAULT_PORT = 8931 UI_DIR = Path(__file__).resolve().parent / "ui"@@ -126,6 +126,70 @@ def _ver_tuple(v):     return tuple(out[:3]) or (0,)  +# ---------------------------------------------------------------------------+# ROUTE TABLE. Two catch-alls used to live here and each one made a check pass+# for the wrong reason (fleet sweep, 2026-08-25):+#   * do_GET rendered index.html for EVERY unmatched path, so a missing settings+#     PNG, demo mp3 or favicon came back as HTML 200. The launch-console 404+#     watch is the one thing watched on a virgin install, so the check that+#     exists to catch a broken fresh install could never fire. It also made+#     proxy-fidelity row 3 ("not upstream's 418") pass on blanket-SERVED paths.+#   * do_OPTIONS answered 204 for EVERY path, so the module's checkSend probes+#     the send route with OPTIONS and its "send route wired: not 404" row went+#     green for routes that do not exist at all.+# A permissive default does not fail a check, it ANSWERS it. Both now consult+# this one table, so a route cannot be live for OPTIONS and dead for GET.+#+# Matching mirrors each handler EXACTLY, endswith included: the adom.cloud proxy+# mounts this app under a sub-path and forwards the prefix, so "/apps/kicad/health"+# is the same route as "/health".+_EXACT_ROUTES = ("/health", "/console", "/events", "/api/targets",+                 "/api/action", "/ui/toast", "/ui/click")+_SUFFIX_ROUTES = ("/newbie-panel.js", "/favicon.svg", "/favicon.png", "/favicon.ico",+                  "/dock-kicad.svg", "/kicad-logo.png", "/updater/status",+                  "/api/viewport", "/ai-threads/run-prompt", "/api/say-send")+_REGEX_ROUTES = (r"/assets/(.+)$",+                 r"^/api/verb_times(?:/([^/]+))?$",+                 r"^/(?:api/status|state)(?:/([^/]+))?(/force)?$")+# Namespaces we own: an unmatched path under one of these is a WRONG route, never+# an app URL, so it 404s even for a browser navigation.+_OWNED_PREFIXES = ("/api/", "/ui/", "/updater/", "/assets/", "/ai-threads/")+_ASSET_SUFFIXES = (".png", ".jpg", ".jpeg", ".webp", ".svg", ".ico", ".gif",+                   ".mp3", ".wav", ".ogg", ".mp4", ".webm",+                   ".js", ".mjs", ".css", ".map", ".json", ".woff", ".woff2", ".ttf")+++def _route_exists(path):+    """True if `path` reaches a real handler. Used by do_OPTIONS so a preflight+    cannot claim a route that GET/POST would not serve."""+    if path in _EXACT_ROUTES or path.endswith(_SUFFIX_ROUTES):+        return True+    return any(re.search(rx, path) for rx in _REGEX_ROUTES)+++def _wants_app_shell(handler, path):+    """The app is a single page, and under the proxy its root arrives WITH the+    mount prefix ("/apps/kicad"), which is shape-identical to a typo. So the+    shell is served only for a real browser navigation, and never for a path+    that is asset-shaped or inside a namespace we own.++    Stated honestly because it is the load-bearing caveat: a browser typing a+    wrong extensionless URL still gets the shell. Every case the 404 watch+    actually watches - a missing png, mp3, favicon, or js - carries an extension+    or an owned prefix and 404s regardless of who asks."""+    if path == "/":+        return True          # the app's own root, whatever client asks for it+    if path.endswith(_ASSET_SUFFIXES):+        return False+    if any(pre in path + "/" for pre in _OWNED_PREFIXES):+        return False+    # A browser NAVIGATION always sends text/html in Accept; curl and every+    # other tool send */* or nothing. That is the only signal separating the+    # proxy's mounted root from a typo, and it is why the one-line fleet test+    # (curl <base>/nonsense) gets an honest 404.+    return "text/html" in (handler.headers.get("Accept") or "")++ def _proxy_control(handler, path, body=None, timeout=20):     """Forward one control-API call VERBATIM: same status, same bytes. @@ -1232,7 +1296,12 @@ class Handler(BaseHTTPRequestHandler):             force = bool(m.group(2))             log(f"status target={target}{' force' if force else ''}")             return self._json(cached_status(target, force))-        try:  # any other GET renders the app (proxy-friendly: sub-paths still work)+        if not _wants_app_shell(self, path):+            # NOT a catch-all any more. An unmatched asset, an unknown route in a+            # namespace we own, or any non-browser client gets a real 404, so the+            # launch-console 404 watch can actually fire on a virgin install.+            return self._json({"error": "not found", "path": path}, 404)+        try:  # a real browser navigation renders the app (proxy sub-paths still work)             # no-store: the dock's Hard Refresh Open must never get a cached             # shell after a version bump (dockbar guide).             # Inject the phrase list at SERVE time. It comes from dockbar.json,@@ -1251,8 +1320,14 @@ class Handler(BaseHTTPRequestHandler):             return self._json({"error": f"ui missing: {e}"}, 500)      def do_OPTIONS(self):-        # CORS preflight on every /api/* route (dockbar guide): the dock card-        # and the webview both call cross-origin.+        # CORS preflight on the routes we ACTUALLY serve (dockbar guide): the dock+        # card and the webview both call cross-origin. This used to answer 204 for+        # every path, which made the module's checkSend "send route wired" row+        # green for routes that do not exist - it probes with OPTIONS, so a blanket+        # preflight answered the probe instead of the route.+        path = urlparse(self.path).path.rstrip("/") or "/"+        if not _route_exists(path):+            return self._json({"error": "not found", "path": path}, 404)         self.send_response(204)         self.send_header("Access-Control-Allow-Origin", "*")         self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "kicad-bridge",-  "version": "1.0.103",+  "version": "1.0.104",   "type": "app",   "description": "Skills for your container so your AI knows how to drive the KiCad bridge. The bridge runtime itself is the release zip; Adom Bridge loads that.",   "tags": [
page.json+37−57
@@ -4,7 +4,7 @@   "slug": "kicad-bridge",   "title": "KiCad - the KiCad Bridge",   "brief": "Reference implementation of the KiCad bridge: multi-instance Python server, forward path via kicad-cli, reverse path via in-process plugin. Most complex of the three bundled bridges.",-  "version": "1.0.103",+  "version": "1.0.104",   "tags": [     "kicad",     "pcb",@@ -17,69 +17,34 @@   ],   "license": "MIT",   "discovery_triggers": [-    "kicad bridge",-    "open my pcb",-    "open my pcb design",-    "open my board in kicad",-    "open my schematic",-    "open my circuit board",-    "design a circuit board",-    "edit my pcb",-    "edit my schematic",-    "open a kicad board",-    "open a kicad project",-    "run drc",-    "run drc on my board",-    "run erc on my schematic",-    "check my board for errors",-    "export gerbers",-    "export gerber files",-    "export gerbers for jlcpcb",-    "make gerbers for my pcb",-    "export a step file from my board",-    "export bom",-    "export a bom from my schematic",-    "show my board in 3d",-    "open the 3d viewer",-    "view my pcb in 3d",     "open kicad",-    "launch kicad",-    "install kicad",-    "upgrade kicad",-    "is kicad installed",-    "what kicad versions do i have",-    "install a kicad symbol",-    "install a footprint in kicad",-    "add a library to kicad",-    "open the symbol editor",-    "open the footprint editor",-    "screenshot kicad",-    "take a screenshot of my board",-    "place a footprint",-    "open a kicad_pcb file",-    "open a kicad_sch file"+    "control kicad",+    "kicad dashboard",+    "show me a footprint",+    "show me a symbol",+    "drive kicad from the cloud",+    "kicad bridge",+    "open the pcb editor",+    "view a part in 3d",+    "install a kicad symbol"   ],-  "discovery_pitch": "Drive KiCad on the user's desktop: open boards/schematics/symbols/footprints, run DRC/ERC, export gerbers/STEP/BOM, view in 3D. First run `adom-bridge-cli bridge_list` to confirm the kicad bridge is installed; if so, drive its `kicad_*` verbs (kicad_describe lists them, kicad_readiness checks KiCad is present). If this container lacks the skill, run `adom-wiki pkg install adom/kicad-bridge` or `adom-bridge-cli sync_skills`.",+  "discovery_pitch": "Drive KiCad on your own Windows desktop from an AI thread: open symbols, footprints, boards and 3D views in the background, and install parts into your libraries.",   "sample_prompts": [     {-      "label": "Read kicad bridge source",-      "prompt": "Download the kicad-bridge zip from the wiki and walk me through its server.py"-    },-    {-      "label": "Add a new kicad verb",-      "prompt": "Help me add a new kicad_get_layer_image verb to the in-process bridge plugin"+      "label": "Show a footprint",+      "prompt": "Show me the IN-S42ATR footprint in KiCad"     },     {-      "label": "Understand reverse bridge",-      "prompt": "Explain how kicad_bridge_call routes from cloud Docker into the running pcbnew.exe"+      "label": "Show a symbol",+      "prompt": "Open the RP2040 symbol in the KiCad symbol editor"     },     {-      "label": "List kicad verbs",-      "prompt": "adom-bridge-cli bridge_list and show me the kicad verbs"+      "label": "See it in 3D",+      "prompt": "Show me that part in 3D"     },     {-      "label": "Try the watcher pip",-      "prompt": "Enable project watch in Adom Bridge settings for a kicad project"+      "label": "Open the dashboard",+      "prompt": "Open the KiCad dashboard and tell me if anything is red"     }   ],   "install": {@@ -88,7 +53,7 @@     "install_hint": "",     "version_cmd": ""   },-  "readme": "# Adom Desktop \u2014 KiCad Bridge\n\nA **reverse bridge** that lets Adom Desktop (AD) \u2014 and the AI driving it \u2014 control the user's own\nKiCad on Windows: open and drive every editor, install symbol/footprint libraries, place parts, run\nDRC/ERC, export manufacturing files, and screenshot any window back to the AI.\n\nKiCad is the user's **host app**. The bridge *detects* an existing KiCad and, if none is present,\n*installs* one for them \u2014 it never asks the user to download or click through anything by hand.\n\n- **Page / docs:** https://wiki.adom.inc/adom/adom-desktop-kicad-bridge\n- **Bridge SDK:** https://wiki.adom.inc/adom/adom-desktop-bridges\n- **Verb namespace:** `kicad_*` &nbsp;\u00b7&nbsp; **Status verb:** `kicad_bridge_status` &nbsp;\u00b7&nbsp; **Health:** `GET /status`\n\n> Every screenshot in this README was captured on **ADOMBASELINE**, a stock Hyper-V VM with **no\n> GPU**, driven end-to-end through the bridge \u2014 install \u2192 libraries \u2192 editors \u2192 2D \u2192 3D. If it\n> renders there, it renders on a real laptop.\n\n---\n\n## Demo \u2014 narrated live editor tour (real screen recording)\n\n[![KiCad bridge live demo](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/kicad-bridge-demo-poster.png)](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/kicad-bridge-demo.mp4)\n\n**\u25b6 [Watch the demo](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/kicad-bridge-demo.mp4)** (48s, \ud83d\udd0a narrated) \u2014 the bridge driving real KiCad on a Windows VM: **select** a footprint and **zoom** the copper in the PCB editor, **rotate** the assembled board in 3D, then tour the schematic, symbol, and footprint editors. Genuine screen recording of the live windows (zoom / select / 3D rotation), narrated section by section \u2014 not animated stills.\n\n\n---\n\n## The demo verb \u2014 `kicad_demo`\n\nOne verb that shows the whole bridge off, built for the AI that demos Adom Desktop\nduring Hydrogen Desktop's install. Six beats, in the order a hardware person thinks:\n\n| Beat | Shows |\n|---|---|\n| 1 | the **schematic symbol** in the user's own Symbol Editor |\n| 2 | the **footprint** for that same part |\n| 3 | that part in **3D** |\n| 4 | a **schematic** using it (U1 + two 10k resistors) |\n| 5 | the **2D board layout** |\n| 6 | the finished **board in 3D** |\n\n```bash\nadom-desktop kicad_demo '{}'                      # start: prepares, warms KiCad, opens beat 1\nadom-desktop kicad_demo '{\"step\":\"footprint\"}'    # ...then follow data.nextCall each time\nadom-desktop kicad_demo '{\"all\":true}'            # bulk (non-interactive; slower than one request budget)\nadom-desktop kicad_close '{\"force\":true}'         # tear down\n```\n\n**It narrates itself.** Every beat returns `say` (a line to speak), `pointOut`\n(what's actually on screen), `window` (match it in `kicad_screenshot_all`) and\n`nextCall`. One beat per call is the default precisely so the AI can talk, screenshot,\nthen move on.\n\n**It ships nothing.** The two demo parts, the schematic and the board are all\ngenerated in code, and the 3D bodies come from KiCad's *own* bundled\n`3dmodels/*.3dshapes` \u2014 so the runtime zip stays small and the 3D views are real.\nParts install into the user's real `Adom` library; the project lands in\n`Documents/adom-kicad-demo`.\n\n**No KiCad? It offers to install it.** `kicad_demo` returns an offer instead of an\nerror; `kicad_demo '{\"installKiCad\": true}'` silently installs the official build\n(per-user, no UAC) and then runs the tour. Watching the AI install your EDA tool is\nitself a good demo beat.\n\n**It never takes your screen.** Every window opens in the background and all input is\nwindow-targeted (UIA / `PostMessage`), so you can keep working while it runs.\n\n---\n\n## How it fits together\n\nThe bridge is a `spawn.kind: python` process AD launches on the user's machine (`entrypoint:\nserver.py`, `port: 0` \u2014 AD picks the port and passes `ADOM_BIND_HOST`). It speaks HTTP\n(`POST /command`, `GET /status`) and reaches KiCad through **four control surfaces**, picking the\nlightest one that can do the job.\n\n![Control surfaces](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/control-surfaces.svg)\n\n| # | Surface | Used for | Where |\n|---|---------|----------|-------|\n| 1 | **kicad-cli** (subprocess) | headless DRC/ERC, gerber/pdf/svg/step/bom export, format upgrade | `handlers/export.py`, `run_drc`, `run_erc`, `lint_*` |\n| 2 | **KiCad IPC (kipy)** | board/schematic introspection, confirmed footprint placement (KiCad 9+) | `handlers/place_footprint.py` |\n| 3 | **Embedded Python** | JSON-RPC *inside* each KiCad process, dispatched on its wx UI thread | `plugin_payload/adom_bridge.py`, `handlers/bridge_client.py` |\n| 4 | **Win32 / UIA / SendKeys** | open editors, screenshots, clicks/keys, dialog handling, window management | `handlers/kicad_ui.py`, `handlers/close_windows.py` |\n\n---\n\n## The window tour\n\nEverything below was opened and captured on the GPU-less VM. Sample data is the **RP2040 breakout**\ngenerated by `tour-pack-rp2040/` (an `AdomRP2040` symbol + `QFN-56_AdomRP2040` footprint + a board).\n\n### Schematic editor \u2014 `kicad_open_schematic`\n\nThe RP2040 breakout: the MCU (U1), USB-C, a 12 MHz crystal, decoupling, and mounting pins.\n\n![Schematic editor](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/schematic-editor.png)\n\n### Symbol editor \u2014 `kicad_open_symbol_editor`\n\nOpens straight to a symbol (`kicad_install_symbol` puts it in a user library first).\n\n![Symbol editor](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/symbol-editor.png)\n\n### Footprint editor \u2014 `kicad_open_footprint_editor`\n\n`{\"footprintName\":\"QFN-56_AdomRP2040\",\"library\":\"AdomRP2040\"}` loads the part directly \u2014 56 pins +\nthermal pad, 7\u00d77 mm, 0.4 mm pitch, courtyard and silkscreen.\n\n![Footprint editor](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/footprint-editor.png)\n\n### PCB editor (2D) \u2014 `kicad_open_board`\n\nThe routed breakout \u2014 copper, silkscreen, the QFN-56 land pattern, mounting holes.\n\n![PCB editor 2D](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/pcb-editor-2d.png)\n\n### 3D viewer \u2014 `kicad_open_3d_viewer {\"editor\":\"pcb\"}`\n\nThe full board in 3D \u2014 board body, the USB-C connector's 3D model, the QFN chip body, SMD parts,\nplated through-holes. **Rendered on the CPU via the software-OpenGL fallback (below); reload 1.7 s.**\n\n![3D viewer](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/3d-viewer.png)\n\n---\n\n## Install & upgrade \u2014 zero manual steps\n\nThe bridge never tells the user to go download KiCad. `kicad_upgrade` fetches the official installer\nand runs it silently, picking the scope automatically:\n\n- **Elevated AD** \u2192 `/allusers /S` (system-wide, `%ProgramFiles%\\KiCad`).\n- **Non-elevated AD** \u2192 `/currentuser /S` (`%LocalAppData%\\Programs\\KiCad`, **no UAC prompt**).\n\n`kicad_readiness` reports whether KiCad is installed and ready without side effects; the AI routes on\nit before offering to install. Hard-won install details (all handled for you):\n\n- KiCad 10's NsisMultiUser installer **requires** a scope flag \u2014 bare `/S` errors `rc=666660`.\n- AD's portable Python has **no CA bundle**; the bridge ships `certs/cacert.pem` and uses it for TLS.\n- Downloads are size-checked against `Content-Length` (a truncated installer otherwise fails at NSIS).\n- `%APPDATA%/kicad/<ver>/` lib tables don't exist until first launch \u2014 the bridge **bootstraps** them\n  so `install_library`/`install_footprint` work on a never-opened KiCad.\n- `kicad_upgrade {\"diagnoseOnly\":true}` reports token-elevation type + `EnableLUA` without installing.\n\n---\n\n## Dialogs & error handling \u2014 the bridge clears the pointless ones\n\nKiCad throws modal dialogs that stall automation. The bridge **scans every window owned by a running\nKiCad process** (by PID \u2014 reliable, unlike title matching), auto-expires the benign ones, screenshots\nwhat it dismissed, and returns a hint so the AI can decide what (if anything) to tell the user.\n\nAuto-expired benign dialogs include:\n\n- **\"Could not use OpenGL / falling back to software rendering\"** (GPU-less hosts).\n- **\"Welcome to KiCad \u2014 starting for the first time\"** first-run wizard (dismissing accepts defaults).\n- **\"This file was created by an older version of KiCad\"** conversion notice.\n\n![First-run wizard, auto-dismissed](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/dialog-firstrun-wizard.png)\n\nVerbs:\n\n- `kicad_window_info` \u2014 lists windows and **self-heals** (auto-expires benign dialogs) by default.\n- `kicad_dismiss_dialogs {\"all\":true}` \u2014 clear everything blocking; `{\"screenshot\":true}` returns\n  images of each; `{\"forceSoftwareCanvas\":true}` persists Cairo canvas; `{\"debug\":true}` dumps the\n  raw PID-based scan.\n- `kicad_screenshot_all` \u2014 one call returns **every** open KiCad window, so the AI can spot an error\n  dialog it didn't expect and read the message.\n\n---\n\n## Software-OpenGL fallback (last resort)\n\n`kicad_enable_software_opengl` deploys Mesa's `llvmpipe` (a CPU OpenGL rasterizer) into KiCad's `bin`\nso a box with **no usable GPU** \u2014 Hyper-V, RDP, headless CI \u2014 can still render the editors and the 3D\nviewer. This is how every 3D shot above exists.\n\n> \u26a0\ufe0f It renders on the CPU and is **slow**. It is a worst-case fallback only \u2014 a real GPU (or GPU-P /\n> DDA passthrough) is vastly better. The bridge keeps it so a GPU-less box isn't a dead end; it is\n> never suggested proactively.\n\n---\n\n## Verb reference\n\nAll 41 verbs (prefix `kicad_`). Call `kicad_describe` for the live catalog with per-verb hints,\nrelated verbs, and pitfalls.\n\n**Windows & UI** \u2014 `open_board`, `open_schematic`, `open_symbol_editor`, `open_footprint_editor`,\n`open_3d_viewer`, `open_editors`, `close_symbol_editor`, `close_footprint_editor`, `close_3d_viewer`,\n`close`, `window_info`, `dismiss_dialogs`, `screenshot_all`, `send_key`, `click`, `fix_keyboard`\n\n**Libraries & parts** \u2014 `install_library`, `install_symbol`, `install_footprint`, `install_plugin`,\n`place_footprint`, `adom_library_status`\n\n**Checks & export** \u2014 `run_drc`, `run_erc`, `lint_board`, `lint_schematic`, `lint_library`,\n`format_upgrade`, `export_gerber`, `export_pdf`, `export_svg`, `export_step`, `export_bom_csv`\n\n**Detect, install & meta** \u2014 `list_versions`, `readiness`, `describe`, `enable_software_opengl`,\n`check_for_updates`, `upgrade`, `bridge_status`, `bridge_call`\n\n---\n\n## Running / dependencies\n\nAD provisions everything; there is nothing to install by hand.\n\n- **Python** \u2265 3.11 (AD provisions it) \u2014 stdlib only, plus the bundled `certs/cacert.pem`.\n- **KiCad** \u2265 7.0 (host app; `kicad_upgrade` installs it if absent).\n- **OS:** Windows (macOS/Linux detection stubs exist; the GUI surfaces are Windows-first).\n\nThe bridge binds `ADOM_BIND_HOST` on an AD-assigned port \u2014 never `0.0.0.0`. Auto-updates via\n`updateManifestUrl` in `bridge.json`.\n\n---\n\n*Developer & publish notes live in `dev-skills/` and `publish-skills/` (source-only, never shipped to\na user install).*\n",+  "readme": "# Adom Desktop — KiCad Bridge\n\nA **reverse bridge** that lets Adom Desktop (AD) — and the AI driving it — control the user's own\nKiCad on Windows: open and drive every editor, install symbol/footprint libraries, place parts, run\nDRC/ERC, export manufacturing files, and screenshot any window back to the AI.\n\nKiCad is the user's **host app**. The bridge *detects* an existing KiCad and, if none is present,\n*installs* one for them — it never asks the user to download or click through anything by hand.\n\n- **Page / docs:** https://wiki.adom.inc/adom/adom-desktop-kicad-bridge\n- **Bridge SDK:** https://wiki.adom.inc/adom/adom-desktop-bridges\n- **Verb namespace:** `kicad_*` &nbsp;·&nbsp; **Status verb:** `kicad_bridge_status` &nbsp;·&nbsp; **Health:** `GET /status`\n\n> Every screenshot in this README was captured on **ADOMBASELINE**, a stock Hyper-V VM with **no\n> GPU**, driven end-to-end through the bridge — install → libraries → editors → 2D → 3D. If it\n> renders there, it renders on a real laptop.\n\n---\n\n## Demo — narrated live editor tour (real screen recording)\n\n[![KiCad bridge live demo](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/kicad-bridge-demo-poster.png)](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/kicad-bridge-demo.mp4)\n\n**▶ [Watch the demo](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/kicad-bridge-demo.mp4)** (48s, 🔊 narrated) — the bridge driving real KiCad on a Windows VM: **select** a footprint and **zoom** the copper in the PCB editor, **rotate** the assembled board in 3D, then tour the schematic, symbol, and footprint editors. Genuine screen recording of the live windows (zoom / select / 3D rotation), narrated section by section — not animated stills.\n\n\n---\n\n## The demo verb — `kicad_demo`\n\nOne verb that shows the whole bridge off, built for the AI that demos Adom Desktop\nduring Hydrogen Desktop's install. Six beats, in the order a hardware person thinks:\n\n| Beat | Shows |\n|---|---|\n| 1 | the **schematic symbol** in the user's own Symbol Editor |\n| 2 | the **footprint** for that same part |\n| 3 | that part in **3D** |\n| 4 | a **schematic** using it (U1 + two 10k resistors) |\n| 5 | the **2D board layout** |\n| 6 | the finished **board in 3D** |\n\n```bash\nadom-desktop kicad_demo '{}'                      # start: prepares, warms KiCad, opens beat 1\nadom-desktop kicad_demo '{\"step\":\"footprint\"}'    # ...then follow data.nextCall each time\nadom-desktop kicad_demo '{\"all\":true}'            # bulk (non-interactive; slower than one request budget)\nadom-desktop kicad_close '{\"force\":true}'         # tear down\n```\n\n**It narrates itself.** Every beat returns `say` (a line to speak), `pointOut`\n(what's actually on screen), `window` (match it in `kicad_screenshot_all`) and\n`nextCall`. One beat per call is the default precisely so the AI can talk, screenshot,\nthen move on.\n\n**It ships nothing.** The two demo parts, the schematic and the board are all\ngenerated in code, and the 3D bodies come from KiCad's *own* bundled\n`3dmodels/*.3dshapes` — so the runtime zip stays small and the 3D views are real.\nParts install into the user's real `Adom` library; the project lands in\n`Documents/adom-kicad-demo`.\n\n**No KiCad? It offers to install it.** `kicad_demo` returns an offer instead of an\nerror; `kicad_demo '{\"installKiCad\": true}'` silently installs the official build\n(per-user, no UAC) and then runs the tour. Watching the AI install your EDA tool is\nitself a good demo beat.\n\n**It never takes your screen.** Every window opens in the background and all input is\nwindow-targeted (UIA / `PostMessage`), so you can keep working while it runs.\n\n---\n\n## How it fits together\n\nThe bridge is a `spawn.kind: python` process AD launches on the user's machine (`entrypoint:\nserver.py`, `port: 0` — AD picks the port and passes `ADOM_BIND_HOST`). It speaks HTTP\n(`POST /command`, `GET /status`) and reaches KiCad through **four control surfaces**, picking the\nlightest one that can do the job.\n\n![Control surfaces](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/control-surfaces.svg)\n\n| # | Surface | Used for | Where |\n|---|---------|----------|-------|\n| 1 | **kicad-cli** (subprocess) | headless DRC/ERC, gerber/pdf/svg/step/bom export, format upgrade | `handlers/export.py`, `run_drc`, `run_erc`, `lint_*` |\n| 2 | **KiCad IPC (kipy)** | board/schematic introspection, confirmed footprint placement (KiCad 9+) | `handlers/place_footprint.py` |\n| 3 | **Embedded Python** | JSON-RPC *inside* each KiCad process, dispatched on its wx UI thread | `plugin_payload/adom_bridge.py`, `handlers/bridge_client.py` |\n| 4 | **Win32 / UIA / SendKeys** | open editors, screenshots, clicks/keys, dialog handling, window management | `handlers/kicad_ui.py`, `handlers/close_windows.py` |\n\n---\n\n## The window tour\n\nEverything below was opened and captured on the GPU-less VM. Sample data is the **RP2040 breakout**\ngenerated by `tour-pack-rp2040/` (an `AdomRP2040` symbol + `QFN-56_AdomRP2040` footprint + a board).\n\n### Schematic editor — `kicad_open_schematic`\n\nThe RP2040 breakout: the MCU (U1), USB-C, a 12 MHz crystal, decoupling, and mounting pins.\n\n![Schematic editor](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/schematic-editor.png)\n\n### Symbol editor — `kicad_open_symbol_editor`\n\nOpens straight to a symbol (`kicad_install_symbol` puts it in a user library first).\n\n![Symbol editor](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/symbol-editor.png)\n\n### Footprint editor — `kicad_open_footprint_editor`\n\n`{\"footprintName\":\"QFN-56_AdomRP2040\",\"library\":\"AdomRP2040\"}` loads the part directly — 56 pins +\nthermal pad, 7×7 mm, 0.4 mm pitch, courtyard and silkscreen.\n\n![Footprint editor](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/footprint-editor.png)\n\n### PCB editor (2D) — `kicad_open_board`\n\nThe routed breakout — copper, silkscreen, the QFN-56 land pattern, mounting holes.\n\n![PCB editor 2D](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/pcb-editor-2d.png)\n\n### 3D viewer — `kicad_open_3d_viewer {\"editor\":\"pcb\"}`\n\nThe full board in 3D — board body, the USB-C connector's 3D model, the QFN chip body, SMD parts,\nplated through-holes. **Rendered on the CPU via the software-OpenGL fallback (below); reload 1.7 s.**\n\n![3D viewer](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/3d-viewer.png)\n\n---\n\n## Install & upgrade — zero manual steps\n\nThe bridge never tells the user to go download KiCad. `kicad_upgrade` fetches the official installer\nand runs it silently, picking the scope automatically:\n\n- **Elevated AD** → `/allusers /S` (system-wide, `%ProgramFiles%\\KiCad`).\n- **Non-elevated AD** → `/currentuser /S` (`%LocalAppData%\\Programs\\KiCad`, **no UAC prompt**).\n\n`kicad_readiness` reports whether KiCad is installed and ready without side effects; the AI routes on\nit before offering to install. Hard-won install details (all handled for you):\n\n- KiCad 10's NsisMultiUser installer **requires** a scope flag — bare `/S` errors `rc=666660`.\n- AD's portable Python has **no CA bundle**; the bridge ships `certs/cacert.pem` and uses it for TLS.\n- Downloads are size-checked against `Content-Length` (a truncated installer otherwise fails at NSIS).\n- `%APPDATA%/kicad/<ver>/` lib tables don't exist until first launch — the bridge **bootstraps** them\n  so `install_library`/`install_footprint` work on a never-opened KiCad.\n- `kicad_upgrade {\"diagnoseOnly\":true}` reports token-elevation type + `EnableLUA` without installing.\n\n---\n\n## Dialogs & error handling — the bridge clears the pointless ones\n\nKiCad throws modal dialogs that stall automation. The bridge **scans every window owned by a running\nKiCad process** (by PID — reliable, unlike title matching), auto-expires the benign ones, screenshots\nwhat it dismissed, and returns a hint so the AI can decide what (if anything) to tell the user.\n\nAuto-expired benign dialogs include:\n\n- **\"Could not use OpenGL / falling back to software rendering\"** (GPU-less hosts).\n- **\"Welcome to KiCad — starting for the first time\"** first-run wizard (dismissing accepts defaults).\n- **\"This file was created by an older version of KiCad\"** conversion notice.\n\n![First-run wizard, auto-dismissed](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/dialog-firstrun-wizard.png)\n\nVerbs:\n\n- `kicad_window_info` — lists windows and **self-heals** (auto-expires benign dialogs) by default.\n- `kicad_dismiss_dialogs {\"all\":true}` — clear everything blocking; `{\"screenshot\":true}` returns\n  images of each; `{\"forceSoftwareCanvas\":true}` persists Cairo canvas; `{\"debug\":true}` dumps the\n  raw PID-based scan.\n- `kicad_screenshot_all` — one call returns **every** open KiCad window, so the AI can spot an error\n  dialog it didn't expect and read the message.\n\n---\n\n## Software-OpenGL fallback (last resort)\n\n`kicad_enable_software_opengl` deploys Mesa's `llvmpipe` (a CPU OpenGL rasterizer) into KiCad's `bin`\nso a box with **no usable GPU** — Hyper-V, RDP, headless CI — can still render the editors and the 3D\nviewer. This is how every 3D shot above exists.\n\n> ⚠️ It renders on the CPU and is **slow**. It is a worst-case fallback only — a real GPU (or GPU-P /\n> DDA passthrough) is vastly better. The bridge keeps it so a GPU-less box isn't a dead end; it is\n> never suggested proactively.\n\n---\n\n## Verb reference\n\nAll 41 verbs (prefix `kicad_`). Call `kicad_describe` for the live catalog with per-verb hints,\nrelated verbs, and pitfalls.\n\n**Windows & UI** — `open_board`, `open_schematic`, `open_symbol_editor`, `open_footprint_editor`,\n`open_3d_viewer`, `open_editors`, `close_symbol_editor`, `close_footprint_editor`, `close_3d_viewer`,\n`close`, `window_info`, `dismiss_dialogs`, `screenshot_all`, `send_key`, `click`, `fix_keyboard`\n\n**Libraries & parts** — `install_library`, `install_symbol`, `install_footprint`, `install_plugin`,\n`place_footprint`, `adom_library_status`\n\n**Checks & export** — `run_drc`, `run_erc`, `lint_board`, `lint_schematic`, `lint_library`,\n`format_upgrade`, `export_gerber`, `export_pdf`, `export_svg`, `export_step`, `export_bom_csv`\n\n**Detect, install & meta** — `list_versions`, `readiness`, `describe`, `enable_software_opengl`,\n`check_for_updates`, `upgrade`, `bridge_status`, `bridge_call`\n\n---\n\n## Running / dependencies\n\nAD provisions everything; there is nothing to install by hand.\n\n- **Python** ≥ 3.11 (AD provisions it) — stdlib only, plus the bundled `certs/cacert.pem`.\n- **KiCad** ≥ 7.0 (host app; `kicad_upgrade` installs it if absent).\n- **OS:** Windows (macOS/Linux detection stubs exist; the GUI surfaces are Windows-first).\n\nThe bridge binds `ADOM_BIND_HOST` on an AD-assigned port — never `0.0.0.0`. Auto-updates via\n`updateManifestUrl` in `bridge.json`.\n\n---\n\n*Developer & publish notes live in `dev-skills/` and `publish-skills/` (source-only, never shipped to\na user install).*\n",   "author": {     "name": "John Lauer",     "email": "[email protected]"@@ -130,7 +95,22 @@     "skills/kicad-bridge-hero/compose_hero.py",     "skills/kicad-bridge-test/SKILL.md",     "skills/kicad-bridge-test/run_verb_tests.py",-    "skills/kicad-bridge-background/SKILL.md"+    "skills/kicad-bridge-background/SKILL.md",+    "dashboard/kicad_dashboard.py",+    "dashboard/ui/index.html",+    "dashboard/ui/newbie-panel.js",+    "dashboard/ui/kicad-logo.png",+    "dashboard/ui/dock-kicad.svg",+    "dashboard/ui/assets/shots/taskbar-overlay.png",+    "dashboard/ui/assets/shots/taskbar-both.png",+    "dashboard/ui/assets/shots/taskbar-flash.png",+    "dashboard/ui/assets/shots/taskbar-progress.png",+    "dashboard/ui/assets/shots/taskbar-none.png",+    "dashboard/ui/assets/icons/settings-cog.svg",+    "kicad-bridge-hero-v6.png",+    "docs/img/dock-kicad.svg",+    "tools/capture_taskbar_shots.py",+    "dockbar.json"   ],   "org": "adom",   "name": "kicad-bridge",@@ -147,4 +127,4 @@   "authors": null,   "author_handle": "john",   "authors_json": null-}+}\ No newline at end of file
skills/kicad-bridge-dev/SKILL.md+35
@@ -425,6 +425,41 @@ the package. Two layers, two commands, every time.  Every one of these came from shipping something that looked verified and was not. +**A PERMISSIVE DEFAULT DOES NOT FAIL A CHECK, IT ANSWERS IT (2026-08-25, fleet sweep).**+This dashboard had TWO catch-alls and each one made a different check pass for+the wrong reason. `do_GET` rendered index.html for every unmatched path, so a+missing settings PNG, demo mp3 or favicon came back as HTML 200 instead of 404 -+and the launch-console 404 watch is the ONE thing watched on a virgin install, so+the check that exists to catch a broken fresh install could never fire. It also+made proxy-fidelity row 3 ("not upstream's 418") pass on blanket-SERVED paths.+Separately `do_OPTIONS` answered 204 for EVERY path, and the module's `checkSend`+probes the send route with OPTIONS, so "send route wired: not 404" was green for+routes that do not exist at all. Test it in one line, and it is the whole fleet's+test now: `curl -o /dev/null -w '%{http_code}' <base>/nonsense` must be 404.+Both handlers now consult ONE route table (`_route_exists`), so a route cannot be+live for OPTIONS and dead for GET.++**The second bug is invisible until the first is fixed.** Fix the 404 first, THEN+re-run the harnesses, because while the catch-all answers everything the other+rows cannot tell you anything. Order: 404 unmatched -> real CORS preflight on the+proxied routes -> re-run proxy-fidelity and checkSend.++**A 404 sweep needs a rule, not a blanket, when a proxy mounts you under a+sub-path.** adom.cloud forwards the prefix (which is why the routes match with+`endswith`), so the app's own root arrives as "/apps/kicad" and is shape-identical+to a typo. The rule that survives both: 404 anything asset-shaped (a known file+extension) or inside a namespace we own (/api/, /ui/, /updater/, /assets/,+/ai-threads/) no matter who asks, always serve "/", and otherwise serve the shell+only for a real browser navigation (`Accept: text/html`). STATE THE CAVEAT rather+than claiming a clean unconditional 404: a browser typing a wrong extensionless+URL still gets the shell. Every case the 404 watch actually watches carries an+extension or an owned prefix, so it 404s regardless of the client.++**Prove a check can still go RED.** `proxy-fidelity.py --self-test` exists for+this and it fails two rows on purpose before you trust a green run. The same+applies to the preflight fix: the send-route row reading 204 only means something+because `OPTIONS /nonsense` now returns 404, where it returned 204 before.+ **A metric that returns one value on success proves nothing (2026-08-25).** I reported "0px phrase headroom, one character from truncating" to the fleet, f7 shipped a whole new selfCheck row for it, and a second app "independently