e853ca8e93
- 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
242 lines
18 KiB
HTML
242 lines
18 KiB
HTML
<!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>
|