/*
* SysDeck — 3rd-Party Modules Panel (v0.0.46)
* Author: Jeremy Anderson (https://dcos.net)
*
* Catalog-driven installer for third-party Cockpit modules.
*
* DESIGN (per v0.0.46 directive): the license, developer/author,
* 3rd-party module name, and a homepage link are rendered INLINE
* in every catalog row. The Install button is a TRUE 1-click install
* — clicking it is the operator's acceptance of the inline-displayed
* license. No modal, no extra confirmation step.
*
* The bridge helper (modules3p.py) still refuses silent installs:
* install() requires --accept-license=1. The JS always passes that
* flag on every click, because the license is shown inline next to
* the button — clicking Install IS the acceptance gesture.
*
* Flow:
* 1. bridge.modules3p.status() → renders rows
* 2. operator clicks Install on a row
* 3. bridge.modules3p.install(id, true) → 1 click, runs as root via polkit
* 4. bridge appends an audit record to /etc/cockpit/MODULE_LICENSES.log
* 5. JS re-renders the catalog
*
* Uninstall follows the same pattern: 1 click, runs uninstall, audit.
*/
const LICENSE_SHORT_NAMES = {
"MIT": { cls: "success", note: "permissive — retains copyright + notice" },
"LGPL-2.1": { cls: "info", note: "weak copyleft — derivatives of the library must stay LGPL" },
"LGPL-2.1+": { cls: "info", note: "weak copyleft — or-later" },
"GPL-2.0": { cls: "warn", note: "copyleft — derivative works must be GPL-2.0+" },
"GPL-2.0+": { cls: "warn", note: "copyleft — or-later" },
"GPL-3.0": { cls: "warn", note: "copyleft — derivative works must be GPL-3.0+" },
"AGPL-3.0": { cls: "warn", note: "strong copyleft — network use triggers source disclosure" },
"Apache-2.0": { cls: "success", note: "permissive — retains NOTICE file + patent grant" },
"BSD-2-Clause": { cls: "success", note: "permissive — retains copyright + notice" },
"BSD-3-Clause": { cls: "success", note: "permissive — retains copyright + notice + no endorsement" },
};
function licenseBadge(lic) {
const meta = LICENSE_SHORT_NAMES[lic] || { cls: "info", note: "see upstream for terms" };
return ``;
}
function esc(s) {
return String(s).replace(/[&<>"']/g, (c) => ({
"&": "&", "<": "<", ">": ">", '"': """, "'": "'",
})[c]);
}
export async function mount(panel, { bridge, EventBus }) {
panel.innerHTML = renderSkeleton();
let catalog = [];
try {
catalog = await bridge.modules3p.status();
} catch (err) {
panel.innerHTML = renderError("Catalog unavailable", err);
return;
}
panel.innerHTML = renderShell(catalog);
wireUp(panel, catalog, bridge);
}
function renderShell(catalog) {
const categories = unique(catalog.map((m) => m.category)).sort();
const total = catalog.length;
const installed = catalog.filter((m) => m.installed).length;
const missingDeps = catalog.filter((m) => m.missing_deps?.length > 0).length;
return `
Optional add-ons. License, developer, source, and homepage
are shown inline next to each Install button — clicking
Install is your acceptance of the displayed license.
3rd-Party Cockpit Modules
Append-only record at /etc/cockpit/MODULE_LICENSES.log.
Every install / uninstall writes one JSON line with module id,
license, author, source URL, action, and UTC timestamp.
Click Refresh to load.
SysDeck (MIT) invokes every upstream tool as a separate
process via cockpit.spawn — no third-party
code is bundled into the SysDeck tarball. Each module's
license applies only to the module itself, not to SysDeck.
For GPL-family modules (45Drives Navigator / File Sharing /
ZFS Manager, cockpit-pacman) the invocation boundary is a
subprocess call; you remain responsible for complying with
each upstream license when distributing the combined system.
See THIRD_PARTY.md for the full license
compatibility matrix.
${esc(m.blurb)}
${esc(err.message || err)}.
Run python3 /usr/lib/sysdeck/bridge/modules3p.py status on the host to debug.
Loading…
`; try { const r = await bridge.modules3p.audit(); body.innerHTML = renderAudit(r.records || []); } catch (e) { body.innerHTML = `Failed: ${esc(e.message || e)}
`; } }); } function showRowFlash(flashBox, msg, kind) { if (!flashBox) return; flashBox.innerHTML = `${esc(msg)}`; } function renderAudit(records) { if (!records.length) { return `No records yet — installs and uninstalls will appear here.
`; } return `| Time (UTC) | Module | License | Author | Action | Detail |
|---|---|---|---|---|---|
| ${esc(r.raw)} | |||||
| ${esc(r.ts || "")} | ${esc(r.module || r.id || "")} | ${esc(r.license || "")} | ${esc(r.author || "")} | ${esc(r.action || "")} | ${esc(r.detail || "")} |