269 lines
12 KiB
JavaScript
269 lines
12 KiB
JavaScript
(() => {
|
|
'use strict';
|
|
|
|
function loadCommandPlatformAssets() {
|
|
if (!document.querySelector('link[data-ux-command-platform]')) {
|
|
const stylesheet = document.createElement('link');
|
|
stylesheet.rel = 'stylesheet';
|
|
stylesheet.href = '/command-platform.css?v=1.44.0';
|
|
stylesheet.setAttribute('data-ux-command-platform', 'style');
|
|
document.head.append(stylesheet);
|
|
}
|
|
if (!document.querySelector('script[data-ux-command-platform]')) {
|
|
const script = document.createElement('script');
|
|
script.src = '/command-platform.js?v=1.44.0';
|
|
script.async = false;
|
|
script.setAttribute('data-ux-command-platform', 'script');
|
|
document.body.append(script);
|
|
}
|
|
}
|
|
|
|
function loadActionRegistryAssets() {
|
|
if (!document.querySelector('link[data-ux-action-registry]')) {
|
|
const stylesheet = document.createElement('link');
|
|
stylesheet.rel = 'stylesheet';
|
|
stylesheet.href = '/action-registry.css?v=1.44.0';
|
|
stylesheet.setAttribute('data-ux-action-registry', 'style');
|
|
document.head.append(stylesheet);
|
|
}
|
|
if (!document.querySelector('script[data-ux-action-registry]')) {
|
|
const script = document.createElement('script');
|
|
script.src = '/action-registry.js?v=1.44.0';
|
|
script.async = false;
|
|
script.setAttribute('data-ux-action-registry', 'script');
|
|
document.body.append(script);
|
|
}
|
|
}
|
|
|
|
function loadGlobalNavigationAssets() {
|
|
if (!document.querySelector('link[data-ux-global-navigation]')) {
|
|
const stylesheet = document.createElement('link');
|
|
stylesheet.rel = 'stylesheet';
|
|
stylesheet.href = '/global-navigation-ux.css?v=1.44.0';
|
|
stylesheet.setAttribute('data-ux-global-navigation', 'style');
|
|
document.head.append(stylesheet);
|
|
}
|
|
if (!document.querySelector('script[data-ux-global-navigation]')) {
|
|
const script = document.createElement('script');
|
|
script.src = '/global-navigation-ux.js?v=1.44.0';
|
|
script.async = false;
|
|
script.setAttribute('data-ux-global-navigation', 'script');
|
|
document.body.append(script);
|
|
}
|
|
}
|
|
|
|
function loadNavigationApi() {
|
|
if (document.querySelector('script[data-ux-navigation-api]')) return;
|
|
const script = document.createElement('script');
|
|
script.src = '/navigation-api.js?v=1.44.0';
|
|
script.async = false;
|
|
script.setAttribute('data-ux-navigation-api', 'script');
|
|
document.body.append(script);
|
|
}
|
|
|
|
function loadSafeActionAdapters() {
|
|
if (document.querySelector('script[data-ux-safe-action-adapters]')) return;
|
|
const script = document.createElement('script');
|
|
script.src = '/safe-action-adapters.js?v=1.44.0';
|
|
script.async = false;
|
|
script.setAttribute('data-ux-safe-action-adapters', 'script');
|
|
document.body.append(script);
|
|
}
|
|
|
|
loadCommandPlatformAssets();
|
|
loadActionRegistryAssets();
|
|
loadGlobalNavigationAssets();
|
|
loadNavigationApi();
|
|
loadSafeActionAdapters();
|
|
|
|
const moduleRoot = document.getElementById('admin-modules-panel');
|
|
const extensionRoot = document.getElementById('extensions');
|
|
if ((!moduleRoot && !extensionRoot) || document.documentElement.dataset.uxModuleExtensionReady === '1') return;
|
|
document.documentElement.dataset.uxModuleExtensionReady = '1';
|
|
|
|
const storageKey = 'vendoo_module_extension_workspace_v1';
|
|
const state = { moduleFilter: 'all', extensionFilter: 'all' };
|
|
try { Object.assign(state, JSON.parse(localStorage.getItem(storageKey) || '{}')); } catch {}
|
|
const persist = () => { try { localStorage.setItem(storageKey, JSON.stringify(state)); } catch {} };
|
|
|
|
const el = (tag, className, text) => {
|
|
const node = document.createElement(tag);
|
|
if (className) node.className = className;
|
|
if (text !== undefined) node.textContent = text;
|
|
return node;
|
|
};
|
|
|
|
function makeFilter(label, value, group, active) {
|
|
const button = el('button', 'ux-module-filter', label);
|
|
button.type = 'button';
|
|
button.dataset.uxModuleFilter = value;
|
|
button.dataset.uxModuleGroup = group;
|
|
button.setAttribute('aria-pressed', active ? 'true' : 'false');
|
|
return button;
|
|
}
|
|
|
|
function text(node) {
|
|
return String(node?.textContent || '').toLocaleLowerCase('de-DE');
|
|
}
|
|
|
|
function setupModules() {
|
|
if (!moduleRoot || moduleRoot.dataset.uxWorkspaceReady === '1') return;
|
|
moduleRoot.dataset.uxWorkspaceReady = '1';
|
|
|
|
const summary = moduleRoot.querySelector('#module-manager-summary');
|
|
const safety = moduleRoot.querySelector('#module-manager-safety');
|
|
const list = moduleRoot.querySelector('#module-manager-list');
|
|
const search = moduleRoot.querySelector('#module-manager-search');
|
|
const file = moduleRoot.querySelector('#module-manager-file');
|
|
const install = moduleRoot.querySelector('#module-manager-install');
|
|
const refreshButton = moduleRoot.querySelector('#module-manager-refresh');
|
|
if (!list) return;
|
|
|
|
const command = el('section', 'ux-module-command');
|
|
command.setAttribute('aria-label', 'Modulverwaltung');
|
|
const copy = el('div', 'ux-module-command-copy');
|
|
copy.append(el('span', 'ux-module-kicker', 'Systemerweiterungen'), el('h3', '', 'Module kontrolliert verwalten'), el('p', '', 'Abhängigkeiten, Vertrauensstatus und Betriebszustand zuerst prüfen. Änderungen bleiben bewusste Einzelaktionen.'));
|
|
const health = el('div', 'ux-module-health');
|
|
command.append(copy, health);
|
|
|
|
const filters = el('div', 'ux-module-filterbar');
|
|
[['Alle', 'all'], ['Aktiv', 'active'], ['Deaktiviert', 'disabled'], ['Installiert', 'external'], ['Probleme', 'problems']]
|
|
.forEach(([label, value]) => filters.append(makeFilter(label, value, 'modules', state.moduleFilter === value)));
|
|
|
|
const installDetails = el('details', 'ux-module-install');
|
|
const installSummary = el('summary');
|
|
installSummary.append(el('strong', '', 'Modulpaket installieren'), el('small', '', 'Nur geprüfte .vmod-Pakete'));
|
|
const installBody = el('div', 'ux-module-install-body');
|
|
if (file) installBody.append(file);
|
|
if (install) installBody.append(install);
|
|
installDetails.append(installSummary, installBody);
|
|
|
|
const tools = el('div', 'ux-module-tools');
|
|
if (search) tools.append(search);
|
|
if (refreshButton) tools.append(refreshButton);
|
|
tools.append(installDetails);
|
|
|
|
const anchor = summary || safety || list;
|
|
anchor.insertAdjacentElement('beforebegin', command);
|
|
command.insertAdjacentElement('afterend', filters);
|
|
filters.insertAdjacentElement('afterend', tools);
|
|
|
|
function classify(card) {
|
|
const value = text(card);
|
|
return {
|
|
active: value.includes('aktiv') && !value.includes('deaktiviert'),
|
|
disabled: value.includes('deaktiviert') || card.classList.contains('is-disabled'),
|
|
external: value.includes('installiert'),
|
|
problems: value.includes('fehler') || value.includes('ungültig') || value.includes('invalid') || card.classList.contains('is-invalid') || Boolean(card.querySelector('.module-error')),
|
|
};
|
|
}
|
|
|
|
function refresh() {
|
|
const cards = [...list.querySelectorAll('.module-card')];
|
|
const counts = { total: cards.length, active: 0, disabled: 0, external: 0, problems: 0 };
|
|
cards.forEach(card => {
|
|
const flags = classify(card);
|
|
Object.keys(flags).forEach(key => { if (flags[key]) counts[key] += 1; });
|
|
const visible = state.moduleFilter === 'all' || flags[state.moduleFilter];
|
|
card.hidden = !visible;
|
|
card.tabIndex = visible ? 0 : -1;
|
|
});
|
|
health.replaceChildren();
|
|
[['Gesamt', counts.total], ['Aktiv', counts.active], ['Installiert', counts.external], ['Probleme', counts.problems]]
|
|
.forEach(([label, value]) => {
|
|
const item = el('div', 'ux-module-health-item');
|
|
item.append(el('small', '', label), el('strong', '', String(value)));
|
|
if (label === 'Probleme' && value > 0) item.dataset.tone = 'danger';
|
|
health.append(item);
|
|
});
|
|
filters.querySelectorAll('[data-ux-module-filter]').forEach(button => button.setAttribute('aria-pressed', button.dataset.uxModuleFilter === state.moduleFilter ? 'true' : 'false'));
|
|
}
|
|
|
|
filters.addEventListener('click', event => {
|
|
const button = event.target.closest('[data-ux-module-filter]');
|
|
if (!button) return;
|
|
state.moduleFilter = button.dataset.uxModuleFilter;
|
|
persist();
|
|
refresh();
|
|
list.querySelector('.module-card:not([hidden])')?.focus({ preventScroll: true });
|
|
});
|
|
|
|
list.addEventListener('keydown', event => {
|
|
const card = event.target.closest('.module-card');
|
|
if (!card || !['Enter', ' '].includes(event.key)) return;
|
|
if (event.target.closest('button, a, input, select')) return;
|
|
event.preventDefault();
|
|
card.querySelector('.module-card-actions button:not(:disabled), .module-card-actions a')?.focus();
|
|
});
|
|
|
|
const observer = new MutationObserver(() => requestAnimationFrame(refresh));
|
|
observer.observe(list, { childList: true });
|
|
refresh();
|
|
window.addEventListener('beforeunload', () => observer.disconnect(), { once: true });
|
|
}
|
|
|
|
function setupExtensions() {
|
|
if (!extensionRoot || extensionRoot.dataset.uxWorkspaceReady === '1') return;
|
|
extensionRoot.dataset.uxWorkspaceReady = '1';
|
|
const grid = extensionRoot.querySelector('#extensions-grid');
|
|
const activeBrowser = extensionRoot.querySelector('#extensions-active-browser');
|
|
const prepare = extensionRoot.querySelector('#extensions-prepare-active');
|
|
if (!grid) return;
|
|
|
|
const board = el('section', 'ux-extension-board');
|
|
const copy = el('div');
|
|
copy.append(el('span', 'ux-module-kicker', 'Browser-Verbindungen'), el('h3', '', 'Extensions vorbereiten und prüfen'), el('p', '', 'Vendoo bereitet Pakete und Pfade vor. Die endgültige Browserfreigabe bleibt immer manuell.'));
|
|
const status = el('div', 'ux-extension-status');
|
|
board.append(copy, status);
|
|
|
|
const filters = el('div', 'ux-module-filterbar ux-extension-filterbar');
|
|
[['Alle', 'all'], ['Bereit', 'ready'], ['Verbunden', 'connected'], ['Aktion nötig', 'attention']]
|
|
.forEach(([label, value]) => filters.append(makeFilter(label, value, 'extensions', state.extensionFilter === value)));
|
|
|
|
grid.insertAdjacentElement('beforebegin', filters);
|
|
filters.insertAdjacentElement('beforebegin', board);
|
|
if (activeBrowser) status.append(activeBrowser);
|
|
if (prepare) status.append(prepare);
|
|
|
|
function classify(card) {
|
|
const value = text(card);
|
|
return {
|
|
connected: value.includes('verbunden') || value.includes('aktiv'),
|
|
ready: value.includes('bereit') || value.includes('vorbereitet') || value.includes('installiert'),
|
|
attention: value.includes('nicht') || value.includes('fehler') || value.includes('aktion') || value.includes('installieren'),
|
|
};
|
|
}
|
|
|
|
function refresh() {
|
|
const cards = [...grid.children].filter(node => node.nodeType === 1);
|
|
let connected = 0;
|
|
let attention = 0;
|
|
cards.forEach(card => {
|
|
const flags = classify(card);
|
|
if (flags.connected) connected += 1;
|
|
if (flags.attention) attention += 1;
|
|
card.hidden = !(state.extensionFilter === 'all' || flags[state.extensionFilter]);
|
|
});
|
|
board.dataset.connected = String(connected);
|
|
board.dataset.attention = String(attention);
|
|
filters.querySelectorAll('[data-ux-module-filter]').forEach(button => button.setAttribute('aria-pressed', button.dataset.uxModuleFilter === state.extensionFilter ? 'true' : 'false'));
|
|
}
|
|
|
|
filters.addEventListener('click', event => {
|
|
const button = event.target.closest('[data-ux-module-filter]');
|
|
if (!button) return;
|
|
state.extensionFilter = button.dataset.uxModuleFilter;
|
|
persist();
|
|
refresh();
|
|
});
|
|
|
|
const observer = new MutationObserver(() => requestAnimationFrame(refresh));
|
|
observer.observe(grid, { childList: true, subtree: true, characterData: true });
|
|
refresh();
|
|
window.addEventListener('beforeunload', () => observer.disconnect(), { once: true });
|
|
}
|
|
|
|
setupModules();
|
|
setupExtensions();
|
|
})();
|