This commit is contained in:
@@ -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
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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."
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user