Volle Mindflux-App ins Deploy-Repo übernehmen

Ersetzt das Hello-World-Gerüst durch die vollständige Anwendung
(com.example.mindflux: 64 Klassen, Thymeleaf-Templates, Static-Assets,
Flyway V1–V6). Deploy-Infrastruktur (.gitea, Dockerfile) bleibt erhalten.

- pom: volle Abhängigkeiten (Security, Thymeleaf, JPA, Flyway), Spring Boot 3.4.4 / Java 17
- docker-compose: SPRING_DATASOURCE_* unverändert; App lauscht auf 8080
  (nginx -> mindflux:8080); Uploads-Volume; Admin-Passwörter via Gitea-Secrets
- Workflow: Deploy-Step injiziert MINDFLUX_ADMIN*_PASSWORD aus Secrets
- .dockerignore ergänzt; Hello-World (com.mindflux) und Test-Datei 'main' entfernt

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
AliceDarkest
2026-06-26 14:51:30 +02:00
parent f3309c1574
commit 9022961762
185 changed files with 22772 additions and 142 deletions
+267
View File
@@ -0,0 +1,267 @@
/* TechTacular – IT-Quizduell im Jeopardy-Stil.
Board kommt aus /api/techtacular/board (ohne Lösung), Prüfung + Score serverseitig. */
(() => {
'use strict';
const GAME = 'techtacular';
const boardEl = document.getElementById('tt-board');
const newBtn = document.getElementById('tt-new');
const finishBtn = document.getElementById('tt-finish');
const cancelBtn = document.getElementById('tt-cancel');
const scoreEl = document.getElementById('tt-score');
const answeredEl= document.getElementById('tt-answered');
const timeEl = document.getElementById('tt-time');
const bestEl = document.getElementById('tt-best');
const qOverlay = document.getElementById('tt-question');
const qCat = document.getElementById('tt-q-cat');
const qPoints = document.getElementById('tt-q-points');
const qText = document.getElementById('tt-q-text');
const qAnswers = document.getElementById('tt-q-answers');
const qFeedback = document.getElementById('tt-q-feedback');
const qNext = document.getElementById('tt-q-next');
const resultEl = document.getElementById('tt-result');
const resultScore = document.getElementById('tt-result-score');
const resultMax = document.getElementById('tt-result-max');
const resultDetail= document.getElementById('tt-result-detail');
const resultHigh = document.getElementById('tt-result-high');
const againBtn = document.getElementById('tt-again');
let board = null;
let previousIds = []; // Frage-IDs der Vorrunde -> werden im nächsten Board vermieden
let tiles = []; // Tile-Zustände {data, categoryTitle, answered, correct, el}
let answeredCount = 0;
let score = 0;
let seconds = 0;
let timerId = null;
let running = false;
let current = null; // gerade offene Kachel
const submitted = []; // {questionId, answerId} für /finish
function formatTime(s) {
return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`;
}
function startTimer() {
if (timerId) return;
timerId = setInterval(() => { seconds++; timeEl.textContent = formatTime(seconds); }, 1000);
}
function stopTimer() { clearInterval(timerId); timerId = null; }
function setIdle(message) {
boardEl.style.gridTemplateColumns = '';
boardEl.innerHTML = `<p class="tt-idle">${message}</p>`;
}
function updateHud() {
scoreEl.textContent = String(score);
answeredEl.textContent = `${answeredCount} / ${tiles.length}`;
}
function renderBoard() {
boardEl.innerHTML = '';
boardEl.style.gridTemplateColumns = `repeat(${board.categories.length}, 1fr)`;
tiles = [];
board.categories.forEach((category) => {
const col = document.createElement('div');
col.className = 'tt-col';
const title = document.createElement('div');
title.className = 'tt-cat-title';
title.textContent = category.title;
col.appendChild(title);
category.tiles.forEach((tileData) => {
const tileEl = document.createElement('button');
tileEl.type = 'button';
tileEl.className = 'tt-tile';
tileEl.textContent = String(tileData.points);
const state = { data: tileData, categoryTitle: category.title, answered: false, correct: false, el: tileEl };
tileEl.addEventListener('click', () => openTile(state));
tiles.push(state);
col.appendChild(tileEl);
});
boardEl.appendChild(col);
});
}
function openTile(state) {
if (!running || state.answered || current) return;
current = state;
qCat.textContent = state.categoryTitle;
qPoints.textContent = String(state.data.points);
qText.textContent = state.data.text;
qFeedback.textContent = '';
qFeedback.className = 'tt-q-feedback';
qNext.hidden = true;
qAnswers.innerHTML = '';
state.data.answers.forEach((answer) => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'tt-answer';
btn.textContent = `Was ist ${answer.text}?`;
btn.addEventListener('click', () => chooseAnswer(state, answer.id, btn));
qAnswers.appendChild(btn);
});
qOverlay.hidden = false;
}
async function chooseAnswer(state, answerId, btn) {
if (state.answered) return;
state.answered = true; // sofort sperren (eine Antwort pro Frage)
const buttons = Array.from(qAnswers.querySelectorAll('.tt-answer'));
buttons.forEach((b) => { b.disabled = true; });
submitted.push({ questionId: state.data.questionId, answerId });
try {
const res = await fetch('/api/techtacular/answer', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ questionId: state.data.questionId, answerId })
});
const data = await res.json();
state.correct = data.correct;
// richtige Antwort grün, eigene falsche rot markieren
state.data.answers.forEach((answer, i) => {
if (answer.id === data.correctAnswerId) buttons[i].classList.add('correct');
});
if (!data.correct) btn.classList.add('wrong');
const correctTerm = (state.data.answers.find((a) => a.id === data.correctAnswerId) || {}).text || '';
if (data.correct) {
score += data.points;
qFeedback.textContent = `Richtig! +${data.points}. ${data.explanation || ''}`.trim();
qFeedback.className = 'tt-q-feedback correct';
} else {
qFeedback.textContent = `Falsch – richtig: „Was ist ${correctTerm}?“. ${data.explanation || ''}`.trim();
qFeedback.className = 'tt-q-feedback wrong';
}
updateHud();
} catch (err) {
qFeedback.textContent = 'Antwort konnte nicht geprüft werden.';
qFeedback.className = 'tt-q-feedback wrong';
console.error(err);
}
qNext.hidden = false;
}
function closeQuestion() {
if (!current) return;
const state = current;
current = null;
qOverlay.hidden = true;
state.el.classList.add('answered', state.correct ? 'correct' : 'wrong');
state.el.textContent = state.correct ? `✓ ${state.data.points}` : '✗';
answeredCount++;
updateHud();
if (answeredCount === tiles.length) {
finishGame();
}
}
async function finishGame() {
if (!running) return;
running = false;
stopTimer();
finishBtn.hidden = true;
cancelBtn.hidden = true;
try {
const res = await fetch('/api/techtacular/finish', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ durationSeconds: seconds, answers: submitted })
});
const data = await res.json();
resultScore.textContent = String(data.score);
resultMax.textContent = String(data.maxScore);
resultDetail.textContent =
`${data.correctCount} von ${data.totalCount} Fragen richtig · ${formatTime(seconds)}`;
resultHigh.hidden = !data.newHighscore;
bestEl.textContent = String(data.highscore);
resultEl.hidden = false;
} catch (err) {
console.error('Runde konnte nicht gespeichert werden:', err);
}
}
function cancelGame() {
if (!running) return;
if (answeredCount > 0 && !window.confirm(
'Aktuelle Runde wirklich abbrechen? Sie wird nicht gespeichert.')) {
return;
}
running = false;
current = null;
stopTimer();
qOverlay.hidden = true;
finishBtn.hidden = true;
cancelBtn.hidden = true;
setIdle('Runde abgebrochen – klicke „Neues Spiel“ für ein frisches Board.');
}
async function loadStats() {
try {
const res = await fetch(`/api/games/${GAME}/stats`);
if (!res.ok) return;
const stats = await res.json();
bestEl.textContent = stats.highscore > 0 ? String(stats.highscore) : '–';
} catch (err) { /* egal */ }
}
async function newGame() {
stopTimer();
seconds = 0; score = 0; answeredCount = 0; current = null;
submitted.length = 0;
timeEl.textContent = '0:00';
resultEl.hidden = true;
qOverlay.hidden = true;
const excludeParam = previousIds.length ? ('?exclude=' + previousIds.join(',')) : '';
try {
const res = await fetch('/api/techtacular/board' + excludeParam);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
board = await res.json();
} catch (err) {
console.error('Board konnte nicht geladen werden:', err);
setIdle('Board konnte nicht geladen werden.');
return;
}
if (!board.categories || !board.categories.length) {
setIdle('Noch keine Kategorien vorhanden. Ein Admin kann sie unter „TechTacular verwalten“ anlegen.');
return;
}
renderBoard();
previousIds = board.categories.flatMap((c) => c.tiles.map((t) => t.questionId));
updateHud();
running = true;
finishBtn.hidden = false;
cancelBtn.hidden = false;
startTimer();
}
newBtn.addEventListener('click', newGame);
againBtn.addEventListener('click', newGame);
finishBtn.addEventListener('click', finishGame);
cancelBtn.addEventListener('click', cancelGame);
qNext.addEventListener('click', closeQuestion);
loadStats();
newGame();
})();