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/web/standalone/.env.production b/web/standalone/.env.production new file mode 100644 index 0000000..cd505e5 --- /dev/null +++ b/web/standalone/.env.production @@ -0,0 +1,28 @@ +# 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 — the editor boots with empty sym/fp-lib-tables. +VITE_LIBS_SOURCE=off + +# 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/components/NewFileDialog.tsx b/web/standalone/src/components/NewFileDialog.tsx index 651b7ef..230bf49 100644 --- a/web/standalone/src/components/NewFileDialog.tsx +++ b/web/standalone/src/components/NewFileDialog.tsx @@ -7,6 +7,7 @@ import { } from "@pcbjam/shared"; import { Loader2 } from "lucide-react"; import { uploadFileBytes } from "@/lib/api"; +import { hardNav } from "@/lib/base-path"; import { currentScope } from "@/lib/config"; import { localProjectStore } from "@/lib/project-source"; import { useLocalProjects } from "@/lib/api"; @@ -124,7 +125,7 @@ export function NewFileDialog({ // A home-created project is browser-local (@local scope); an in-project new // file keeps the current scope. The tool is inferred from the file's ext. const scope = homeMode ? LOCAL_SCOPE : currentScope(); - window.location.assign(projectPath(scope, slug, finalName)); + hardNav(projectPath(scope, slug, finalName)); } catch (e) { setBusy(false); setError(e instanceof Error ? e.message : String(e)); diff --git a/web/standalone/src/components/WasmTool.tsx b/web/standalone/src/components/WasmTool.tsx index 78b57b8..643db7a 100644 --- a/web/standalone/src/components/WasmTool.tsx +++ b/web/standalone/src/components/WasmTool.tsx @@ -20,6 +20,7 @@ import { yjsProviderConfig, type DocSource, } from "@/lib/config"; +import { stripBase } from "@/lib/base-path"; import { redirectTargetFor } from "@/lib/redirect"; import { loadSessionIdentity, seedSessionIdentity } from "@/lib/session-identity"; import { useThemeValue } from "@/lib/theme"; @@ -629,7 +630,10 @@ export function WasmTool({ // non-editor surfaces: on a backed deploy (APP_URL set) they belong to the // management app, so quit goes straight there (one hop instead of letting // App.tsx's 0006 redirect bounce it). - const segments = win.location.pathname.split("/").filter(Boolean); + // stripBase: segments are relative to the mount, so segments[1] is + // "projects"/"libs" even under /vendor/pcbjam/. exitPath stays app-absolute + // (no mount prefix) — installQuitHook's hardNav() re-adds it. + const segments = stripBase(win.location.pathname).split("/").filter(Boolean); const exitPath = segments[1] === "libs" ? "/" : projectPath(currentScope(), slug); const removeQuitHook = installQuitHook(win, { diff --git a/web/standalone/src/components/wasm-tool/quit-hook.ts b/web/standalone/src/components/wasm-tool/quit-hook.ts index dfd3f78..8cf77bf 100644 --- a/web/standalone/src/components/wasm-tool/quit-hook.ts +++ b/web/standalone/src/components/wasm-tool/quit-hook.ts @@ -6,6 +6,8 @@ // the page itself unloads (app.cpp UnloadCallback), so the dispatcher latches // off as soon as any unload/navigation is under way. +import { hardNav } from "@/lib/base-path"; + let activeQuitHook: (() => void) | undefined; let quitHandled = false; @@ -93,7 +95,9 @@ export function installQuitHook( // wasm stack has unwound. setTimeout(() => { opts.log(`[quit] editor closed — going to ${opts.exitUrl}`); - win.location.assign(opts.exitUrl); + // hardNav: re-add the CMMS mount prefix to the app-absolute exit path + // (a full APP_URL redirect target passes through untouched). + hardNav(opts.exitUrl, win); }, 0); }; diff --git a/web/standalone/src/components/wasm-tool/tool-navigation.ts b/web/standalone/src/components/wasm-tool/tool-navigation.ts index 77aeb39..2b9ea4e 100644 --- a/web/standalone/src/components/wasm-tool/tool-navigation.ts +++ b/web/standalone/src/components/wasm-tool/tool-navigation.ts @@ -7,6 +7,7 @@ import { toolSchema, type Tool, } from "@pcbjam/shared"; +import { hardNav } from "@/lib/base-path"; import { currentScope } from "@/lib/config"; import { defaultFileName, newFileTemplate, withExtension } from "@/lib/new-file"; import { memfsProjectDir } from "@/wasm/constants"; @@ -175,7 +176,7 @@ export function installToolNavigationHook( await createFile(relPath, bytes); opts.log(`[nav] created missing ${nextTool} file ${relPath} -> ${url}`); markDeliberateNavigation(); - win.location.assign(url); + hardNav(url, win); } catch (e) { pendingCreate = null; opts.log( @@ -196,7 +197,7 @@ export function installToolNavigationHook( opts.log(`[nav] ${rawToolName} ${rawFileName || "(no file)"} -> ${url}`); markDeliberateNavigation(); - win.location.assign(url); + hardNav(url, win); return true; }; diff --git a/web/standalone/src/lib/base-path.ts b/web/standalone/src/lib/base-path.ts new file mode 100644 index 0000000..bf47041 --- /dev/null +++ b/web/standalone/src/lib/base-path.ts @@ -0,0 +1,57 @@ +/** + * Sub-path mount support (cmms-embed fork). + * + * The CMMS serves this SPA under `/vendor/pcbjam/`, built with + * `vite build --base=/vendor/pcbjam/` — so `import.meta.env.BASE_URL` is that + * prefix (always a leading + trailing slash; "/" for a root deploy). + * + * `` already makes + * `` / `navigate()` / `useLocation()` mount-relative. This module covers + * the two things the router can't: + * 1. code that reads `window.location.pathname` raw (currentScope, WasmTool's + * quit-path), and + * 2. the hard cross-document `window.location.assign()` navigations — the + * WASM runtime is process-global, so a tool switch / File→Quit / new file + * is a full page load, not a router transition, and the app paths handed + * to it (`@pcbjam/shared` `projectPath()` etc.) are origin-absolute. + * + * Nothing here touches `@pcbjam/shared` — it also compiles for the backend, + * where `import.meta` does not exist. + */ + +/** `import.meta.env.BASE_URL` without its trailing slash ("" for a root deploy). */ +export const BASE_PREFIX = import.meta.env.BASE_URL.replace(/\/$/, ""); + +/** + * `pathname` with the mount prefix removed; the result always starts with "/". + * A pathname outside the mount (or a root deploy) is returned unchanged. + */ +export function stripBase(pathname: string): string { + if ( + BASE_PREFIX && + (pathname === BASE_PREFIX || pathname.startsWith(`${BASE_PREFIX}/`)) + ) { + const rest = pathname.slice(BASE_PREFIX.length); + return rest === "" ? "/" : rest; + } + return pathname; +} + +/** + * `window.location.assign()` to an app-absolute path (e.g. from + * `projectPath()`), re-adding the mount prefix. A full URL (has a scheme or is + * protocol-relative) passes through untouched — so an `APP_URL` redirect target + * is never corrupted. + * + * `win` is injectable for the same reason the tool-navigation / quit hooks take + * a window: test seams and the wx-port's frame window. + */ +export function hardNav( + appPath: string, + win: { location: { assign(url: string): void } } = window, +): void { + const url = /^([a-z][a-z0-9+.-]*:)?\/\//i.test(appPath) + ? appPath + : BASE_PREFIX + appPath; + win.location.assign(url); +} diff --git a/web/standalone/src/lib/config.ts b/web/standalone/src/lib/config.ts index 0685612..959f587 100644 --- a/web/standalone/src/lib/config.ts +++ b/web/standalone/src/lib/config.ts @@ -133,6 +133,7 @@ export const LOCAL_PROJECTS_ENABLED = import.meta.env.VITE_LOCAL_PROJECTS === "idb"; import { colorForUser, type PresenceUser } from "@pcbjam/shared"; +import { stripBase } from "@/lib/base-path"; import { sessionIdentity } from "@/lib/session-identity"; import type { ProviderConfig, ProviderKind } from "@/wasm/collab"; import { cdnLibsSource } from "@/wasm/libs/cdn-source"; @@ -282,7 +283,11 @@ export const PRESENCE_TUNER_ENABLED = import.meta.env.VITE_PRESENCE_TUNER === "1 */ export function currentScope(): string { if (typeof window !== "undefined") { - const seg = window.location.pathname.split("/").filter(Boolean)[0]; + // stripBase: under the CMMS sub-path mount the scope is the first segment + // AFTER /vendor/pcbjam/, not "vendor". No-op for a root deploy. + const seg = stripBase(window.location.pathname) + .split("/") + .filter(Boolean)[0]; if (seg && seg !== "projects" && seg !== "libs") { return decodeURIComponent(seg); } diff --git a/web/standalone/src/main.tsx b/web/standalone/src/main.tsx index bb194f1..ba35ef6 100644 --- a/web/standalone/src/main.tsx +++ b/web/standalone/src/main.tsx @@ -38,7 +38,10 @@ const queryClient = new QueryClient({ // instantiate exactly once per navigation. ReactDOM.createRoot(document.getElementById("root")!).render( - + {/* basename = the vite `base` (import.meta.env.BASE_URL) so the CMMS + sub-path mount (/vendor/pcbjam/) is transparent to /navigate(); + the hard location.assign() navs use hardNav() (lib/base-path.ts). */} + , diff --git a/web/standalone/src/pages/ProjectView.tsx b/web/standalone/src/pages/ProjectView.tsx index a79c2ab..00fbbb3 100644 --- a/web/standalone/src/pages/ProjectView.tsx +++ b/web/standalone/src/pages/ProjectView.tsx @@ -17,6 +17,7 @@ import { Trash2, } from "lucide-react"; import { fetchFileBytes, useProject, useSourceDescriptor } from "@/lib/api"; +import { hardNav } from "@/lib/base-path"; import { downloadBytes } from "@/lib/download"; import { localProjectStore } from "@/lib/project-source"; import { zipFiles } from "@/lib/zip"; @@ -108,7 +109,7 @@ export function ProjectView() { // scoped with a full reload; document tools create a templated file first. const launchTool = (tool: Tool) => { if (FILELESS_TOOLS.has(tool)) { - window.location.assign(projectToolPath(scope, slug, tool)); + hardNav(projectToolPath(scope, slug, tool)); } else { setNewFileTool(tool); }