dadssad
All checks were successful
Deploy / deploy-staging (push) Successful in 24s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-06-22 01:16:50 +02:00
parent ba8794c371
commit f83a64b854
12 changed files with 489 additions and 2 deletions

View File

@@ -35,6 +35,7 @@ Zentraler Dokumentationsindex fuer das Projekt.
- Keycloak bleibt das Auth-System.
- API und Desktop laufen auf demselben Host. Zielpfad ist `(staging.)desktop.kusche.berlin/api/v1/...`.
- der Theme-Handoff ist in [keycloak-theme-handoff.md](/home/lars/Schreibtisch/Projekte/desktop.kusche.berlin/docs/keycloak-theme-handoff.md) beschrieben.
- das globale Admin-Debugging ist Teil der Desktop-Shell und wird nicht pro App als eigene Grundfunktion dupliziert
## Dokumentationsregel
@@ -51,3 +52,4 @@ Aktuell wichtig:
- der Mining-Checker ist das erste als echtes Desktop-Modul angebundene Modul unter `modules/mining-checker/`
- Modul-Assets koennen ueber einen gemeinsamen Auslieferungsweg aus dem Modul selbst geladen werden
- die API bleibt vorerst auf demselben Host und wird nicht auf `api.desktop.kusche.berlin` ausgelagert
- das gemeinsame Admin-Debug-Widget neben der Uhr ist der Standard fuer Live-Debugging in Desktop und Native-Apps

View File

@@ -28,6 +28,7 @@ Empfohlene Ergänzungen:
- `/public/assets/desktop/`
- globale Desktop-CSS
- JavaScript für Fenster, Dock, Tray, Desktop-Interaktion
- globale Debug-Infrastruktur fuer Admin-Widget, Debug-Bus und Debug-Fenster
- `/partials/desktop/`
- Desktop Shell
- Window Templates
@@ -51,6 +52,7 @@ Das bedeutet:
- der neue Desktop-Layer ist nur die neue Oberfläche
- Module dürfen nicht ungeordnet in den Desktop-Core verschoben werden
- gemeinsame Desktop-Mechaniken liegen nicht in Modulen, sondern im globalen Kern
- gemeinsame Debug-Grundlogik liegt ebenfalls nicht in Modulen, sondern im globalen Desktop-Core
## Trennung
@@ -64,6 +66,7 @@ Es gibt drei Ebenen:
- keine Vermischung von Fenster-Manager-Code mit Modulbusinesslogik
- keine Skin-spezifischen Sonderlösungen in einzelnen Modulen
- keine eigene globale Debug-Basis je Modul oder App
- keine harte Kopplung zwischen alter Seitenstruktur und neuem Desktop-Verhalten
- keine technische Abhängigkeit zu `Old-Nexus/`
- keine Includes, Imports, Autoloading-Pfade oder Assets aus `Old-Nexus/`
@@ -75,6 +78,7 @@ Die neue Oberfläche muss so gebaut sein, dass:
- Module später als Fenster gerendert werden können
- Apps, Widgets und Seitenmodule aus einer gemeinsamen Registry kommen
- Skins ausschließlich Layout, Stil und Interaktionsdetails definieren
- globale Admin-Werkzeuge wie Debug als gemeinsame Desktop-Funktion bereitgestellt werden
## Sonderregel für `Old-Nexus/`

View File

@@ -26,6 +26,8 @@ Rahmenbedingungen:
- Die neue Oberfläche ist eine Desktop-Shell, keine klassische Website.
- Root `/` soll die Desktop-Oberfläche sein.
- Skins sollen als `Windows`, `Apple`, `Linux` angelegt werden, technisch aber auf einer gemeinsamen Shell beruhen.
- Globale Desktop-Funktionen wie Admin-Debugging sollen zentral in der Shell liegen und nicht pro App neu gebaut werden.
- Der Standard fuer Live-Debugging ist ein gemeinsames Admin-Debug-Widget neben der Uhr mit eigenem Debug-Fenster und gemeinsamem Debug-Bus.
- Spätere Modul-Anbindung muss vorbereitet werden, aber die Shell ist zuerst zu bauen.
Erste Aufgabe:
@@ -40,5 +42,6 @@ Wichtig:
- keine Arbeiten mehr im alten Nexus durchführen
- keine Modulmigration im Blindflug
- keine Vermischung von alter Website-Navigation und neuer Desktop-Interaktion
- keine neue globale Debug-UI pro App bauen, wenn die Desktop-Shell diese Funktion zentral bereitstellen soll
- `Old-Nexus/` nur lesen, nie technisch verwenden
```

View File

@@ -26,6 +26,7 @@ Verbindlich ist:
- Desktop-Mechaniken zentral halten, nicht pro App duplizieren
- Fensterverhalten konsistent halten
- gemeinsame Standards fuer Fensterinhalte nutzen, sofern eine App kein bewusst eigenes UI benoetigt
- globale Debug-Infrastruktur zentral halten und ueber alle Apps wiederverwenden
- Begriffe in UI und Doku konsistent nach `CONTENT.md` verwenden
- Apps als bereitgestellte Systemfunktionen denken, nicht nur als sichtbare Fenster
- Unterschiede zwischen `App`, `Infobereich` und `Widget-Bereich` sauber trennen
@@ -41,6 +42,7 @@ Verbindlich ist:
- keine Rueckbauten bestehender Desktop-Mechaniken ohne ausdrueckliche Entscheidung
- keine Vermischung von Modul-Businesslogik mit globalem Desktop-Core
- keine Skin-Sonderlogik direkt in einzelnen Apps, wenn sie global loesbar ist
- keine neue globale Debug-Grundlogik pro App, wenn der Desktop-Debug-Bus und das zentrale Debug-Fenster genutzt werden koennen
- keine nur lokalen README-Informationen ohne zentrale Uebernahme der wichtigen Punkte
- keine Hilfeinhalte nur in Chatverlaeufen oder Ad-hoc-Notizen belassen
- keine produktive Architekturabhaengigkeit zu `Old-Nexus/` oder vergleichbaren Altbestaenden
@@ -51,6 +53,7 @@ Verbindlich ist:
- `modules/<modul>/` bleibt Ort fuer klassische Module
- globale Desktop-Mechaniken liegen im gemeinsamen Kern
- globale Modul-Helfer duerfen im gemeinsamen Kern liegen, Fachlogik aber nicht
- globale Debug-Steuerung fuer Admins liegt im Desktop-Core; Aktivierung erfolgt ueber das gemeinsame Debug-Widget neben der Uhr
- API und Desktop teilen sich im aktuellen Zielbild denselben Host; offizielle Basis ist `/api/v1/...`
- Skins definieren Darstellung und Interaktionsdetails, nicht die Fachlogik
- Hilfe- und Inhaltsdateien sollen spaeter maschinenlesbar oder zumindest klar strukturierbar in einen Hilfebereich ueberfuehrt werden koennen
@@ -76,3 +79,5 @@ Bei jeder groesseren Aenderung ist zu pruefen:
- Benutzerdaten sollen spaeter sauber in LDAP oder Keycloak geschrieben werden
- Standard-Fensterinhalte sollen ein gemeinsames Design nutzen
- Desktop-Nutzereinstellungen liegen in einer eigenen Tabelle mit erkennbarem `_user_data`-Suffix
- das globale Admin-Debug-Fenster ist der Standardweg fuer Live-Debugging in Desktop und Native-Apps
- Debug-Tracking soll nur waehrend eines geoeffneten globalen Debug-Fensters laufen; der letzte Status soll einen Reload pro Benutzer ueberstehen

View File

@@ -15,6 +15,15 @@ Ein Desktop-Modul kann zusaetzlich diese Projektdateien besitzen:
Module-Assets werden in diesem Projekt nicht direkt aus `public/assets/apps/...` dupliziert, sondern ueber den generischen Endpoint `/module-assets/index.php` aus dem jeweiligen Modul ausgeliefert.
## Globale Desktop-Standards fuer Module
- gemeinsame Desktop-Mechaniken wie Fenster, Tray, globale Persistenz und Debug-Infrastruktur liegen im Desktop-Core
- Module und andere Apps sollen keine eigene Grund-Debug-Oberflaeche im Stil eines separaten globalen Debuggers bauen
- fuer Debugging im Desktop gilt das zentrale Admin-Debug-Widget neben der Uhr mit eigenem Debug-Fenster als Standard
- Debug-Events aus Modulen sollen in den gemeinsamen Desktop-Debug-Bus geschrieben werden, damit sie im globalen Debug-Fenster sichtbar sind
- wenn das globale Debug-Fenster geschlossen ist, sollen Module kein dauerhaft aktives Live-Debugging erzwingen
- app-spezifische Debug-Darstellungen sind nur zulaessig, wenn sie fachliche Zusatzinformationen zeigen, die ueber den globalen Stream hinausgehen
## Pflegehinweis
Diese Datei muss gepflegt bleiben, wenn sich Modulstruktur oder Modulregeln aendern.

View File

@@ -46,7 +46,11 @@
return [];
}
})();
const initialDebugMode = normalizedOptions.moduleDebugEnabled === true
const desktopDebugBus = window.__desktopDebugBus && typeof window.__desktopDebugBus === 'object'
? window.__desktopDebugBus
: null;
const initialDebugMode = (desktopDebugBus && desktopDebugBus.enabled === true)
|| normalizedOptions.moduleDebugEnabled === true
|| (document.body && document.body.dataset.moduleDebugEnabled === '1');
const fallbackSections = [
['overview', 'Übersicht'],
@@ -88,11 +92,12 @@
function setCookie(name, value, maxAgeSeconds) {
document.cookie = `${name}=${encodeURIComponent(value)}; path=/; max-age=${maxAgeSeconds}; samesite=lax`;
}
const debugBus = window.__nexusDebugBus || {
const debugBus = desktopDebugBus || window.__nexusDebugBus || {
enabled: initialDebugMode,
listener: null,
sequence: 0,
};
window.__desktopDebugBus = debugBus;
window.__nexusDebugBus = debugBus;
const browserTimezone = (() => {
try {
@@ -103,6 +108,10 @@
})();
function emitDebug(entry) {
if (typeof debugBus.emit === 'function') {
return debugBus.emit(entry);
}
debugBus.sequence += 1;
const payload = {
id: debugBus.sequence,

View File

@@ -280,6 +280,7 @@ final class AnalyticsService
}
$payouts = is_array($settings['payouts'] ?? null) ? $settings['payouts'] : [];
$walletWithdrawals = is_array($settings['wallet_withdrawals'] ?? null) ? $settings['wallet_withdrawals'] : [];
$purchasedMiners = is_array($settings['purchased_miners'] ?? null) ? $settings['purchased_miners'] : [];
$minerOffers = is_array($settings['miner_offers'] ?? null) ? $settings['miner_offers'] : [];
$currentHashrateMh = $this->totalHashrateMh(array_merge(

View File

@@ -87,6 +87,9 @@ $renderStartIcon = static function (array $profile): string {
<span>Fenster</span>
</div>
<div class="system-bar-right">
<?php if (($desktopPayload['session']['is_admin'] ?? false) === true): ?>
<button class="tray-pill tray-pill-button tray-pill-debug" id="debug-toggle-top" type="button" aria-expanded="false">Debug</button>
<?php endif; ?>
<time id="clock-top"></time>
</div>
</header>
@@ -157,6 +160,9 @@ $renderStartIcon = static function (array $profile): string {
<?php foreach ($desktopPayload['tray'] as $trayItem): ?>
<button class="tray-pill tray-pill-button" type="button" data-tray-id="<?= htmlspecialchars((string) $trayItem['id'], ENT_QUOTES) ?>"><?= htmlspecialchars($trayItem['label'], ENT_QUOTES) ?></button>
<?php endforeach; ?>
<?php if (($desktopPayload['session']['is_admin'] ?? false) === true): ?>
<button class="tray-pill tray-pill-button tray-pill-debug" id="debug-toggle" type="button" aria-expanded="false">Debug</button>
<?php endif; ?>
<time id="clock"></time>
</div>
<div class="tray-menu" id="tray-account-menu" hidden>

View File

@@ -503,6 +503,13 @@ h1 {
background: #f8fafc;
}
.window-content--desktop-debug {
padding: 0;
background:
linear-gradient(180deg, rgba(241, 245, 249, 0.98), rgba(226, 232, 240, 0.98)),
radial-gradient(circle at top right, rgba(34, 197, 94, 0.12), transparent 36%);
}
.window-module-host {
position: absolute;
inset: 0;
@@ -518,6 +525,94 @@ h1 {
color: #991b1b;
}
.desktop-debug-window {
display: grid;
grid-template-rows: auto minmax(0, 1fr);
height: 100%;
min-height: 0;
}
.desktop-debug-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 18px;
border-bottom: 1px solid rgba(148, 163, 184, 0.22);
background: rgba(255, 255, 255, 0.72);
}
.desktop-debug-copy {
margin: 0;
color: #475569;
font-size: 14px;
}
.desktop-debug-actions {
display: flex;
align-items: center;
gap: 10px;
}
.desktop-debug-action {
min-width: 0;
}
.desktop-debug-log {
min-height: 0;
overflow: auto;
padding: 18px;
display: grid;
gap: 12px;
background: rgba(15, 23, 42, 0.04);
}
.desktop-debug-entry,
.desktop-debug-empty {
padding: 14px 16px;
border-radius: 14px;
background: rgba(255, 255, 255, 0.92);
border: 1px solid rgba(148, 163, 184, 0.2);
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
}
.desktop-debug-entry-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 6px;
font-size: 13px;
}
.desktop-debug-entry-meta,
.desktop-debug-entry-url,
.desktop-debug-entry-message {
margin: 0;
font-size: 13px;
color: #475569;
}
.desktop-debug-entry-url {
color: #0f172a;
word-break: break-all;
}
.desktop-debug-entry-message {
margin-top: 8px;
}
.desktop-debug-entry-context {
margin: 10px 0 0;
padding: 12px;
border-radius: 12px;
background: #0f172a;
color: #e2e8f0;
overflow: auto;
font-size: 12px;
line-height: 1.45;
}
.window-app-shell {
height: 100%;
color: #0f172a;
@@ -1238,6 +1333,11 @@ h1 {
background: rgba(255, 255, 255, 0.16);
}
.tray-pill-debug.is-active {
background: linear-gradient(135deg, rgba(34, 197, 94, 0.34), rgba(59, 130, 246, 0.28));
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.18);
}
.tray-menu {
position: absolute;
right: 16px;
@@ -1364,4 +1464,9 @@ h1 {
display: grid;
grid-template-columns: 1fr;
}
.desktop-debug-toolbar {
display: grid;
grid-template-columns: 1fr;
}
}

View File

@@ -19,6 +19,8 @@ if (payloadNode) {
const accountMenu = document.getElementById('tray-account-menu');
const clockNode = document.getElementById('clock');
const clockTopNode = document.getElementById('clock-top');
const debugToggleNode = document.getElementById('debug-toggle');
const debugToggleTopNode = document.getElementById('debug-toggle-top');
const windows = new Map();
const activeWidgets = new Set(payload.widgets.active_ids);
const activeSkin = payload.meta.active_skin;
@@ -29,6 +31,8 @@ if (payloadNode) {
const storageScope = payload.desktop.storage_scope || 'guest';
const storageKey = `desktop.kusche.berlin.window-state.${storageScope}`;
const iconStorageKey = `desktop.kusche.berlin.icon-state.${storageScope}.${activeSkin}`;
const debugStorageKey = `desktop.kusche.berlin.debug-state.${storageScope}`;
const isAdmin = payload.session?.is_admin === true;
let zIndex = 20;
let topDesktopInset = 0;
let sideDesktopInset = 0;
@@ -38,6 +42,92 @@ if (payloadNode) {
const windowControlsMode = document.body.dataset.windowControls || 'windows';
let wallpaperImagePromise = null;
const loadDebugState = () => {
try {
const raw = window.localStorage.getItem(debugStorageKey);
const parsed = raw ? JSON.parse(raw) : null;
return {
open: parsed?.open === true,
};
} catch {
return { open: false };
}
};
const debugState = loadDebugState();
const saveDebugState = () => {
try {
window.localStorage.setItem(debugStorageKey, JSON.stringify({
open: debugState.open === true,
}));
} catch {
// Ignore storage failures for now.
}
};
const debugBus = window.__desktopDebugBus || {
enabled: isAdmin && debugState.open === true,
sequence: 0,
entries: [],
listener: null,
maxEntries: 400,
emit(entry) {
if (!this.enabled) {
return null;
}
this.sequence += 1;
const payloadEntry = {
id: this.sequence,
time: new Date().toISOString(),
...entry,
};
this.entries.push(payloadEntry);
if (this.entries.length > this.maxEntries) {
this.entries.shift();
}
if (typeof this.listener === 'function') {
this.listener(payloadEntry, this.entries.slice());
}
return payloadEntry;
},
subscribe(listener) {
this.listener = typeof listener === 'function' ? listener : null;
if (typeof this.listener === 'function') {
this.listener(null, this.entries.slice());
}
},
setEnabled(nextEnabled) {
this.enabled = nextEnabled === true;
if (!this.enabled) {
this.entries = [];
} else {
this.emit({
type: 'debug:enabled',
source: 'desktop',
message: 'Desktop-Debug aktiviert.',
});
}
if (typeof this.listener === 'function') {
this.listener(null, this.entries.slice());
}
},
clear() {
this.entries = [];
if (typeof this.listener === 'function') {
this.listener(null, this.entries.slice());
}
},
};
window.__desktopDebugBus = debugBus;
window.__nexusDebugBus = debugBus;
const escapeHtml = (value) => String(value)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
@@ -45,6 +135,89 @@ if (payloadNode) {
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
const emitDebug = (entry) => debugBus.emit(entry);
const updateDebugToggleUi = () => {
[debugToggleNode, debugToggleTopNode].forEach((node) => {
if (!(node instanceof HTMLElement)) {
return;
}
node.setAttribute('aria-expanded', String(debugState.open === true));
node.classList.toggle('is-active', debugState.open === true);
node.textContent = debugState.open === true ? 'Debug an' : 'Debug';
});
};
const ensureFetchDebugHook = () => {
if (window.__desktopDebugFetchHookInstalled === true || typeof window.fetch !== 'function') {
return;
}
const originalFetch = window.fetch.bind(window);
window.fetch = async (input, init) => {
const method = String(init?.method || 'GET').toUpperCase();
const url = typeof input === 'string' ? input : (input instanceof Request ? input.url : String(input));
const startedAt = performance.now();
const requestId = emitDebug({
type: 'fetch:start',
source: 'desktop',
method,
url,
})?.id ?? null;
try {
const response = await originalFetch(input, init);
emitDebug({
type: 'fetch:end',
source: 'desktop',
request_id: requestId,
method,
url,
status: response.status,
ok: response.ok,
duration_ms: Math.round((performance.now() - startedAt) * 100) / 100,
});
return response;
} catch (error) {
emitDebug({
type: 'fetch:error',
source: 'desktop',
request_id: requestId,
method,
url,
duration_ms: Math.round((performance.now() - startedAt) * 100) / 100,
message: error instanceof Error ? error.message : 'Fetch fehlgeschlagen',
});
throw error;
}
};
window.__desktopDebugFetchHookInstalled = true;
};
ensureFetchDebugHook();
window.addEventListener('error', (event) => {
emitDebug({
type: 'window:error',
source: 'desktop',
message: event.message || 'Unbekannter Fehler',
file: event.filename || null,
line: event.lineno || null,
column: event.colno || null,
});
});
window.addEventListener('unhandledrejection', (event) => {
const reason = event.reason;
emitDebug({
type: 'window:unhandledrejection',
source: 'desktop',
message: reason instanceof Error ? reason.message : String(reason || 'Promise wurde verworfen'),
});
});
const buildAppIconMarkup = (app, className = '') => {
const safeClassName = className ? ` ${className}` : '';
@@ -292,6 +465,22 @@ if (payloadNode) {
};
const buildWindowBody = (definition) => {
if (definition.content_mode === 'desktop-debug') {
return `
<div class="window-content window-content--desktop-debug">
<div class="desktop-debug-window" data-debug-window="1">
<div class="desktop-debug-toolbar">
<p class="desktop-debug-copy">Live-Debug fuer Desktop und Native-Module. Tracking laeuft nur, solange dieses Fenster offen ist.</p>
<div class="desktop-debug-actions">
<button class="window-app-button desktop-debug-action" type="button" data-debug-action="clear">Log leeren</button>
</div>
</div>
<div class="desktop-debug-log" data-debug-log></div>
</div>
</div>
`;
}
if (definition.content_mode === 'native-module' && definition.app_id === 'mining-checker') {
return `
<div class="window-content window-content--embedded window-content--module">
@@ -660,6 +849,132 @@ if (payloadNode) {
setAccountMenuOpen(false);
};
const renderDebugEntries = (container, entries) => {
if (!(container instanceof HTMLElement)) {
return;
}
if (!Array.isArray(entries) || entries.length === 0) {
container.innerHTML = '<div class="desktop-debug-empty">Noch keine Debug-Eintraege.</div>';
return;
}
container.innerHTML = entries.slice().reverse().map((entry) => {
const meta = [];
if (entry.source) {
meta.push(String(entry.source));
}
if (entry.method) {
meta.push(String(entry.method));
}
if (entry.status !== undefined && entry.status !== null) {
meta.push(`HTTP ${entry.status}`);
}
if (entry.duration_ms !== undefined && entry.duration_ms !== null) {
meta.push(`${entry.duration_ms} ms`);
}
const reservedKeys = new Set([
'id', 'time', 'type', 'source', 'message', 'method', 'url', 'status',
'ok', 'duration_ms', 'request_id', 'server_time', 'file', 'line', 'column',
]);
const context = Object.fromEntries(
Object.entries(entry).filter(([key]) => !reservedKeys.has(key))
);
const contextText = Object.keys(context).length > 0
? escapeHtml(JSON.stringify(context, null, 2))
: '';
return `
<article class="desktop-debug-entry">
<div class="desktop-debug-entry-head">
<strong>${escapeHtml(entry.type || 'debug')}</strong>
<span>${escapeHtml(new Date(entry.time || Date.now()).toLocaleTimeString('de-DE'))}</span>
</div>
${meta.length ? `<p class="desktop-debug-entry-meta">${escapeHtml(meta.join(' · '))}</p>` : ''}
${entry.url ? `<p class="desktop-debug-entry-url">${escapeHtml(String(entry.url))}</p>` : ''}
${entry.message ? `<p class="desktop-debug-entry-message">${escapeHtml(String(entry.message))}</p>` : ''}
${contextText ? `<pre class="desktop-debug-entry-context">${contextText}</pre>` : ''}
</article>
`;
}).join('');
container.scrollTop = 0;
};
const initializeDesktopDebugWindow = (record) => {
const host = record.node.querySelector('[data-debug-window="1"]');
const logNode = record.node.querySelector('[data-debug-log]');
const clearButton = record.node.querySelector('[data-debug-action="clear"]');
if (!(host instanceof HTMLElement) || !(logNode instanceof HTMLElement)) {
return;
}
renderDebugEntries(logNode, debugBus.entries);
debugBus.subscribe((_entry, entries) => {
renderDebugEntries(logNode, entries);
});
if (clearButton instanceof HTMLButtonElement) {
clearButton.addEventListener('click', () => {
debugBus.clear();
});
}
};
const findDebugWindow = () => Array.from(windows.values()).find((record) => record.definition.app_id === 'desktop-debug') || null;
const closeDebugWindow = () => {
const existing = findDebugWindow();
if (existing) {
closeWindow(existing.id);
} else {
debugState.open = false;
saveDebugState();
debugBus.setEnabled(false);
updateDebugToggleUi();
}
};
const openDebugWindow = () => {
if (!isAdmin) {
return;
}
const existing = findDebugWindow();
debugState.open = true;
saveDebugState();
debugBus.setEnabled(true);
updateDebugToggleUi();
if (existing) {
focusWindow(existing.id);
return;
}
createWindow({
window_id: `window-desktop-debug-${Date.now()}`,
app_id: 'desktop-debug',
title: 'Desktop Debug',
x: 120,
y: 88,
width: 720,
height: 520,
content: '',
entry_route: '',
content_mode: 'desktop-debug',
});
};
const toggleDebugWindow = () => {
if (debugState.open === true) {
closeDebugWindow();
return;
}
openDebugWindow();
};
const applyWindowLayout = (record) => {
const { x, y, width, height } = record.layout;
record.node.style.left = `${x}px`;
@@ -736,6 +1051,13 @@ if (payloadNode) {
return;
}
if (record.definition.app_id === 'desktop-debug') {
debugState.open = false;
saveDebugState();
debugBus.setEnabled(false);
updateDebugToggleUi();
}
windows.delete(windowId);
record.node.remove();
syncTaskbar();
@@ -1052,6 +1374,10 @@ if (payloadNode) {
}
}
if (definition.content_mode === 'desktop-debug') {
initializeDesktopDebugWindow(record);
}
node.querySelectorAll('.window-action').forEach((action) => {
action.addEventListener('click', (event) => {
event.stopPropagation();
@@ -1328,6 +1654,14 @@ if (payloadNode) {
setAccountMenuOpen(accountMenu.hidden);
});
debugToggleNode?.addEventListener('click', () => {
toggleDebugWindow();
});
debugToggleTopNode?.addEventListener('click', () => {
toggleDebugWindow();
});
accountMenu?.addEventListener('click', (event) => {
const target = event.target instanceof Element ? event.target.closest('[data-account-action]') : null;
@@ -1426,9 +1760,13 @@ if (payloadNode) {
renderWidgets();
renderStartMenu();
updateDebugToggleUi();
setStartMenuOpen(false);
measureDesktopBounds();
applyStoredIconLayout();
if (isAdmin && debugState.open === true) {
openDebugWindow();
}
payload.windows.forEach(createWindow);
renderClock();
window.setInterval(renderClock, 1000);

View File

@@ -36,6 +36,7 @@ final class App
? $_SESSION['desktop_auth']['user']
: [];
$authGroups = array_values(array_map('strval', (array) ($authUser['groups'] ?? [])));
$isAdmin = in_array('administrators', $authGroups, true);
$visibleApps = AppVisibility::filterByGroups($registry->all(), $authGroups);
$userSelfManagement = new UserSelfManagementService($this->projectRoot);
$resolvedSkin = $userSelfManagement->resolveActiveSkin(
@@ -97,6 +98,8 @@ final class App
'session' => [
'display_name' => $displayName,
'authenticated' => $isAuthenticated,
'is_admin' => $isAdmin,
'groups' => $authGroups,
'logout_url' => $isAuthenticated && $auth->isConfigured() ? '/auth/logout' : null,
],
'menu' => [

View File

@@ -40,6 +40,8 @@ Danach bei Bedarf die fachlichen Projektanweisungen lesen:
- `UMSETZUNGSSTATUS.md` nur dann auf `ERLEDIGT` setzen, wenn es ausdruecklich freigegeben wurde
- Desktop-Mechaniken nicht versehentlich zurueckbauen
- Standard-Design fuer Fensterinhalte beibehalten, sofern eine App kein eigenes UI braucht
- globale Debug-Infrastruktur nicht pro App neu bauen; fuer Admin-Debugging ist das gemeinsame Desktop-Debug-Widget mit Debug-Fenster zu verwenden
- Debug-Tracking soll nur laufen, wenn das globale Debug-Fenster aktiv geoeffnet ist; der letzte Debug-Status soll pro Benutzer einen Reload ueberstehen
- Begriffe und offizielle Benennungen aus `docs/CONTENT.md` verwenden
- `README.md`-Dateien in Teilbereichen pflegen
- wichtige Informationen aus dezentralen `README.md`-Dateien auch zentral in `docs/` halten