From 8bf5fd3a9a025298750af53cf389915cb8fa01b7 Mon Sep 17 00:00:00 2001 From: Stuart Date: Thu, 3 Sep 2026 14:16:42 +1000 Subject: [PATCH] CMMS fork: embedding hooks (window.PCBRETRACE_CONFIG) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New docs/cmms-bridge.js — inert unless window.PCBRETRACE_CONFIG is present (only public/vendor/pcb-retrace/edit.php in the CMMS sets it). When set: - injects "Save" / "Save & close" into the top bar; hides the standalone import/export/share buttons. - on boot, if config.loadUrl is set, pulls the project .pcbretrace.zip from the CMMS and restores it silently (reuses processUrlImport). - Save builds the whole-device ZIP in memory and POSTs it multipart to the CMMS attachment endpoint (action/csrf/job_id/id/original_name/file, same contract as circuit-edit.php); first save of a new board switches to in-place updates. studio.js: - exportDeviceZIP() split into buildDeviceZIP(pass, forCmms) -> {blob, filename} + a thin exportDeviceZIP() wrapper. forCmms adjusts the embedded README + manifest.source. - importFile/processZIP/processUrlImport take an opts arg; opts.silent skips the restore/import confirms and re-throws instead of alert(). - init() awaits window.CMMS.init() at the end when present. studio.html: loads cmms-bridge.js (defer, after studio.js). Standalone pcb.etaras.com / local use is unchanged — every new path is gated on the config object or opts.silent, both absent by default. Dark mode: config.dark is recorded as data-theme="dark" but studio.html is light-only today; CMMS-theme-follow is a later task. --- docs/cmms-bridge.js | 133 ++++++++++++++++++++++++++++++++++++++++++++ docs/studio.html | 3 + docs/studio.js | 51 +++++++++++------ 3 files changed, 170 insertions(+), 17 deletions(-) create mode 100644 docs/cmms-bridge.js diff --git a/docs/cmms-bridge.js b/docs/cmms-bridge.js new file mode 100644 index 0000000..afeb566 --- /dev/null +++ b/docs/cmms-bridge.js @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2025-2026 Taras Greben (upstream) — CMMS fork addition. + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial-pcb-retrace + * + * cmms-bridge.js — glue between pcb-retrace and the CMMS attachment system. + * + * Completely inert unless window.PCBRETRACE_CONFIG is present, which only + * public/vendor/pcb-retrace/edit.php sets. Standalone pcb.etaras.com / local + * use is unaffected. + * + * PCBRETRACE_CONFIG = { + * loadUrl: GET url for the project .pcbretrace.zip (omit for a new board) + * saveUrl: POST target ("/attachment.php") + * saveAction: "pcb_retrace_save" | "pcb_retrace_save_new" + * attachmentId: existing attachment id (omit for a new board) + * jobId: owner scope token "entityType:entityId", echoed back on save + * csrfToken: CMMS CSRF token + * originalName: filename to save as + * returnTo: url to navigate to after "Save & close" + * dark: boolean (studio.html is light-only today — recorded, not styled) + * } + * + * Relies on globals from studio.js (same page, classic scripts share scope): + * db, currentDeviceId, buildDeviceZIP(), processUrlImport(). + * window.CMMS.init() is awaited at the end of studio.js's init(). + */ +(function () { + 'use strict'; + + const CFG = window.PCBRETRACE_CONFIG; + if (!CFG) return; + + let saveInFlight = false; + let statusEl = null; + + function status(msg, isError) { + if (!statusEl) return; + statusEl.textContent = msg || ''; + statusEl.style.color = isError ? '#b91c1c' : '#475569'; + if (msg && !isError) { + clearTimeout(status._t); + status._t = setTimeout(() => { if (statusEl.textContent === msg) statusEl.textContent = ''; }, 4000); + } + } + + function injectControls() { + const topBar = document.querySelector('.top-bar'); + if (!topBar) return; + + const mk = (label, title, primary) => { + const b = document.createElement('button'); + b.textContent = label; + b.title = title; + b.className = primary ? 'primary' : 'secondary'; + b.style.cssText = 'white-space:nowrap;' + (primary ? 'background:#2563eb;color:#fff;border-color:#2563eb;' : ''); + return b; + }; + + const saveBtn = mk('💾 Save', 'Save this board back to the CMMS', true); + const saveExitBtn = mk('Save & close', 'Save and return to the CMMS', false); + statusEl = document.createElement('span'); + statusEl.style.cssText = 'font-size:0.8rem; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:180px;'; + + saveBtn.addEventListener('click', () => save(false)); + saveExitBtn.addEventListener('click', () => save(true)); + + const box = document.createElement('div'); + box.style.cssText = 'margin-left:auto; display:flex; gap:6px; align-items:center; flex:0 0 auto;'; + box.append(statusEl, saveBtn, saveExitBtn); + topBar.appendChild(box); + + // Hide the standalone import/export/share buttons — the CMMS owns + // persistence here. (The whole-device drag-drop still works.) + ['Import from URL', 'Import Device', 'Export Device'].forEach(t => { + const el = document.querySelector('#device-actions button[title="' + t + '"]'); + if (el) el.style.display = 'none'; + }); + } + + async function save(thenExit) { + if (saveInFlight) return; + if (!currentDeviceId) { status('Nothing to save yet', true); return; } + saveInFlight = true; + status('Saving…'); + try { + const name = CFG.originalName || 'board.pcbretrace.zip'; + const { blob } = await buildDeviceZIP(null, true); + const fd = new FormData(); + fd.append('action', CFG.saveAction); + fd.append('csrf_token', CFG.csrfToken); + fd.append('job_id', CFG.jobId); + fd.append('id', CFG.attachmentId ? String(CFG.attachmentId) : ''); + fd.append('original_name', name); + fd.append('file', blob, name); + + const r = await fetch(CFG.saveUrl, { method: 'POST', body: fd, credentials: 'same-origin' }); + const data = await r.json().catch(() => { throw new Error('HTTP ' + r.status); }); + if (!data || !data.ok) throw new Error((data && data.error) || ('HTTP ' + r.status)); + + // First save of a new board — switch to updating it in place. + if (!CFG.attachmentId && data.id) { + CFG.attachmentId = String(data.id); + CFG.saveAction = 'pcb_retrace_save'; + } + status('Saved ✓'); + if (thenExit && CFG.returnTo) window.location.href = CFG.returnTo; + } catch (e) { + status('Save failed: ' + (e.message || e), true); + } finally { + saveInFlight = false; + } + } + + async function init() { + if (CFG.dark) document.documentElement.setAttribute('data-theme', 'dark'); + injectControls(); + + if (CFG.loadUrl) { + status('Loading board…'); + try { + // processUrlImport → importFile → processZIP → restoreDevice, + // which refreshes the device/board UI itself. silent: skip the + // "restore?" confirm for a trusted CMMS attachment. + await processUrlImport(CFG.loadUrl, { silent: true }); + status(''); + } catch (e) { + status('Could not load the saved board: ' + (e.message || e), true); + } + } + } + + window.CMMS = { init, save }; +})(); diff --git a/docs/studio.html b/docs/studio.html index ecaa43a..e16355b 100644 --- a/docs/studio.html +++ b/docs/studio.html @@ -27,6 +27,9 @@ + +