CMMS fork: embedding hooks (window.PCBRETRACE_CONFIG)

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.
This commit is contained in:
Stuart 2026-09-03 14:16:42 +10:00
commit 8bf5fd3a9a
3 changed files with 170 additions and 17 deletions

133
docs/cmms-bridge.js Normal file
View file

@ -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 };
})();