Make progress data-driven and build Knowledge Core
mindflux-ci / build (push) Successful in 23s

This commit is contained in:
AliceDarkest
2026-07-13 22:31:57 +02:00
parent b0d8dd48db
commit d718a09561
15 changed files with 205 additions and 56 deletions
@@ -6,6 +6,8 @@ import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import java.security.Principal;
import java.util.List;
import com.example.mindflux.dto.WeeklyActivity;
@Controller
public class DashboardController {
@@ -20,6 +22,14 @@ public class DashboardController {
public String showDashboard(Model model, Principal principal) {
// Echte XP/Level/Streak-Werte für die Sidebar-Widgets (statt Dummy-Daten im Template).
model.addAttribute("overall", progressService.getOverallProgress(principal.getName()));
WeeklyActivity weekly = progressService.getCurrentWeekActivity(principal.getName());
model.addAttribute("weekly", weekly != null ? weekly : emptyWeekly());
var topGames = progressService.getTopGames(principal.getName(), 4);
model.addAttribute("topGames", topGames != null ? topGames : List.of());
return "dashboard";
}
private WeeklyActivity emptyWeekly() {
return new WeeklyActivity(List.of(0, 0, 0, 0, 0, 0, 0), List.of(0, 0, 0, 0, 0, 0, 0), 0, 0);
}
}
@@ -2,6 +2,7 @@ package com.example.mindflux.controller;
import com.example.mindflux.model.User;
import com.example.mindflux.service.UserService;
import com.example.mindflux.service.ProgressService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -27,6 +28,8 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.Principal;
import java.util.UUID;
import java.util.List;
import com.example.mindflux.dto.WeeklyActivity;
@Controller
public class ProfileController {
@@ -39,6 +42,9 @@ public class ProfileController {
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Autowired
private ProgressService progressService;
private static final String UPLOAD_DIR = System.getProperty("user.dir") + "/uploads/profiles/";
@GetMapping("/profile")
@@ -97,6 +103,12 @@ public class ProfileController {
User user = userService.findByUsername(username);
model.addAttribute("user", user);
model.addAttribute("overall", progressService.getOverallProgress(username));
WeeklyActivity weekly = progressService.getCurrentWeekActivity(username);
model.addAttribute("weekly", weekly != null ? weekly : new WeeklyActivity(
List.of(0, 0, 0, 0, 0, 0, 0), List.of(0, 0, 0, 0, 0, 0, 0), 0, 0));
var topGames = progressService.getTopGames(username, 3);
model.addAttribute("topGames", topGames != null ? topGames : List.of());
return "knowledge";
}
@@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.GetMapping;
import java.security.Principal;
import java.util.List;
import com.example.mindflux.dto.WeeklyActivity;
@Controller
public class ProgressController {
@@ -41,6 +42,9 @@ public class ProgressController {
}
model.addAttribute("gameProgress", games != null ? games : List.of()); // leere Liste, falls null
WeeklyActivity weekly = progressService.getCurrentWeekActivity(username);
model.addAttribute("weekly", weekly != null ? weekly : new WeeklyActivity(
List.of(0, 0, 0, 0, 0, 0, 0), List.of(0, 0, 0, 0, 0, 0, 0), 0, 0));
return "progress"; // ruft progress.html auf
}
@@ -0,0 +1,7 @@
package com.example.mindflux.dto;
import java.util.List;
/** Tatsächlich aufgezeichnete Aktivität von Montag bis Sonntag. */
public record WeeklyActivity(List<Integer> minutes, List<Integer> xp, int totalMinutes, int totalXp) {
}
@@ -7,6 +7,7 @@ import org.springframework.data.repository.query.Param;
import java.util.List;
import java.util.Optional;
import java.time.LocalDateTime;
public interface GameScoreRepository extends JpaRepository<GameScore, Long> {
@@ -19,6 +20,10 @@ public interface GameScoreRepository extends JpaRepository<GameScore, Long> {
/** Wie oft ein Spieler ein Spiel abgeschlossen hat. */
long countByUsernameAndGame(String username, String game);
/** Abgeschlossene Runden eines Zeitraums – Basis für Spielzeit und Wochenverlauf. */
List<GameScore> findByUsernameAndPlayedAtGreaterThanEqualAndPlayedAtLessThanOrderByPlayedAtAsc(
String username, LocalDateTime from, LocalDateTime until);
/** Alle Spielstände eines Spielers löschen (z. B. beim Löschen des Kontos). */
void deleteByUsername(String username);
@@ -10,6 +10,10 @@ import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import com.example.mindflux.dto.WeeklyActivity;
/**
* Speichert Spielergebnisse und liefert Statistiken. Der Score wird hier
@@ -110,4 +114,39 @@ public class GameScoreService {
.orElse(null);
return new GameStats(highscore, plays, lastPlayed);
}
/**
* Aggregiert ausschließlich gespeicherte Abschlusszeiten. Runden ohne gemessene
* Dauer zählen für XP, aber bewusst nicht als Spielzeit.
*/
public WeeklyActivity weeklyActivity(String username, LocalDate monday) {
LocalDateTime from = monday.atStartOfDay();
LocalDateTime until = monday.plusDays(7).atStartOfDay();
int[] seconds = new int[7];
int[] xp = new int[7];
for (GameScore score : repository
.findByUsernameAndPlayedAtGreaterThanEqualAndPlayedAtLessThanOrderByPlayedAtAsc(username, from, until)) {
int day = (int) (score.getPlayedAt().toLocalDate().toEpochDay() - monday.toEpochDay());
if (day < 0 || day >= 7) {
continue;
}
xp[day] += Math.max(score.getScore(), 0);
if (score.getDurationSeconds() != null) {
seconds[day] += Math.max(score.getDurationSeconds(), 0);
}
}
List<Integer> minutes = new ArrayList<>(7);
List<Integer> weeklyXp = new ArrayList<>(7);
int totalSeconds = 0;
int totalXp = 0;
for (int day = 0; day < 7; day++) {
minutes.add((int) Math.ceil(seconds[day] / 60.0));
weeklyXp.add(xp[day]);
totalSeconds += seconds[day];
totalXp += xp[day];
}
return new WeeklyActivity(minutes, weeklyXp, (int) Math.ceil(totalSeconds / 60.0), totalXp);
}
}
@@ -3,6 +3,7 @@ package com.example.mindflux.service;
import com.example.mindflux.dto.GameProgress;
import com.example.mindflux.dto.GameStats;
import com.example.mindflux.dto.ProgressData;
import com.example.mindflux.dto.WeeklyActivity;
import org.springframework.stereotype.Service;
@@ -11,6 +12,9 @@ import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Comparator;
import java.time.temporal.TemporalAdjusters;
import java.time.DayOfWeek;
@Service
public class ProgressService {
@@ -91,6 +95,25 @@ public class ProgressService {
return result;
}
/** Aktuelle Wochenwerte, jeweils Montag bis Sonntag. */
public WeeklyActivity getCurrentWeekActivity(String username) {
LocalDate monday = LocalDate.now().with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY));
return gameScoreService.weeklyActivity(username, monday);
}
/** Die tatsächlich gespielten Spiele, nach ihrem Highscore sortiert. */
public List<GameProgress> getTopGames(String username, int limit) {
List<GameProgress> games = getGameProgress(username).stream()
.filter(game -> game.getPlays() > 0)
.sorted(Comparator.comparingInt(GameProgress::getHighscore).reversed()
.thenComparing(Comparator.comparingInt(GameProgress::getPlays).reversed()))
.limit(limit)
.toList();
int topScore = games.stream().mapToInt(GameProgress::getHighscore).max().orElse(1);
games.forEach(game -> game.setProgressPercent(Math.round(game.getHighscore() * 100f / topScore)));
return games;
}
private GameProgress toGameProgress(String displayName, GameStats stats) {
GameProgress game = new GameProgress();
game.setName(displayName);
@@ -14,4 +14,5 @@ body[class*="mf-a11y-skin-"] .mf-a11y-close{background:var(--mf-a11y-accent);col
body[class*="mf-a11y-skin-"] .mf-a11y-option[aria-pressed="true"]{border-color:var(--mf-a11y-accent);background:color-mix(in srgb,var(--mf-a11y-accent) 18%,var(--surface));box-shadow:inset 0 0 0 2px var(--mf-a11y-accent)}
.bh-cabinet-level{grid-column:1/-1;margin:.5rem 0 -.15rem;padding-bottom:.35rem;border-bottom:1px solid #60473d;color:var(--bh-accent);font:700 .66rem "Courier New",monospace;letter-spacing:.12em}
.bh-drawer.solved{overflow:hidden}.bh-solved-stamp{display:inline-block;border:3px solid #79bf91;border-radius:4px;padding:.24rem .48rem;color:#79bf91;font:700 .78rem/1 "Courier New",monospace;letter-spacing:.12em;transform:rotate(-12deg);text-shadow:none;box-shadow:0 0 12px color-mix(in srgb,#79bf91 26%,transparent);pointer-events:none}.bh-drawer .bh-solved-stamp{position:absolute;right:3.1rem;top:.75rem}.bh-solved-stamp-detail{position:absolute;right:1.4rem;top:1.25rem;font-size:1rem}.bh-solved-stamp-result{margin:0 0 .8rem;font-size:1.15rem}
.nw-app{background:repeating-linear-gradient(0deg,rgba(103,187,255,.12) 0 1px,transparent 1px 30px),repeating-linear-gradient(90deg,rgba(103,187,255,.12) 0 1px,transparent 1px 30px),radial-gradient(circle at 50% -20%,rgba(27,89,148,.38),transparent 48%),#071527!important}.nw-board{background:repeating-linear-gradient(0deg,rgba(103,187,255,.12) 0 1px,transparent 1px 30px),repeating-linear-gradient(90deg,rgba(103,187,255,.12) 0 1px,transparent 1px 30px),rgba(5,18,33,.9)!important}
@media(max-width:700px){.mf-a11y-panel{inset:70px 6px auto 6px!important;width:auto;max-height:calc(100dvh - 82px)}}
+1 -1
View File
@@ -126,7 +126,7 @@ a { color:var(--secondary); }
.mf-card-codeescape { --accent:var(--esc-accent); --glow-s:var(--esc-glow); --glow-w:color-mix(in srgb,var(--esc-accent) 16%,transparent); background:repeating-linear-gradient(0deg,rgba(255,255,255,.015) 0 1px,transparent 1px 4px),var(--surface); }
.mf-card-codeescape::after { content:"ACCESS"; position:absolute; right:12px; top:9px; color:var(--esc-accent); font:700 .52rem/1 "Courier New",monospace; letter-spacing:.12em; opacity:.6; }
.mf-card-bughunter { --accent:var(--esc-accent); --glow-s:var(--esc-glow); --glow-w:color-mix(in srgb,var(--esc-accent) 16%,transparent); background:linear-gradient(135deg,color-mix(in srgb,var(--esc-accent) 8%,transparent),var(--surface)); }
.mf-card-bughunter { --accent:var(--esc-accent); --glow-s:var(--esc-glow); --glow-w:color-mix(in srgb,var(--esc-accent) 16%,transparent); background:var(--surface); }
.mf-card-bughunter::after { content:"FALLAKTE"; position:absolute; right:12px; top:9px; color:var(--esc-accent); font:700 .52rem/1 "Courier New",monospace; letter-spacing:.12em; opacity:.7; }
/* ===== Panels / Widgets ===== */
+1 -1
View File
@@ -32,7 +32,7 @@
document.head.appendChild(accessibilityCss);
var accessibilityPlacementCss = document.createElement('link');
accessibilityPlacementCss.rel = 'stylesheet';
accessibilityPlacementCss.href = '/css/accessibility-placement.css?v=6';
accessibilityPlacementCss.href = '/css/accessibility-placement.css?v=7';
accessibilityPlacementCss.setAttribute('data-accessibility', '');
document.head.appendChild(accessibilityPlacementCss);
}
+18 -14
View File
@@ -81,22 +81,23 @@
</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">Woche ▾</span></div>
<div class="op" style="color:var(--muted);font-size:.72rem;margin:.2rem 0">Gesamtfortschritt</div>
<div class="prog-big"><b>65<span>%</span></b><span class="xpbadge">+120 XP</span></div>
<div class="chart">
<svg viewBox="0 0 240 74" preserveAspectRatio="none">
<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="M0 60 L34 50 L70 56 L104 38 L140 44 L176 24 L210 30 L240 14 L240 74 L0 74 Z" fill="url(#ar)"/>
<path d="M0 60 L34 50 L70 56 L104 38 L140 44 L176 24 L210 30 L240 14" fill="none" stroke="var(--accent)" stroke-width="2" style="filter:drop-shadow(0 0 5px var(--glow-s))"/>
<circle cx="176" cy="24" r="3.5" fill="var(--accent)"/>
</svg>
<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 Kategorien</h4>
<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">
@@ -148,9 +149,12 @@
<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 SVG={globe:'<circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3c3 3 3 15 0 18M12 3c-3 3-3 15 0 18"/>',chart:'<path d="M4 20V10M10 20V4M16 20v-8M22 20H2"/>',lock:'<rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/>',grid:'<rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/>',code:'<path d="M8 7l-5 5 5 5M16 7l5 5-5 5"/>'};
const CATS=[['Netzwerk','globe',85],['Datenbanken','chart',78],['IT-Sicherheit','lock',72],['Hardware','grid',66],['Programmierung','code',60]];
document.getElementById('cats').innerHTML += CATS.map(([n,i,p])=>`<div class="cat"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${SVG[i]}</svg><span class="nm">${n}</span><span class="cbar"><i style="width:${p}%"></i></span><span class="pct">${p}%</span></div>`).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))"/>`;
</script>
</body>
</html>
+25 -26
View File
@@ -3,42 +3,41 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mindflux – Knowledge Center</title>
<title>Knowledge Core – Mindflux</title>
<link rel="stylesheet" href="/css/theme.css">
<script src="/js/theme.js"></script>
<style>
.mf-footer { text-align:center; color:var(--muted); font-size:.82rem; padding:1.6rem 1rem; }
.mf-footer a { color:var(--muted); text-decoration:none; margin:0 .15rem; }
.mf-footer a:hover { color:var(--accent); }
.kc-intro{max-width:720px;color:var(--muted);line-height:1.6}.kc-stats,.kc-routes{display:grid;gap:1rem}.kc-stats{grid-template-columns:repeat(3,1fr);margin:1.2rem 0}.kc-stat{padding:1rem;border:1px solid var(--border);border-radius:10px;background:var(--surface-2)}.kc-stat span,.kc-route small{display:block;color:var(--muted);font-size:.74rem}.kc-stat b{display:block;margin-top:.25rem;color:var(--heading);font-size:1.35rem}.kc-routes{grid-template-columns:repeat(3,1fr)}.kc-route{display:flex;flex-direction:column;gap:.7rem;padding:1.2rem;border:1px solid var(--border);border-radius:12px;background:var(--surface)}.kc-route h2{color:var(--heading);font-size:1.05rem;margin:0}.kc-route p{color:var(--muted);font-size:.86rem;line-height:1.5;flex:1;margin:0}.kc-links{display:flex;flex-wrap:wrap;gap:.5rem}.kc-links a{color:var(--accent);font-size:.76rem;text-decoration:none;border:1px solid var(--border-accent);border-radius:7px;padding:.42rem .55rem}.kc-links a:hover{background:var(--accent);color:var(--accent-ink)}.kc-history{margin-top:1rem}.kc-history li{display:flex;justify-content:space-between;gap:1rem;padding:.65rem 0;border-bottom:1px solid var(--border);color:var(--text)}.kc-history li span{color:var(--muted);font-size:.82rem}.mf-footer{text-align:center;color:var(--muted);font-size:.82rem;padding:1.6rem 1rem}.mf-footer a{color:var(--muted);text-decoration:none;margin:0 .15rem}.mf-footer a:hover{color:var(--accent)}@media(max-width:720px){.kc-stats,.kc-routes{grid-template-columns:1fr}}
</style>
</head>
<body>
<div class="mf-theme-float"><button class="mf-theme"></button></div>
<div class="mf-container">
<div class="mf-page-head">
<a th:href="@{/dashboard}" class="mf-back">⟵ Zurück zum Dashboard</a>
</div>
<main class="mf-container">
<div class="mf-page-head"><a th:href="@{/dashboard}" class="mf-back">⟵ Zurück zum Dashboard</a></div>
<span class="mf-eyebrow">Knowledge Core</span>
<h1 style="color:var(--heading);font-size:2.1rem;margin:.3rem 0 .45rem;">Deine Lernzentrale</h1>
<p class="kc-intro">Wähle einen Prüfungsweg oder vertiefe ein Spielmodul. Der Status hier basiert auf deinen gespeicherten XP und abgeschlossenen Runden.</p>
<div class="mf-panel accent" style="text-align:center; padding:2.5rem 1.5rem;">
<h1 style="color:var(--heading); font-size:1.7rem; font-weight:800; margin-bottom:.7rem;">Welcome to the Knowledge Center!</h1>
<p style="color:var(--text);">Hier entsteht dein cooles Modul 📚🧠</p>
</div>
<section class="kc-stats" aria-label="Dein Lernstatus">
<div class="kc-stat"><span>Aktuelles Level</span><b th:text="${overall.userLevel}">1</b></div>
<div class="kc-stat"><span>XP diese Woche</span><b th:text="${weekly.totalXp}">0</b></div>
<div class="kc-stat"><span>Gemessene Spielzeit</span><b th:text="${weekly.totalMinutes} + ' Min.'">0 Min.</b></div>
</section>
</div>
<!-- Footer -->
<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>
<section class="kc-routes" aria-label="Lernwege">
<article class="kc-route"><small>PRÜFUNGSGRUNDLAGEN</small><h2>AP1 kompakt</h2><p>Grundlagen, Zahlensysteme, Datenschutz und Projektarbeit strukturiert wiederholen.</p><div class="kc-links"><a href="/finalflux">FinalFlux öffnen</a><a href="/finalflux/ap1/ip-berechnung.html">IP-Berechnung</a></div></article>
<article class="kc-route"><small>SYSTEMINTEGRATION</small><h2>Netzwerke verstehen</h2><p>Topologien und Adressierung erst festigen, dann die Konfiguration in NetWorker trainieren.</p><div class="kc-links"><a href="/networker">NetWorker spielen</a><a href="/finalflux/systemintegration/osi.html">OSI-Modell</a></div></article>
<article class="kc-route"><small>ENTWICKLUNG &amp; SECURITY</small><h2>Fehler sicher lesen</h2><p>Debugging-Fälle lösen, Zugänge absichern und die Muster danach im Lernstoff nachschlagen.</p><div class="kc-links"><a href="/bughunter">BugHunter spielen</a><a href="/codeescape">SystemEscape spielen</a></div></article>
</section>
<section class="mf-panel kc-history">
<h2 class="mf-section-title">Deine stärksten Spiele</h2>
<p th:if="${#lists.isEmpty(topGames)}" style="color:var(--muted);font-size:.86rem;">Noch keine abgeschlossene Runde – dein erster Highscore erscheint hier.</p>
<ul th:unless="${#lists.isEmpty(topGames)}" style="list-style:none;padding:0;margin:0;">
<li th:each="game : ${topGames}"><b th:text="${game.name}">MindMatch</b><span th:text="${game.highscore} + ' XP · ' + ${game.plays} + ' Runden'">0 XP · 0 Runden</span></li>
</ul>
</section>
</main>
<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>
</body>
</html>
+1 -1
View File
@@ -1 +1 @@
<!doctype html><html lang="de" data-theme="dark"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>NetWorker · Mindflux</title><link rel="stylesheet" href="/css/theme.css"><link rel="stylesheet" href="/css/networker.css?v=1"><script src="/js/theme.js"></script></head><body><main class="nw-app"><header class="nw-top"><a href="/dashboard" class="nw-back">← Dashboard</a><div class="nw-brand"><b>NETWORKER</b><span>NETZPLAN · DIAGNOSE</span></div><span class="mf-a11y-host"></span></header><section id="nwView" class="nw-view"><div class="nw-loading">NETZWERKPLÄNE WERDEN KALIBRIERT …</div></section></main><script src="/js/networker.js?v=1"></script></body></html>
<!doctype html><html lang="de" data-theme="dark"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>NetWorker · Mindflux</title><link rel="stylesheet" href="/css/theme.css"><link rel="stylesheet" href="/css/networker.css?v=1"><script src="/js/theme.js"></script></head><body><main class="nw-app"><header class="nw-top"><a href="/dashboard" class="nw-back">← Dashboard</a><div class="nw-brand"><b>NetWorker</b><span>NETZPLAN · DIAGNOSE</span></div><span class="mf-a11y-host"></span></header><section id="nwView" class="nw-view"><div class="nw-loading">NETZWERKPLÄNE WERDEN KALIBRIERT …</div></section></main><script src="/js/networker.js?v=1"></script></body></html>
+12 -13
View File
@@ -49,28 +49,27 @@
<p class="mf-percentage" th:text="${xpPercentage + '%'}">78%</p>
</div>
<div class="mf-progress-info">
<p><strong>Korrekte Antworten:</strong> <span th:text="${correctAnswers}">432</span></p>
<p><strong>Level:</strong> <span th:text="${userLevel}">5</span></p>
<p><strong>Erfasste Spielaktionen:</strong> <span th:text="${correctAnswers}">432</span></p>
<p><strong>Level (XP):</strong> <span th:text="${userLevel}">5</span></p>
<div class="mf-bar" style="margin:.6rem 0 .4rem;"><i th:style="'width:' + ${xpPercentage} + '%'"></i></div>
<p><span th:text="${currentXp}">2400</span> / <span th:text="${xpForNextLevel}">3000</span> XP</p>
</div>
</div>
</section>
<!-- Lernzeit pro Woche -->
<!-- Tatsächlich gemessene Spielzeit pro Woche -->
<section class="mf-panel" style="margin-bottom:1.2rem;">
<h2 class="mf-section-title">Lernzeit – Diese Woche</h2>
<h2 class="mf-section-title">Spielzeit – Diese Woche</h2>
<p style="color:var(--muted);font-size:.82rem;margin:-.35rem 0 .9rem;">Nur die Dauer abgeschlossener, zeitgemessener Runden.</p>
<canvas id="learnTimeChart" width="600" height="180"></canvas>
</section>
<!-- Fortschritt pro Spiel -->
<!-- Echte Spielstatistiken -->
<section class="mf-panel" th:if="${gameProgress != null}" style="margin-bottom:1.2rem;">
<h2 class="mf-section-title">Spiel-Fortschritte</h2>
<h2 class="mf-section-title">Spiel-Statistiken</h2>
<div class="mf-game-grid">
<div class="mf-game-card" th:each="game : ${gameProgress}">
<h3 th:text="${game.name}">Hangman</h3>
<p>Fortschritt: <span th:text="${game.progressPercent}">65</span>%</p>
<div class="mf-bar" style="margin:.4rem 0;"><i th:style="'width:' + ${game.progressPercent} + '%'"></i></div>
<p>Beste Punktzahl: <span th:text="${game.highscore}">1200</span></p>
<p>Gespielt: <span th:text="${game.plays}">15</span>×</p>
<p>Letzter Zugriff: <span th:text="${game.lastPlayed}">14.04.2025</span></p>
@@ -81,7 +80,7 @@
</div>
<!-- Diagramme -->
<script th:inline="none">
<script th:inline="javascript">
// Theme-Tokens auslesen, damit die Charts in Dark (Grün) & Light (Magenta) passen
const css = getComputedStyle(document.documentElement);
const accent = css.getPropertyValue('--accent').trim() || '#2bd96a';
@@ -95,7 +94,7 @@
type: 'doughnut',
data: {
datasets: [{
data: [78, 22], // Achtung: Dummywerte, kannst du später dynamisch machen
data: [/*[[${xpPercentage}]]*/ 0, 100 - /*[[${xpPercentage}]]*/ 0],
backgroundColor: [accent, track],
borderWidth: 0
}]
@@ -108,15 +107,15 @@
}
});
// Lernzeit-Balkendiagramm
// Spielzeit-Balkendiagramm
const learnCtx = document.getElementById('learnTimeChart').getContext('2d');
new Chart(learnCtx, {
type: 'bar',
data: {
labels: ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'],
datasets: [{
label: 'Lernzeit (Minuten)',
data: [30, 45, 20, 60, 50, 0, 10], // Beispielwerte
label: 'Spielzeit (Minuten)',
data: /*[[${weekly.minutes}]]*/ [0, 0, 0, 0, 0, 0, 0],
backgroundColor: accent,
borderRadius: 6
}]
@@ -0,0 +1,46 @@
package com.example.mindflux.service;
import com.example.mindflux.dto.WeeklyActivity;
import com.example.mindflux.model.GameScore;
import com.example.mindflux.repository.GameScoreRepository;
import org.junit.jupiter.api.Test;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class GameScoreActivityTest {
@Test
void wochenAktivitaetZaehltNurTatsaechlichGemesseneDauer() {
GameScoreRepository repository = mock(GameScoreRepository.class);
GameScoreService service = new GameScoreService(repository, mock(AchievementService.class));
LocalDate monday = LocalDate.of(2026, 7, 13);
when(repository.findByUsernameAndPlayedAtGreaterThanEqualAndPlayedAtLessThanOrderByPlayedAtAsc(
"alice", monday.atStartOfDay(), monday.plusDays(7).atStartOfDay()))
.thenReturn(List.of(
score(400, 10, monday.atTime(9, 0)),
score(250, 61, monday.plusDays(1).atTime(10, 0)),
score(150, null, monday.plusDays(1).atTime(11, 0))));
WeeklyActivity activity = service.weeklyActivity("alice", monday);
assertEquals(List.of(1, 2, 0, 0, 0, 0, 0), activity.minutes());
assertEquals(List.of(400, 400, 0, 0, 0, 0, 0), activity.xp());
assertEquals(2, activity.totalMinutes());
assertEquals(800, activity.totalXp());
}
private GameScore score(int points, Integer seconds, LocalDateTime playedAt) {
GameScore score = new GameScore();
score.setScore(points);
score.setDurationSeconds(seconds);
score.setPlayedAt(playedAt);
return score;
}
}