dadssad
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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/`
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
@@ -45,6 +135,89 @@ if (payloadNode) {
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
|
||||
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);
|
||||
|
||||
@@ -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' => [
|
||||
|
||||
2
start.md
2
start.md
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user