Files
mindflux/src/main/resources/templates/dashboard.html
T
AliceDarkest e853ca8e93 AP1 Light Mode: Token-Nachschaerfung, Light-Kartenakzente, 3 echte Light-Varianten
- theme.css: Light-Tokens auf WCAG AA nachgeschaerft (accent #c2410c, accent-2
  #b45309, muted #6b5f4c, secondary #146b96, secondary-2 #0f5a80, Spiel-Durch-
  faller esc/sc/dl/ss/ff). Neues Token --warn (hell #8a6a06 / dunkel #edcb4c).
- platform-neural.css: Light-Kartenakzente je Spiel (farbton-treu abgedunkelt,
  alle >=4.6:1 auf Kartengrund) statt der Neon-Leaks; Light-finalflux-Override.
- dashboard.html: #edcb4c-Hardcodes -> var(--warn).
- ServiceClash/Networker/BugHunter: echte Light-Varianten ueber scoped Custom
  Properties auf .sc-body/.nw-body/.bh-body (Dark-Defaults = alte Werte, Dark
  pixelgleich), handentworfene Light-Paletten (alle Kontraste WCAG-AA geprueft),
  .mf-theme-Toggle im Header ergaenzt, Networker-var(--text)/(--muted)-Bug
  behoben. Spielfremde .nw-*/.bh-*-Regeln aus accessibility-placement.css in die
  jeweiligen Spiel-CSS verschoben (ohne !important).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WFxcjtc9HNP4VECT6Ur1tE
2026-07-21 00:00:49 +02:00

242 lines
18 KiB
HTML
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.
<!DOCTYPE html>
<html lang="de" data-theme="dark"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity6">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SysnaptiK Neural Dashboard</title>
<link rel="stylesheet" href="/css/theme.css?v=3">
<script src="/js/theme.js?v=4"></script>
<style>
/* Dashboard-spezifische Widgets (rechte Spalte) */
.network-status { display:grid; grid-template-columns:auto 1fr; gap:.85rem; align-items:start; }
.network-status svg { width:54px; height:54px; color:var(--accent); filter:drop-shadow(0 0 8px var(--glow-s)); }
.status-summary { color:var(--heading); font-size:.9rem; font-weight:800; letter-spacing:.04em; }
.status-summary::before { content:""; display:inline-block; width:.52rem; height:.52rem; margin-right:.42rem; border-radius:50%; background:var(--muted); box-shadow:0 0 8px color-mix(in srgb,var(--muted) 60%,transparent); }
.network-status.ready .status-summary { color:var(--accent); }
.network-status.ready .status-summary::before { background:var(--accent); box-shadow:0 0 8px var(--glow-s); }
.network-status.restricted .status-summary { color:var(--warn); }
.network-status.restricted .status-summary::before { background:var(--warn); box-shadow:0 0 8px color-mix(in srgb,var(--warn) 48%,transparent); }
.status-caption { color:var(--muted); font-size:.72rem; line-height:1.45; margin:.25rem 0 0; }
.status-details { grid-column:1 / -1; display:none; gap:.45rem; padding-top:.2rem; color:var(--muted); font-size:.72rem; }
.status-details.open { display:grid; }
.status-details div { display:flex; justify-content:space-between; gap:1rem; border-top:1px solid var(--border); padding-top:.45rem; }
.status-details b { color:var(--text); font-weight:600; text-align:right; }
.status-toggle { grid-column:1 / -1; justify-self:start; border:0; padding:0; background:transparent; color:var(--secondary); font:inherit; font-size:.72rem; cursor:pointer; }
.status-toggle:hover { text-decoration:underline; }
.nn-trigger { position:relative; }
.nn-trigger.unread { color:var(--surface); border-color:var(--secondary); background:var(--secondary); box-shadow:0 0 14px color-mix(in srgb,var(--secondary) 30%,transparent); }
.nn-trigger svg { width:20px; height:20px; }
.nn-unread { position:absolute; top:-6px; right:-7px; min-width:17px; height:17px; padding:0 4px; display:grid; place-items:center; border:2px solid var(--surface); border-radius:99px; background:var(--panel); color:var(--secondary); font-size:.62rem; font-weight:900; line-height:1; box-shadow:0 0 10px color-mix(in srgb,var(--secondary) 28%,transparent); }
.nn-unread[hidden] { display:none; }
.prog-top { display:flex; justify-content:space-between; align-items:center; margin-bottom:.4rem; }
.pill { font-size:.68rem; color:var(--muted); border:1px solid var(--border); border-radius:7px; padding:.2rem .5rem; }
.prog-big { display:flex; align-items:baseline; gap:.5rem; }
.prog-big b { color:var(--heading); font-size:2rem; font-weight:800; } .prog-big b span{ font-size:1rem; color:var(--muted); }
.xpbadge { font-size:.66rem; color:var(--accent); background:var(--surface-2); border:1px solid var(--border-accent); border-radius:6px; padding:.15rem .45rem; margin-left:auto; }
.chart svg { width:100%; height:74px; } .chart .days { display:flex; justify-content:space-between; color:var(--muted); font-size:.6rem; margin-top:.25rem; }
.cat { display:flex; align-items:center; gap:.6rem; margin:.55rem 0; font-size:.78rem; }
.cat svg { width:15px; height:15px; color:var(--accent); flex:0 0 auto; }
.cat .nm { color:var(--text); width:92px; flex:0 0 auto; }
.cat .cbar { flex:1; height:6px; border-radius:99px; background:var(--surface-2); border:1px solid var(--border); overflow:hidden; }
.cat .cbar i { display:block; height:100%; background:linear-gradient(90deg,var(--accent),var(--accent-2)); box-shadow:0 0 8px var(--glow-w); }
.cat .pct { color:var(--muted); width:30px; text-align:right; }
.challenge .ch-row { display:flex; gap:.7rem; }
.challenge .bolt { color:var(--accent); flex:0 0 auto; filter:drop-shadow(0 0 6px var(--glow-s)); }
.challenge h5 { color:var(--accent); font-size:.82rem; letter-spacing:.04em; text-transform:uppercase; margin-bottom:.3rem; }
.challenge p { color:var(--muted); font-size:.78rem; line-height:1.45; }
</style>
</head>
<body>
<div class="mf-shell">
<!-- ===== Sidebar (gemeinsames Fragment) ===== -->
<aside th:replace="~{fragments/layout :: sidebar('dashboard')}"></aside>
<!-- ===== Topbar ===== -->
<header class="mf-topbar">
<div class="mf-hello">
<h1>Willkommen zurück, <span class="nm" sec:authentication="name">Alice</span>!</h1>
<p>Wähle einen Lernmodus und verknüpfe neues Wissen mit deinem Netzwerk.</p>
</div>
<div class="mf-tools">
<button class="mf-iconbtn ft-trigger" type="button" data-fluxtimer-toggle aria-label="NeuralPause öffnen" aria-expanded="false">
<svg class="ft-trigger-icon" viewBox="0 0 24 24" aria-hidden="true">
<circle cx="10.5" cy="12" r="7.5"/>
<path d="M10.5 7.5V12l3 2M10.5 2v2M3 12H1.5M4.3 5.8 2.8 4.3"/>
<path class="ft-trigger-bolt" d="M17 8.5h4l-2.5 4H22l-6.2 8 1.7-5H14z"/>
</svg>
<span class="ft-trigger-dot" aria-hidden="true"></span>
</button>
<a class="mf-iconbtn nn-trigger" href="/soziales" data-neuralnet-trigger aria-label="NeuralNet – keine ungelesenen Nachrichten" title="NeuralNet"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 11.5a7.5 7.5 0 0 1-10.9 6.7L4 20l1.8-4.7A7.5 7.5 0 1 1 20 11.5Z"/><path d="M8.5 11.5h.01M12 11.5h.01M15.5 11.5h.01"/></svg><span class="nn-unread" data-neuralnet-unread hidden aria-hidden="true"></span></a>
<div class="mf-search"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4-4"/></svg><input placeholder="Suche..."></div>
<button class="mf-theme"></button>
<form th:action="@{/logout}" method="post" style="display:inline">
<button type="submit" class="mf-iconbtn" aria-label="Abmelden" title="Abmelden" style="color:var(--danger)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9"/></svg></button>
</form>
</div>
</header>
<!-- ===== Spielkarten ===== -->
<main class="mf-main"><div class="mf-grid" id="grid"></div></main>
<!-- ===== Rechte Spalte ===== -->
<aside class="mf-rightbar">
<div class="mf-panel">
<h4><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12h4l2-7 4 14 2-7h6"/></svg> Lernnetz-Status</h4>
<div class="network-status" id="platformStatus" aria-live="polite">
<svg viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M15 26 29 14l18 8-5 20-20 8-9-13zM29 14l-2 22m20-14-20 14m-5 14 5-14m15 6-15-6"/>
<circle cx="15" cy="26" r="3" fill="currentColor"/><circle cx="29" cy="14" r="3" fill="currentColor"/><circle cx="47" cy="22" r="3" fill="currentColor"/><circle cx="42" cy="42" r="3" fill="currentColor"/><circle cx="22" cy="50" r="3" fill="currentColor"/><circle cx="27" cy="36" r="3" fill="currentColor"/>
</svg>
<div><div class="status-summary" data-status-summary>Lernnetz wird geprüft</div><p class="status-caption" data-status-caption>Prüfe Fortschrittsspeicher und Lernmodule …</p></div>
<div class="status-details" data-status-details>
<div><span>Fortschrittsspeicher</span><b data-status-storage>Wird geprüft</b></div>
<div><span>Lernmodule</span><b data-status-modules>Wird geprüft</b></div>
<div><span>Letzte Prüfung</span><b data-status-checked>–</b></div>
</div>
<button class="status-toggle" type="button" data-status-toggle aria-expanded="false">Details anzeigen</button>
</div>
</div>
<div class="mf-panel">
<div class="prog-top"><h4 style="margin:0"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 17l6-6 4 4 8-8"/></svg> Dein Fortschritt</h4><span class="pill">Diese Woche</span></div>
<div class="op" style="color:var(--muted);font-size:.72rem;margin:.2rem 0">Zum nächsten Level</div>
<div class="prog-big"><b th:text="${overall.progressPercent}">0</b><span style="color:var(--muted);font-size:1rem">%</span><span class="xpbadge" th:text="'+' + ${weekly.totalXp} + ' XP'">+0 XP</span></div>
<div class="chart" id="weeklyXpChart" th:attr="data-values=${weekly.xp}">
<svg viewBox="0 0 240 74" preserveAspectRatio="none"></svg>
<div class="days"><span>Mo</span><span>Di</span><span>Mi</span><span>Do</span><span>Fr</span><span>Sa</span><span>So</span></div>
</div>
</div>
<div class="mf-panel" id="cats">
<h4><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2l3 6 6 .5-4.5 4 1.5 6.5L12 16l-6 3 1.5-6.5L3 8.5 9 8z"/></svg> Top Spiele</h4>
<p th:if="${#lists.isEmpty(topGames)}" style="color:var(--muted);font-size:.78rem;line-height:1.45">Noch keine abgeschlossene Runde. Starte ein Spiel, dann erscheinen hier echte Highscores.</p>
<div class="cat" th:each="game : ${topGames}">
<span class="nm" th:text="${game.name}">MindMatch</span>
<span class="cbar"><i th:style="'width:' + ${game.progressPercent} + '%'"/></span>
<span class="pct" th:text="${game.highscore} + ' XP'">0 XP</span>
</div>
</div>
<div class="mf-panel accent challenge">
<div class="ch-row">
<svg class="bolt" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2L4 14h7l-1 8 9-12h-7z"/></svg>
<div><h5>Bereit für die nächste Challenge?</h5><p>Starte jetzt ein Spiel und steigere dein Level!</p></div>
</div>
<a class="mf-btn" href="/mindmatch" style="margin-top:.8rem">LOS GEHT'S! <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M13 6l6 6-6 6"/></svg></a>
</div>
</aside>
</div>
<footer class="mf-footer">
<a href="#">Support</a>
<a href="#">Feedback</a>
<a href="/nutzungsbedingungen">Nutzungsbedingungen</a>
<a href="/richtlinien">Richtlinien</a>
<a href="/datenschutz">Datenschutz</a>
<a href="/impressum">Impressum</a>
</footer>
<script th:inline="none">
const GI = {
brain:'<path d="M28 8c-9-5-22 0-23 11-7 2-9 12-3 18-3 7 2 15 10 16 3 6 13 7 18 2M28 8c9-5 22 0 23 11 7 2 9 12 3 18 3 7-2 15-10 16-3 6-13 7-18 2M28 8v45M16 22c6 2 6 10 0 13M40 22c-6 2-6 10 0 13M12 40c7 0 10 5 10 11M44 40c-7 0-10 5-10 11"/>',
code:'<path d="M22 16L8 28l14 12M42 16l14 12-14 12"/>',
terminal:'<rect x="6" y="10" width="52" height="44" rx="4"/><path d="M16 26l9 6-9 6M32 40h14"/>',
lab:'<path d="M26 8h12"/><path d="M30 8v18L16 48c-2.3 3.7.4 8.5 4.8 8.5h22.4c4.4 0 7.1-4.8 4.8-8.5L34 26V8"/><path d="M23 43h18"/><path d="M25 49h14"/><path d="M25 32l-4 4 4 4"/><path d="M39 32l4 4-4 4"/><path d="M30 42l4-12"/>',
lock:'<rect x="12" y="28" width="40" height="28" rx="5"/><path d="M20 28v-7a12 12 0 0 1 24 0v7M32 39v6"/>',
sword:'<path d="M48 8l4 4-22 22-4-4zM26 30l8 8M30 26l8 8M22 38l-6 6 6 6 6-6M16 44l-6 6"/>',
bug:'<rect x="20" y="20" width="24" height="30" rx="12"/><path d="M32 20v-6M24 12l4 6M40 12l-4 6M20 30h-8M52 30h-8M20 40H10M54 40H44M20 50l-7 7M44 50l7 7"/>',
network:'<circle cx="32" cy="14" r="6"/><circle cx="14" cy="48" r="6"/><circle cx="50" cy="48" r="6"/><path d="M30 19L17 43M34 19l13 24M20 48h24"/>',
trophy:'<path d="M18 10h28v10a14 14 0 0 1-28 0zM18 14h-7v3a9 9 0 0 0 9 9M46 14h7v3a9 9 0 0 1-9 9M26 40h12M24 40v6h16v-6M20 56h24"/>'
};
const GAMES = [
['MindMatch','brain','Trainiere dein Gedächtnis mit IT-Begriffen in einem klassischen Memory-Spiel.','/mindmatch'],
['TechTacular','code','Das IT-Quizduell im Jeopardy-Stil – teste dein Wissen in verschiedenen Kategorien.','/techtacular'],
['DevLab','lab','Das Entwicklerlabor bündelt Java, SQL, CSS, PHP, HTML, Git, REST und Regex.','/devlab'],
['SyntaxSaga','terminal','Interaktive IT-Geschichten mit Lückentexten – wie ein Textadventure zum Mitdenken.','/syntaxsaga'],
['SystemEscape','lock','Löse IT-Rätsel, sammle Zugriffsfragmente und entkomme dem System.','/codeescape'],
['ServiceClash','sword','Der chaotische Admin-Alltag als Spiel – meistere Anfragen & halte Systeme am Laufen!','/serviceclash'],
['BugHunter','bug','Finde Fehler im Code, denke wie ein Debugger und löse knifflige Bugs.','/bughunter'],
['NetWorker','network','Verbinde Netzwerke, konfiguriere IPs und löse Routing-Herausforderungen.','/networker'],
['FinalProtocol','trophy','Deine Prüfungszentrale: gezielt lernen, strukturiert trainieren, sicher bestehen.','/finalflux']
];
const CARD_KEYS = {MindMatch:'mindmatch',TechTacular:'techtacular',DevLab:'devlab',SyntaxSaga:'syntaxsaga',SystemEscape:'codeescape',ServiceClash:'serviceclash',BugHunter:'bughunter',NetWorker:'networker',FinalProtocol:'finalprotocol'};
document.getElementById('grid').innerHTML = GAMES.map(([t,ic,d,href])=>`
<a class="mf-card mf-card-${CARD_KEYS[t]}" href="${href}">
<span class="ico"><svg viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round">${GI[ic]}</svg></span>
<h3>${t}</h3><p>${d}</p>
<span class="mf-play">SPIEL STARTEN <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14M13 6l6 6-6 6"/></svg></span>
</a>`).join('');
const weekChart = document.getElementById('weeklyXpChart');
const values = JSON.parse(weekChart.dataset.values || '[0,0,0,0,0,0,0]');
const max = Math.max(...values, 1);
const points = values.map((value, index) => `${Math.round(index * 40)} ${64 - Math.round(value / max * 54)}`);
const line = `M${points.join(' L')}`;
weekChart.querySelector('svg').innerHTML = `<defs><linearGradient id="ar" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="var(--accent)" stop-opacity=".35"/><stop offset="1" stop-color="var(--accent)" stop-opacity="0"/></linearGradient></defs><path d="${line} L240 74 L0 74 Z" fill="url(#ar)"/><path d="${line}" fill="none" stroke="var(--accent)" stroke-width="2" style="filter:drop-shadow(0 0 5px var(--glow-s))"/>`;
// NeuralNet: Die Sprechblase zeigt ausschließlich echte, ungelesene
// Direktnachrichten. Aktualisierung erfolgt schlank im Hintergrund.
const neuralNetTrigger = document.querySelector('[data-neuralnet-trigger]');
const neuralNetUnread = document.querySelector('[data-neuralnet-unread]');
function refreshNeuralNetNotice() {
fetch('/api/neuralnet/overview', { headers: { Accept: 'application/json' } })
.then(response => response.ok ? response.json() : null)
.then(overview => {
const unread = overview && overview.enabled ? overview.unread || 0 : 0;
neuralNetTrigger.classList.toggle('unread', unread > 0);
neuralNetUnread.hidden = unread === 0;
neuralNetUnread.textContent = unread > 99 ? '99+' : unread;
neuralNetTrigger.setAttribute('aria-label', unread > 0
? `NeuralNet – ${unread} ungelesene Nachricht${unread === 1 ? '' : 'en'}`
: 'NeuralNet – keine ungelesenen Nachrichten');
})
.catch(() => { neuralNetTrigger.classList.remove('unread'); neuralNetUnread.hidden = true; });
}
refreshNeuralNetNotice();
setInterval(refreshNeuralNetNotice, 30000);
// Der Status prüft den Fortschrittsspeicher serverseitig. Die Modulzahl ist
// der registrierte Lernkatalog, nicht ein erfundener Prozentwert.
const statusCard = document.getElementById('platformStatus');
const statusSummary = statusCard.querySelector('[data-status-summary]');
const statusCaption = statusCard.querySelector('[data-status-caption]');
const statusStorage = statusCard.querySelector('[data-status-storage]');
const statusModules = statusCard.querySelector('[data-status-modules]');
const statusChecked = statusCard.querySelector('[data-status-checked]');
const details = statusCard.querySelector('[data-status-details]');
const toggle = statusCard.querySelector('[data-status-toggle]');
toggle.addEventListener('click', () => {
const open = details.classList.toggle('open');
toggle.setAttribute('aria-expanded', String(open));
toggle.textContent = open ? 'Details ausblenden' : 'Details anzeigen';
});
fetch('/api/platform/status', { headers: { Accept: 'application/json' } })
.then(async response => ({ response, status: await response.json() }))
.then(({ response, status }) => {
const ready = response.ok && status.storageConnected;
statusCard.classList.toggle('ready', ready);
statusCard.classList.toggle('restricted', !ready);
statusSummary.textContent = ready ? 'Bereit für deine Session' : 'Lernnetz eingeschränkt';
statusCaption.textContent = ready
? 'Fortschritt wird sicher gespeichert.'
: 'Fortschritt kann gerade nicht zuverlässig gespeichert werden.';
statusStorage.textContent = ready ? 'Verbunden' : 'Nicht erreichbar';
statusModules.textContent = `${status.registeredModules} registriert`;
statusChecked.textContent = new Date(status.checkedAt).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
})
.catch(() => {
statusCard.classList.add('restricted');
statusSummary.textContent = 'Statusprüfung nicht möglich';
statusCaption.textContent = 'Der aktuelle Speicherzustand konnte nicht bestätigt werden.';
statusStorage.textContent = 'Unbekannt';
statusModules.textContent = 'Nicht bestätigt';
});
</script>
</body>
</html>