Files
vendoo/public/modules/module-manager/module-manager.js
T
Masterluke77andGitHub 40071f718e Vendoo 1.43.0 – Production Update
Manifestbasierter Production-Updater 3.3, Modulnavigation, optionales Produktbild Studio, Listing-Studio-Aktionsleiste sowie plattformübergreifende Windows-/Linux-Release-Gates.
2026-07-10 13:16:21 +02:00

192 lines
9.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const state = { data: null, filter: '', busy: false };
function csrfToken() {
const match = document.cookie.match(/(?:^|;\s*)csrf_token=([^;]+)/);
return match ? decodeURIComponent(match[1]) : '';
}
async function request(method, url, body, { multipart = false } = {}) {
const headers = { 'X-Vendoo-Request-ID': globalThis.crypto?.randomUUID?.() || `modules-${Date.now()}` };
const options = { method, credentials: 'same-origin', headers };
if (!['GET', 'HEAD'].includes(method)) headers['X-CSRF-Token'] = csrfToken();
if (body !== undefined) {
if (multipart) options.body = body;
else { headers['Content-Type'] = 'application/json'; options.body = JSON.stringify(body); }
}
const response = await fetch(url, options);
if (response.status === 401) { location.href = '/login.html'; return null; }
const type = response.headers.get('content-type') || '';
const payload = type.includes('json') ? await response.json().catch(() => ({})) : await response.text();
if (!response.ok) throw new Error(payload?.error || payload?.message || payload || `HTTP ${response.status}`);
return payload;
}
function el(tag, className, text) {
const node = document.createElement(tag);
if (className) node.className = className;
if (text !== undefined) node.textContent = text;
return node;
}
function button(label, action, id, { danger = false, disabled = false } = {}) {
const node = el('button', danger ? 'secondary-btn danger' : 'secondary-btn', label);
node.type = 'button';
node.dataset.moduleAction = action;
node.dataset.moduleId = id;
node.disabled = disabled;
return node;
}
function renderSummary(data) {
const root = document.getElementById('module-manager-summary');
if (!root) return;
root.replaceChildren();
const items = [
['Module gesamt', data.summary.total], ['Aktiv', data.summary.enabled], ['Deaktiviert', data.summary.disabled],
['Nativ', data.summary.native], ['Installiert', data.summary.external],
];
for (const [label, value] of items) {
const card = el('article', 'module-summary-card');
card.append(el('span', '', label), el('strong', '', String(value)));
root.append(card);
}
const safety = document.getElementById('module-manager-safety');
if (safety) {
safety.className = `module-safety-card${data.safety.isolatedHostAvailable ? '' : ' is-warning'}`;
safety.replaceChildren();
const text = el('div');
text.append(el('strong', '', 'Sicherheitsmodus'), el('p', '', data.safety.isolatedHostAvailable
? 'Signierte Fremdmodule laufen ausschließlich im isolierten Extension-Host.'
: 'Deklarative Module sind aktivierbar. Ausführbarer Fremdcode bleibt bis zum isolierten Extension-Host quarantänisiert.'));
safety.append(text);
}
}
function renderModules() {
const root = document.getElementById('module-manager-list');
if (!root) return;
root.replaceChildren();
const query = state.filter.trim().toLowerCase();
const modules = (state.data?.modules || []).filter(item => !query || [item.id, item.name, item.description, item.status, item.installSource].join(' ').toLowerCase().includes(query));
if (!modules.length) { root.append(el('div', 'module-empty', 'Keine passenden Module gefunden.')); return; }
for (const item of modules) {
const card = el('article', `module-card ${item.enabled ? '' : 'is-disabled'} ${item.state === 'invalid' ? 'is-invalid' : ''}`.trim());
const head = el('header', 'module-card-head');
const titleWrap = el('div', 'module-card-title');
titleWrap.append(el('span', `module-state-dot is-${item.state || (item.enabled ? 'running' : 'disabled')}`));
const title = el('div');
title.append(el('h4', '', item.name || item.id), el('code', '', item.id));
titleWrap.append(title);
const version = el('strong', '', item.version ? `v${item.version}` : '—');
head.append(titleWrap, version);
card.append(head);
const badges = el('div', 'module-badges');
badges.append(el('span', 'module-badge', item.enabled ? 'Aktiv' : 'Deaktiviert'));
badges.append(el('span', 'module-badge', item.status === 'legacy-bridge' ? 'Legacy Bridge' : item.status === 'native' ? 'Nativ' : item.status));
badges.append(el('span', 'module-badge', item.installSource === 'builtin' ? 'Vendoo' : 'Installiert'));
if (item.protected) badges.append(el('span', 'module-badge', 'Geschützt'));
if (item.package?.trust?.signed) badges.append(el('span', 'module-badge', item.package.trust.trusted ? 'Signatur vertraut' : 'Signatur unbekannt'));
card.append(badges);
card.append(el('p', 'module-description', item.description || 'Keine Beschreibung vorhanden.'));
if (item.requires?.length) {
const deps = el('div', 'module-dependencies');
deps.append(el('strong', '', 'Benötigt: '), document.createTextNode(item.requires.join(', ')));
card.append(deps);
}
if (item.dependents?.length) {
const deps = el('div', 'module-dependencies');
deps.append(el('strong', '', 'Wird benötigt von: '), document.createTextNode(item.dependents.join(', ')));
card.append(deps);
}
if (item.package?.error || item.error) card.append(el('div', 'module-error', item.package?.error || item.error));
const actions = el('div', 'module-card-actions');
if (item.actions?.enable) actions.append(button('Aktivieren', 'enable', item.id));
if (item.actions?.disable) actions.append(button('Deaktivieren', 'disable', item.id, { danger: true }));
if (item.actions?.export) {
const link = el('a', 'secondary-btn', 'Exportieren');
link.href = `/api/admin/modules/${encodeURIComponent(item.id)}/export`;
link.download = `${item.id}.vmod`;
actions.append(link);
}
if (item.actions?.remove) actions.append(button('Löschen', 'remove', item.id, { danger: true }));
if (item.protected && !actions.childNodes.length) actions.append(el('span', 'module-dependencies', 'Kernmodul Änderungen gesperrt'));
card.append(actions);
root.append(card);
}
}
async function load() {
const list = document.getElementById('module-manager-list');
if (!list) return;
list.replaceChildren(el('div', 'module-empty', 'Module werden geladen …'));
try {
state.data = await request('GET', '/api/admin/modules');
renderSummary(state.data);
renderModules();
} catch (error) {
list.replaceChildren(el('div', 'module-error', error.message));
}
}
async function perform(action, id) {
if (state.busy) return;
state.busy = true;
try {
if (action === 'enable') await request('POST', `/api/admin/modules/${encodeURIComponent(id)}/enable`, {});
if (action === 'disable') {
const item = state.data?.modules?.find(module => module.id === id);
const hasDependents = Boolean(item?.dependents?.length);
const message = hasDependents
? `Dieses Modul wird von ${item.dependents.join(', ')} benötigt. Abhängige optionale Module ebenfalls deaktivieren?`
: 'Modul wirklich deaktivieren? Die zugehörigen API-Funktionen stehen danach bis zur Reaktivierung nicht zur Verfügung.';
if (!confirm(message)) return;
await request('POST', `/api/admin/modules/${encodeURIComponent(id)}/disable`, { cascade: hasDependents });
}
if (action === 'remove') {
if (!confirm('Installiertes Modul dauerhaft löschen? Die exportierte .vmod-Datei sollte vorher gesichert werden.')) return;
await request('DELETE', `/api/admin/modules/${encodeURIComponent(id)}`);
}
await load();
window.dispatchEvent(new CustomEvent('vendoo:modules-changed', { detail: { action, id } }));
globalThis.toast?.(action === 'enable' ? 'Modul aktiviert.' : action === 'disable' ? 'Modul deaktiviert.' : 'Modul gelöscht.');
} catch (error) {
globalThis.toast?.(error.message);
if (!globalThis.toast) alert(error.message);
} finally { state.busy = false; }
}
async function install() {
const input = document.getElementById('module-manager-file');
const file = input?.files?.[0];
if (!file) { globalThis.toast?.('Bitte zuerst eine .vmod-Datei auswählen.'); return; }
if (!file.name.toLowerCase().endsWith('.vmod')) { globalThis.toast?.('Nur .vmod-Pakete werden akzeptiert.'); return; }
const form = new FormData();
form.append('module', file, file.name);
try {
await request('POST', '/api/admin/modules/install', form, { multipart: true });
input.value = '';
await load();
window.dispatchEvent(new CustomEvent('vendoo:modules-changed', { detail: { action: 'install' } }));
globalThis.toast?.('Modulpaket geprüft und installiert.');
} catch (error) {
globalThis.toast?.(error.message);
if (!globalThis.toast) alert(error.message);
}
}
function setup() {
document.getElementById('module-manager-refresh')?.addEventListener('click', load);
document.getElementById('module-manager-install')?.addEventListener('click', install);
document.getElementById('module-manager-search')?.addEventListener('input', event => { state.filter = event.target.value; renderModules(); });
document.getElementById('module-manager-list')?.addEventListener('click', event => {
const target = event.target.closest('[data-module-action]');
if (target) perform(target.dataset.moduleAction, target.dataset.moduleId);
});
}
document.addEventListener('DOMContentLoaded', setup, { once: true });
globalThis.VendooModuleManager = Object.freeze({ load });