diff --git a/SOURCE.txt b/SOURCE.txt new file mode 100644 index 0000000..9012317 --- /dev/null +++ b/SOURCE.txt @@ -0,0 +1,81 @@ +PCBJam — self-hosted fork (CMMS embed) +====================================== + +This repository is a self-hosted fork of PCBJam (browser-based KiCad compiled to +WebAssembly), maintained for embedding the editor in the tas-tech.net CMMS +(TT-DB) at /vendor/pcbjam/. + +Upstream +-------- + https://github.com/emergence-engineering/pcbjam + tracked here as git remote `upstream`. + + Forked at upstream commit: + 766e2f409fa8b098555226479742ec694709ea48 (branch main, 2026-09-01) + +Branches +-------- + main pristine mirror of upstream @ 766e2f4 (no local changes) + build/host-networking docker/docker-compose.yml: host networking on the build + host (its dockerd runs bridge:none + iptables:false for + a VPN killswitch, so the image build + compile container + use the host network namespace). 1 commit (28c545f). + cmms-embed build/host-networking + sub-path mount support so the + standalone editor serves under /vendor/pcbjam/ with no + change to KiCad/wxWidgets or @pcbjam/shared. See the + commit message for the file-by-file breakdown. A pure + no-op when built without `--base` (root deploy). + +Submodules +---------- +Pinned by the superproject gitlinks (nothing in scripts/ overrides them — +`KICAD_COMMIT` in scripts/common/versions.sh is unused/informational): + + kicad 56678639eea1b3c0358f384fa47ef7fab78bd09b + upstream https://github.com/PCBJam/kicad-source-mirror (branch wasm-port) + wxwidgets 4e6cc5a441e46ac07b242caa3adacfd8e7c7ec50 + upstream https://github.com/PCBJam/wxWidgets (branch wasm-port) + web/pcbjam-shared a4984c626d847bd07ce66148d6ec96fde8ee8e42 + upstream https://github.com/PCBJam/pcbjam-shared (branch main) + +.gitmodules still points at PCBJam's GitHub forks — these are NOT mirrored into +Forgejo. A checkout needs `git submodule update --init --recursive` against GitHub. + +Toolchain / dependency pins (scripts/common/versions.sh) +------------------------------------------------------- + Emscripten SDK 6.0.6 (installed from emsdk inside docker/Dockerfile; + base image ubuntu:22.04) + KiCad 8.99 + OpenCASCADE 7.8.0 + wxWidgets 3.3.1 + Boost 1.84.0 + Protobuf 3.21.12 + Python 3.11.5 + ngspice 46 + FreeType 2.13.2 + HarfBuzz 8.3.0 + Cairo 1.18.0 + Pixman 0.42.2 + Zstd 1.5.5 + GLM 0.9.9.8 + (every download in versions.sh also carries a SHA256 pin) + +Build +----- + KiCad WASM: ./docker/build.sh kicad_editor -j 4 --build-deps (debug) + ./docker/build.sh kicad_editor -j 4 --full --release (ship) + Editor UI: cd web && pnpm install --frozen-lockfile + pnpm --filter @pcbjam/standalone exec vite build --base=/vendor/pcbjam/ + (env: web/standalone/.env.production on the cmms-embed branch) + +License +------- +KiCad and wxWidgets are GPLv3. PCBJam's own code carries its upstream licenses +(see LICENSE and the per-package headers; @pcbjam/shared is MIT). This fork is +distributed under the same terms. + +The corresponding source for any deployed WASM build IS this repository at the +commit the build was made from, together with the submodule revisions its +gitlinks pin (listed above). Repository: + + https://repo.tas-tech.net/AI_Assistant/pcbjam diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index d17d33f..83a9f70 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -3,6 +3,9 @@ services: build: context: .. dockerfile: docker/Dockerfile + # Build host runs dockerd with bridge:none + iptables:false (PIA killswitch, + # no Docker NAT). Image-build RUN steps (emsdk download) use the host netns. + network: host args: # Single source of truth: scripts/common/versions.sh, exported into the env by the scripts # that drive compose (docker/build.sh, docker/shell.sh). No default -> the Dockerfile fails @@ -10,6 +13,10 @@ services: EMSCRIPTEN_VERSION: ${EMSCRIPTEN_VERSION:?source scripts/common/versions.sh before docker compose} # Container name is auto-generated with project prefix (set in build.sh) + # The compile container shares the host network namespace so it inherits the + # PIA killswitch directly — the build host's dockerd has no bridge/NAT. + network_mode: host + # Resource limits. Defaults are sized for a dev Mac (Docker Desktop VM). # CI overrides via env: the 32-core Hetzner runner sets KICAD_DOCKER_CPUS=$(nproc) # and KICAD_DOCKER_MEM=110G — with the 10-CPU default, run 27226030304 compiled diff --git a/web/standalone/.env.production b/web/standalone/.env.production new file mode 100644 index 0000000..118ef4d --- /dev/null +++ b/web/standalone/.env.production @@ -0,0 +1,33 @@ +# CMMS embed (cmms-embed branch). Consumed by `vite build` (mode=production). +# Pair with `vite build --base=/vendor/pcbjam/`. All values are public (they ship +# in the bundle) — no secrets here. + +# The PHP adapter. Relative so it resolves against the CMMS origin. The adapter +# handles PATH_INFO, so /api/scopes/... and /api/me land as +# /pcb-project-api.php/api/scopes/... and /pcb-project-api.php/api/me. +VITE_API_BASE_URL=/pcb-project-api.php + +# Projects come from the REST adapter above; file bytes fetched + saves PUT back. +VITE_PROJECT_SOURCE=remote +VITE_DOC_SOURCE=api + +# No library backend for v1. `static` (not `off`): `off` leaves `libsSource` null so boot.ts +# never calls installLibsProvider → `window.kicadLibs` is never installed → any C++ path that +# resolves a symbol/footprint through the libs bridge on schematic open (park site K1, +# sch_io_pcbjam_lib) calls a method on undefined → throws inside a suspending import → +# SuspendError → abort. `static` installs the bridge backed by 2 built-in example symbols; it +# answers "not found" gracefully for everything else. No backend, no manifest, self-contained. +VITE_LIBS_SOURCE=static + +# Flat WASM layout, served static by Apache under the mount (no per-tool subdir, +# VITE_WASM_MANIFEST intentionally unset). Files: kicad_editor.{js,wasm}, wx.js, +# wx-dom.js, images.tar.gz. +VITE_WASM_ROOT=/vendor/pcbjam/wasm + +# Cross-tab only; no collab server. +VITE_YJS_PROVIDER=broadcastchannel + +# VITE_APP_URL deliberately UNSET — with it set, every non-editor route would +# redirect away from the mount (lib/redirect.ts). +# VITE_USER / VITE_SCOPE deliberately UNSET — identity comes from +# /pcb-project-api.php/api/me, scope from the URL path segment after the mount. diff --git a/web/standalone/src/App.tsx b/web/standalone/src/App.tsx index 2d550ea..49b27a9 100644 --- a/web/standalone/src/App.tsx +++ b/web/standalone/src/App.tsx @@ -1,5 +1,6 @@ import { useEffect } from "react"; import { Route, Routes, useLocation } from "react-router-dom"; +import { CmmsAiWidget } from "@/components/CmmsAiWidget"; import { VersionBadge } from "@/components/VersionBadge"; import { useChromeHidden } from "@/lib/chrome-visibility"; import { APP_URL } from "@/lib/config"; @@ -34,6 +35,9 @@ export default function App() { {/* Version + source link, bottom-right on every route (home + editor). Keys off the Figma-like hide-UI toggle (hidden is the mobile default). */} {!chromeHidden && } + {/* No-op (fetches config, renders nothing) unless the CMMS embed's + ai_assistant module is on for this tenant — see CmmsAiWidget. */} + ); } diff --git a/web/standalone/src/components/CmmsAiWidget.tsx b/web/standalone/src/components/CmmsAiWidget.tsx new file mode 100644 index 0000000..3aabf5f --- /dev/null +++ b/web/standalone/src/components/CmmsAiWidget.tsx @@ -0,0 +1,139 @@ +// cmms-embed / PCBJam Part 2: floating AI-assistant launcher + panel, for the +// CMMS embed only. Inert (renders nothing) unless `/api/cmms-config` answers +// `aiEnabled: true` — a plain pcbjam.io deploy or a CMMS tenant with the +// module off never fetches or renders anything here beyond the one config +// call. Visual language (position, colors, toggle/panel shape) mirrors +// `render_ai_assistant_editor_embed()` (TT-DB `src/ai_assistant_widget.php`) +// so it reads as the same widget as every other embedded editor; the +// draggable/resizable geometry system that PHP version has is CMMS-side +// static JS this bundle can't import, so this is fixed-position (bottom +// right) — a reasonable v1 given the panel is small relative to the board +// canvas. +import * as React from "react"; +import { fetchCmmsConfig, type CmmsConfig } from "@/lib/cmms-config"; +import { getActiveFilePath, installAiBridge } from "@/wasm/collab/ai-bridge"; +import { setTheme } from "@/lib/theme"; + +export function CmmsAiWidget(): React.ReactElement | null { + const [config, setConfig] = React.useState(null); + const [open, setOpen] = React.useState(false); + + React.useEffect(() => { + let cancelled = false; + // fetchCmmsConfig() never rejects (its own internal .catch() falls back + // to NO_WIDGET) — this .catch() guards the callback body itself instead + // (setConfig/installAiBridge/setTheme), so a mistake here becomes a + // logged no-op, not a silent unhandled rejection with nothing to say why + // the widget didn't appear. + fetchCmmsConfig() + .then((c) => { + if (cancelled) return; + setConfig(c); + if (c.aiEnabled) installAiBridge(); + // Belt-and-braces alongside the `?theme=` hand-off (lib/theme.ts) — + // that param already wins whenever edit.php sets it, so this only + // matters if it's ever absent. Skip if the URL param already ran; + // setTheme is otherwise idempotent (persists + toggles `.dark`). + if (c.dark && !new URLSearchParams(window.location.search).get("theme")) { + setTheme("dark"); + } + }) + .catch((err: unknown) => { + console.error("[cmms-ai-widget] boot failed:", err); + }); + return () => { + cancelled = true; + }; + }, []); + + if (!config?.aiEnabled) return null; + + // Computed at render time (not memoized) so re-opening the panel after + // switching sheets sends the file that's actually open now — a context + // hint only; get-buffer carries the real content regardless. + const params = new URLSearchParams({ + editor: "kicad", + editor_entity_type: config.entityType, + editor_entity_id: config.entityId, + editor_filename: getActiveFilePath() ?? "", + page_context: "PCBJam (KiCad editor)", + }); + const src = `/modules/ai_assistant/widget-frame.php?${params.toString()}`; + + return ( +
+ + {open && ( +
+
+ AI Assistant + +
+