Make dashboard learning status meaningful
mindflux-ci / build (push) Successful in 23s

This commit is contained in:
AliceDarkest
2026-07-14 15:03:18 +02:00
parent 56cd17520e
commit bd76cd11de
13 changed files with 233 additions and 27 deletions
+13
View File
@@ -7,6 +7,19 @@ Versionierung [semantisch](https://semver.org/lang/de/).
## [Unreleased] – 2026-07-14 · SysnaptiK Neural Rebrand ## [Unreleased] – 2026-07-14 · SysnaptiK Neural Rebrand
### Marke & Plattform ### Marke & Plattform
- **Echter Lernnetz-Status:** Der Dashboard-Status prüft nun serverseitig den
Fortschrittsspeicher und zeigt nachvollziehbar „bereit“ oder „eingeschränkt“ samt
Zeitstempel und registrierten Lernmodulen. Der dekorative 100%-Ring wurde entfernt.
- **Akzente klar getrennt:** MindMatch ist nun Magenta, SyntaxSaga ein kühleres Violett,
SystemEscape Gelb-Bernstein und FinalProtocol Koralle. Die Plattformkarten sind damit
auf einen Blick unterscheidbar, ohne die Farben in den Spielen selbst anzutasten.
- **Fortschrittsdiagramme korrigiert:** Donut- und Wochenchart lesen den Neural-Akzent
nun von der Plattformhülle statt den alten globalen Orange-Token zu verwenden.
- **Alte Platzhalter-Copy entfernt:** Die Registrierungsrakete und der Zeitreise-Spruch
auf der Vorbereitungsseite wurden durch neutrale SysnaptiK-Neural-Texte ersetzt.
- **Admin-Umfang transparent:** Das Admin-Dashboard benennt jetzt klar, welche
Inhaltsverwaltungen vorhanden sind und dass ServiceClash, NetWorker und FinalProtocol
noch direkt mit der Anwendung ausgeliefert werden.
- **Bestehendes Emblem bewahrt:** Die kurzfristig ergänzte CSS-Knotenmarke wurde - **Bestehendes Emblem bewahrt:** Die kurzfristig ergänzte CSS-Knotenmarke wurde
zurückgenommen. Das vorhandene Emblem bleibt als Wiedererkennungszeichen bestehen, zurückgenommen. Das vorhandene Emblem bleibt als Wiedererkennungszeichen bestehen,
weil es die neue neuronale Metapher bereits passend transportiert. weil es die neue neuronale Metapher bereits passend transportiert.
+2 -1
View File
@@ -103,7 +103,8 @@ Accessibility-Widget soll anschließend jeweils den lokalen Akzent übernehmen.
> Plattformseiten und FinalProtocol. Die Spielwurzeln sind explizit vom Plattform-Scope > Plattformseiten und FinalProtocol. Die Spielwurzeln sind explizit vom Plattform-Scope
> ausgeschlossen und behalten ihre jeweiligen internen Skins. NeuralPause ersetzt die > ausgeschlossen und behalten ihre jeweiligen internen Skins. NeuralPause ersetzt die
> sichtbare FluxTimer-/Zeitreise-Copy. Domain-/DNS-Umzug und technische Paket-/Tabellennamen > sichtbare FluxTimer-/Zeitreise-Copy. Domain-/DNS-Umzug und technische Paket-/Tabellennamen
> bleiben bewusst separate Schritte. > bleiben bewusst separate Schritte. Der Dashboard-Status ist als echter Lernnetz-Status
> umgesetzt: Er prüft den Fortschrittsspeicher statt eine feste Verfügbarkeit vorzutäuschen.
### Ziel und Abgrenzung ### Ziel und Abgrenzung
@@ -0,0 +1,24 @@
package com.example.mindflux.controller;
import com.example.mindflux.dto.PlatformStatus;
import com.example.mindflux.service.PlatformStatusService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class PlatformStatusController {
private final PlatformStatusService platformStatusService;
public PlatformStatusController(PlatformStatusService platformStatusService) {
this.platformStatusService = platformStatusService;
}
@GetMapping("/api/platform/status")
public ResponseEntity<PlatformStatus> status() {
PlatformStatus status = platformStatusService.currentStatus();
return ResponseEntity.status(status.ready() ? HttpStatus.OK : HttpStatus.SERVICE_UNAVAILABLE).body(status);
}
}
@@ -0,0 +1,18 @@
package com.example.mindflux.dto;
import java.time.Instant;
/**
* Ein bewusst kleiner, nachvollziehbarer Plattformzustand für das Dashboard.
* Er behauptet keine fiktive Verfügbarkeit, sondern prüft den Speicher, der
* Spielstände und Fortschritt tatsächlich trägt.
*/
public record PlatformStatus(
boolean storageConnected,
int registeredModules,
Instant checkedAt
) {
public boolean ready() {
return storageConnected;
}
}
@@ -0,0 +1,37 @@
package com.example.mindflux.service;
import com.example.mindflux.dto.PlatformStatus;
import com.example.mindflux.repository.GameScoreRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.stereotype.Service;
import java.time.Instant;
@Service
public class PlatformStatusService {
private static final Logger log = LoggerFactory.getLogger(PlatformStatusService.class);
private static final int REGISTERED_MODULES = 9;
private final GameScoreRepository gameScoreRepository;
public PlatformStatusService(GameScoreRepository gameScoreRepository) {
this.gameScoreRepository = gameScoreRepository;
}
/**
* Die Abfrage ist absichtlich echt: Nur wenn der Fortschrittsspeicher lesbar
* ist, meldet das Dashboard das Lernnetz als bereit.
*/
public PlatformStatus currentStatus() {
try {
gameScoreRepository.count();
return new PlatformStatus(true, REGISTERED_MODULES, Instant.now());
} catch (DataAccessException exception) {
log.warn("Fortschrittsspeicher ist für den Lernnetz-Status nicht erreichbar: {}", exception.getMessage());
return new PlatformStatus(false, REGISTERED_MODULES, Instant.now());
}
}
}
@@ -38,15 +38,15 @@ body.sn-platform .mf-panel, body.sn-platform .mf-card, body.sn-platform .mf-prof
body.sn-platform .mf-card { overflow:hidden; } body.sn-platform .mf-card { overflow:hidden; }
body.sn-platform .mf-card::after { content:""; position:absolute; inset:0; pointer-events:none; opacity:.55; background:radial-gradient(circle at 90% 8%,color-mix(in srgb,var(--accent) 22%,transparent) 0 1px,transparent 2px),linear-gradient(132deg,transparent 0 45%,color-mix(in srgb,var(--accent) 12%,transparent) 45.2% 45.4%,transparent 45.6%); } body.sn-platform .mf-card::after { content:""; position:absolute; inset:0; pointer-events:none; opacity:.55; background:radial-gradient(circle at 90% 8%,color-mix(in srgb,var(--accent) 22%,transparent) 0 1px,transparent 2px),linear-gradient(132deg,transparent 0 45%,color-mix(in srgb,var(--accent) 12%,transparent) 45.2% 45.4%,transparent 45.6%); }
body.sn-platform .mf-card > * { position:relative; z-index:1; } body.sn-platform .mf-card > * { position:relative; z-index:1; }
body.sn-platform .mf-card-mindmatch { --accent:#c078f2; --accent-2:#ebc0ff; --glow-s:rgba(192,120,242,.40); } body.sn-platform .mf-card-mindmatch { --accent:#df68b9; --accent-2:#ffc0e7; --glow-s:rgba(223,104,185,.40); }
body.sn-platform .mf-card-techtacular { --accent:#d6db47; --accent-2:#f2f58a; --glow-s:rgba(214,219,71,.36); } body.sn-platform .mf-card-techtacular { --accent:#d6db47; --accent-2:#f2f58a; --glow-s:rgba(214,219,71,.36); }
body.sn-platform .mf-card-devlab { --accent:#78d7a0; --accent-2:#b4ebc8; --glow-s:rgba(120,215,160,.40); } body.sn-platform .mf-card-devlab { --accent:#78d7a0; --accent-2:#b4ebc8; --glow-s:rgba(120,215,160,.40); }
body.sn-platform .mf-card-syntaxsaga { --accent:#b176ef; --accent-2:#dfbfff; --glow-s:rgba(177,118,239,.40); } body.sn-platform .mf-card-syntaxsaga { --accent:#9276f0; --accent-2:#d1c8ff; --glow-s:rgba(146,118,240,.40); }
body.sn-platform .mf-card-codeescape { --accent:#e5ae5b; --accent-2:#f8d09b; --glow-s:rgba(229,174,91,.38); } body.sn-platform .mf-card-codeescape { --accent:#edcb4c; --accent-2:#fff0a8; --glow-s:rgba(237,203,76,.38); }
body.sn-platform .mf-card-serviceclash { --accent:#3abec5; --accent-2:#9de9e6; --glow-s:rgba(58,190,197,.38); } body.sn-platform .mf-card-serviceclash { --accent:#3abec5; --accent-2:#9de9e6; --glow-s:rgba(58,190,197,.38); }
body.sn-platform .mf-card-bughunter { --accent:#e85e68; --accent-2:#ffb6bd; --glow-s:rgba(232,94,104,.38); } body.sn-platform .mf-card-bughunter { --accent:#e85e68; --accent-2:#ffb6bd; --glow-s:rgba(232,94,104,.38); }
body.sn-platform .mf-card-networker { --accent:#55bdf0; --accent-2:#b2e7ff; --glow-s:rgba(85,189,240,.38); } body.sn-platform .mf-card-networker { --accent:#55bdf0; --accent-2:#b2e7ff; --glow-s:rgba(85,189,240,.38); }
body.sn-platform .mf-card-finalprotocol { --accent:#f09043; --accent-2:#ffd0a2; --glow-s:rgba(240,144,67,.38); } body.sn-platform .mf-card-finalprotocol { --accent:#ed7849; --accent-2:#ffd0b5; --glow-s:rgba(237,120,73,.38); }
body.sn-platform .mf-footer { background:rgba(3,11,13,.72); border-color:#173438; } body.sn-platform .mf-footer { background:rgba(3,11,13,.72); border-color:#173438; }
body.sn-platform .mf-center { min-height:100vh; position:relative; } body.sn-platform .mf-center { min-height:100vh; position:relative; }
body.sn-platform .auth-container { background:linear-gradient(145deg,rgba(10,29,32,.98),rgba(4,14,17,.98)); border-color:rgba(120,215,160,.46); } body.sn-platform .auth-container { background:linear-gradient(145deg,rgba(10,29,32,.98),rgba(4,14,17,.98)); border-color:rgba(120,215,160,.46); }
@@ -35,7 +35,7 @@
<header style="margin-bottom:1.4rem"> <header style="margin-bottom:1.4rem">
<div class="mf-eyebrow">Control Center</div> <div class="mf-eyebrow">Control Center</div>
<h1 style="color:var(--heading); font-size:1.85rem; font-weight:800; margin-top:.35rem">✨ Admin-Dashboard ✨</h1> <h1 style="color:var(--heading); font-size:1.85rem; font-weight:800; margin-top:.35rem">✨ Admin-Dashboard ✨</h1>
<p style="color:var(--muted); margin-top:.4rem">Hier steuerst du die Plattform und ihre Lerninhalte.</p> <p style="color:var(--muted); margin-top:.4rem">Hier steuerst du die verfügbaren Lerninhalte und die Benutzerverwaltung.</p>
</header> </header>
<main> <main>
@@ -52,7 +52,8 @@
<a th:href="@{/admin/bughunter}" class="admin-card">🕵️ BugHunter-Fallakten verwalten</a> <a th:href="@{/admin/bughunter}" class="admin-card">🕵️ BugHunter-Fallakten verwalten</a>
</div> </div>
<p style="margin-top:1.1rem; color:var(--muted);"> <p style="margin-top:1.1rem; color:var(--muted);">
Hier entsteht noch Großes... ✨ Bleib dran! Für ServiceClash, NetWorker und FinalProtocol gibt es derzeit bewusst keine
Inhaltsverwaltung: Ihre Aufgaben werden noch direkt mit der Anwendung ausgeliefert.
</p> </p>
</div> </div>
+66 -12
View File
@@ -10,10 +10,21 @@
<script src="/js/theme.js"></script> <script src="/js/theme.js"></script>
<style> <style>
/* Dashboard-spezifische Widgets (rechte Spalte) */ /* Dashboard-spezifische Widgets (rechte Spalte) */
.gauge { display:flex; flex-direction:column; align-items:center; gap:.2rem; } .network-status { display:grid; grid-template-columns:auto 1fr; gap:.85rem; align-items:start; }
.gauge svg { width:130px; height:130px; } .network-status svg { width:54px; height:54px; color:var(--accent); filter:drop-shadow(0 0 8px var(--glow-s)); }
.gauge .ok { color:var(--accent); font-weight:800; font-size:1rem; letter-spacing:.05em; margin-top:.3rem; text-shadow:0 0 12px var(--glow-s); } .status-summary { color:var(--heading); font-size:.9rem; font-weight:800; letter-spacing:.04em; }
.gauge .op { color:var(--muted); font-size:.72rem; } .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:#edcb4c; }
.network-status.restricted .status-summary::before { background:#edcb4c; box-shadow:0 0 8px rgba(237,203,76,.48); }
.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; }
.prog-top { display:flex; justify-content:space-between; align-items:center; margin-bottom:.4rem; } .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; } .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 { display:flex; align-items:baseline; gap:.5rem; }
@@ -68,15 +79,19 @@
<!-- ===== Rechte Spalte ===== --> <!-- ===== Rechte Spalte ===== -->
<aside class="mf-rightbar"> <aside class="mf-rightbar">
<div class="mf-panel"> <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> System Status</h4> <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="gauge"> <div class="network-status" id="platformStatus" aria-live="polite">
<svg viewBox="0 0 120 120" fill="none"> <svg viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="60" cy="60" r="50" stroke="var(--line)" stroke-width="6"/> <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="60" cy="60" r="50" stroke="var(--accent)" stroke-width="6" stroke-linecap="round" stroke-dasharray="300 314" transform="rotate(-90 60 60)" style="filter:drop-shadow(0 0 6px var(--glow-s))"/> <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"/>
<path d="M37 52l17-13 20 8-8 21-22 8-12-15zM54 39l-2 22m22-14-22 14m-8 15 8-15m14 7-14-7" stroke="var(--accent)" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="37" cy="52" r="3" fill="var(--accent)"/><circle cx="54" cy="39" r="3" fill="var(--accent)"/><circle cx="74" cy="47" r="3" fill="var(--accent)"/><circle cx="66" cy="68" r="3" fill="var(--accent)"/><circle cx="44" cy="76" r="3" fill="var(--accent)"/><circle cx="52" cy="61" r="3" fill="var(--accent)"/>
</svg> </svg>
<div class="ok">ALLE SYSTEME STABIL</div><div class="op">100% OPERATIONAL</div> <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> </div>
@@ -156,6 +171,45 @@
const points = values.map((value, index) => `${Math.round(index * 40)} ${64 - Math.round(value / max * 54)}`); const points = values.map((value, index) => `${Math.round(index * 40)} ${64 - Math.round(value / max * 54)}`);
const line = `M${points.join(' L')}`; 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))"/>`; 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))"/>`;
// 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> </script>
</body> </body>
</html> </html>
+2 -4
View File
@@ -8,7 +8,6 @@
<script src="/js/theme.js"></script> <script src="/js/theme.js"></script>
<style> <style>
.mf-cardbox { max-width:560px; text-align:center; } .mf-cardbox { max-width:560px; text-align:center; }
.mf-rocket { font-size:3rem; margin-bottom:.6rem; filter:drop-shadow(0 0 14px var(--glow-s)); }
.mf-cardbox h1 { color:var(--heading); font-size:1.5rem; font-weight:800; margin-bottom:.5rem; } .mf-cardbox h1 { color:var(--heading); font-size:1.5rem; font-weight:800; margin-bottom:.5rem; }
.mf-cardbox .slogan { color:var(--muted); font-size:.95rem; margin-bottom:1.6rem; } .mf-cardbox .slogan { color:var(--muted); font-size:.95rem; margin-bottom:1.6rem; }
.mf-cardbox .quote { color:var(--accent); font-size:1.05rem; line-height:1.6; font-style:italic; .mf-cardbox .quote { color:var(--accent); font-size:1.05rem; line-height:1.6; font-style:italic;
@@ -23,9 +22,8 @@
<div class="mf-center"> <div class="mf-center">
<div class="mf-cardbox"> <div class="mf-cardbox">
<div class="mf-rocket">🚀</div> <h1>Bereich in Vorbereitung</h1>
<h1>Zukunft in Arbeit</h1> <p class="slogan">Hier entsteht ein weiterer Knoten für dein Lernnetz.</p>
<p class="slogan">Hier entsteht etwas, das eure Zeitlinie verändern wird...</p>
<p class="quote"> <p class="quote">
"Dieser Bereich wird gerade für dein Lernnetz vorbereitet." "Dieser Bereich wird gerade für dein Lernnetz vorbereitet."
+1 -1
View File
@@ -80,7 +80,7 @@
<input id="reg-password2" class="mf-input" type="password" name="confirmPassword" placeholder="Passwort wiederholen" required> <input id="reg-password2" class="mf-input" type="password" name="confirmPassword" placeholder="Passwort wiederholen" required>
</div> </div>
<p id="reg-pw-msg" class="mf-alert err" style="display:none">Die Passwörter stimmen nicht überein.</p> <p id="reg-pw-msg" class="mf-alert err" style="display:none">Die Passwörter stimmen nicht überein.</p>
<button class="neon-btn" type="submit"><span class="lock">🚀</span> Account erstellen</button> <button class="neon-btn" type="submit">Account erstellen</button>
</form> </form>
<p class="mobile-switch">Schon dabei? <a data-goto="login">Zum Login</a></p> <p class="mobile-switch">Schon dabei? <a data-goto="login">Zum Login</a></p>
+3 -2
View File
@@ -81,8 +81,9 @@
<!-- Diagramme --> <!-- Diagramme -->
<script th:inline="javascript"> <script th:inline="javascript">
// Theme-Tokens auslesen, damit die Charts in Dark (Grün) & Light (Magenta) passen // Die Plattform-Tokens liegen auf body.sn-platform. Von dort lesen, damit die
const css = getComputedStyle(document.documentElement); // Diagramme nicht auf die alte globale Akzentfarbe zurückfallen.
const css = getComputedStyle(document.body);
const accent = css.getPropertyValue('--accent').trim() || '#2bd96a'; const accent = css.getPropertyValue('--accent').trim() || '#2bd96a';
const track = css.getPropertyValue('--surface-2').trim() || '#2c2c2c'; const track = css.getPropertyValue('--surface-2').trim() || '#2c2c2c';
const tick = css.getPropertyValue('--text').trim() || '#f1f1f1'; const tick = css.getPropertyValue('--text').trim() || '#f1f1f1';
@@ -4,11 +4,14 @@ import com.example.mindflux.config.SecurityConfig;
import com.example.mindflux.controller.AdminUserController; import com.example.mindflux.controller.AdminUserController;
import com.example.mindflux.controller.DashboardController; import com.example.mindflux.controller.DashboardController;
import com.example.mindflux.controller.ProgressController; import com.example.mindflux.controller.ProgressController;
import com.example.mindflux.controller.PlatformStatusController;
import com.example.mindflux.dto.ProgressData; import com.example.mindflux.dto.ProgressData;
import com.example.mindflux.dto.PlatformStatus;
import com.example.mindflux.dto.WeeklyActivity; import com.example.mindflux.dto.WeeklyActivity;
import com.example.mindflux.repository.UserRepository; import com.example.mindflux.repository.UserRepository;
import com.example.mindflux.service.LoginAttemptService; import com.example.mindflux.service.LoginAttemptService;
import com.example.mindflux.service.ProgressService; import com.example.mindflux.service.ProgressService;
import com.example.mindflux.service.PlatformStatusService;
import com.example.mindflux.service.UserService; import com.example.mindflux.service.UserService;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -27,7 +30,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/** Absichert die zentralen Security-Regeln: Login-Zwang, Admin-Schutz, gelöschter Legacy-Endpunkt. */ /** Absichert die zentralen Security-Regeln: Login-Zwang, Admin-Schutz, gelöschter Legacy-Endpunkt. */
@WebMvcTest(controllers = {DashboardController.class, ProgressController.class, AdminUserController.class}) @WebMvcTest(controllers = {DashboardController.class, ProgressController.class, PlatformStatusController.class, AdminUserController.class})
@Import(SecurityConfig.class) @Import(SecurityConfig.class)
class SecurityRulesTest { class SecurityRulesTest {
@@ -37,6 +40,9 @@ class SecurityRulesTest {
@MockitoBean @MockitoBean
private ProgressService progressService; private ProgressService progressService;
@MockitoBean
private PlatformStatusService platformStatusService;
@MockitoBean @MockitoBean
private UserService userService; private UserService userService;
@@ -60,6 +66,21 @@ class SecurityRulesTest {
.andExpect(status().isOk()); .andExpect(status().isOk());
} }
@Test
void lernnetzStatusIstNichtOeffentlich() throws Exception {
mockMvc.perform(get("/api/platform/status"))
.andExpect(status().is3xxRedirection());
}
@Test
@WithMockUser(username = "alice")
void eingeloggteNutzerKoennenDenLernnetzStatusPruefen() throws Exception {
when(platformStatusService.currentStatus()).thenReturn(new PlatformStatus(true, 9, java.time.Instant.now()));
mockMvc.perform(get("/api/platform/status"))
.andExpect(status().isOk());
}
@Test @Test
@WithMockUser(username = "alice") @WithMockUser(username = "alice")
void eingeloggteNutzerSehenDieFortschrittsseite() throws Exception { void eingeloggteNutzerSehenDieFortschrittsseite() throws Exception {
@@ -0,0 +1,38 @@
package com.example.mindflux.service;
import com.example.mindflux.repository.GameScoreRepository;
import org.junit.jupiter.api.Test;
import org.springframework.dao.DataAccessResourceFailureException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class PlatformStatusServiceTest {
@Test
void reportsReadyOnlyWhenTheProgressStoreCanBeRead() {
GameScoreRepository repository = mock(GameScoreRepository.class);
when(repository.count()).thenReturn(12L);
var status = new PlatformStatusService(repository).currentStatus();
assertThat(status.ready()).isTrue();
assertThat(status.storageConnected()).isTrue();
assertThat(status.registeredModules()).isEqualTo(9);
verify(repository).count();
}
@Test
void reportsRestrictedWhenTheProgressStoreIsUnavailable() {
GameScoreRepository repository = mock(GameScoreRepository.class);
doThrow(new DataAccessResourceFailureException("database unavailable")).when(repository).count();
var status = new PlatformStatusService(repository).currentStatus();
assertThat(status.ready()).isFalse();
assertThat(status.storageConnected()).isFalse();
}
}