Wizard-Hoehe fix, Logout in Konto, Server-Reset, responsive Tabs, Set-Zeilen, Brand-Date
Image bauen und in die Registry schieben / build (push) Successful in 7s

- Wizard: feste Hoehe (600px, intern scrollbar), Schritt-Inhalt zentriert
  (.wizard-body) -> Schritte gleich gross, auch beim User anlegen
- Benutzer-Anlage: Bestaetigung 'Benutzer x angelegt.' (ohne 'Weiter mit'),
  Solo-Option nach User-Anlage deaktiviert (kein irrefuehrendes Umschalten)
- User-Chip + Abmelden aus der Topbar entfernt, 'Abmelden' in Konto-Sektion
- Topbar-Datum: 'Fr 02.10.2026' + aktive Session ('Push aktiv') statt
  redundantem 'Today'
- Tabs: flex-wrap statt Scrollen, auf Mobil volle Breite + kompakter
- Server-Reset (Owner): Passwort + 'RESET'-Eingabe, loescht alle Daten
- Benutzerliste: Querbalken vor dem Anlegen-Formular
- Tests: 163 Checks gruen (39+18+49+32+25)
This commit is contained in:
AI-Agent committed 2026-10-02 19:42:46 +00:00
1 parent 0ebaa8b132
commit 655316278a
6 files changed
+183 -52

No files matched your search

+97 -39
View File
@@ -114,6 +114,11 @@ const LANG = {
setting_saved: "Einstellung gespeichert.",
account: "Konto", account_desc: "Angemeldet als '{u}'. Kennwort ändern:",
change_pw: "Kennwort ändern", current_pw: "Aktuelles Kennwort",
logout_label: "Abmelden", session_active: "aktiv",
reset_server: "Server zurücksetzen",
reset_server_desc: "Löscht alle Daten und Benutzer dieser Instanz. Die App startet danach wie neu (Einrichtungs-Wizard). Gib dein Kennwort ein und tippe RESET zum Bestätigen.",
reset_confirm_hint: "Zum Bestätigen RESET eintippen",
reset_server_go: "Zurücksetzen", reset_done: "Server zurückgesetzt.",
new_pw: "Neues Kennwort", pw_changed: "Kennwort geändert.",
users: "Benutzer", setup_users: "Benutzer einrichten",
users_solo_desc: "Du bist im Einzelmodus (ohne Anmeldung). Lege den ersten Benutzer an, um Login und mehrere Benutzer zu aktivieren.",
@@ -152,7 +157,7 @@ const LANG = {
reorder_hint: "Übungen per Ziehen sortieren",
prev_week: "Vorige Woche", next_week: "Nächste Woche", week_of: "Woche vom",
wizard_done: "Alles erledigt - die App ist bereit.",
wizard_user_created: "Benutzer '{u}' angelegt. Weiter mit 'Weiter'.",
wizard_user_created: "Benutzer '{u}' angelegt.",
logged_in: "Angemeldet.", user_created_toast: "Benutzer angelegt.",
app_load_fail: "App konnte nicht geladen werden: {m}",
},
@@ -224,6 +229,11 @@ const LANG = {
setting_saved: "Setting saved.",
account: "Account", account_desc: "Logged in as '{u}'. Change password:",
change_pw: "Change password", current_pw: "Current password",
logout_label: "Log out", session_active: "active",
reset_server: "Reset server",
reset_server_desc: "Deletes all data and users of this instance. The app starts fresh afterwards (setup wizard). Enter your password and type RESET to confirm.",
reset_confirm_hint: "Type RESET to confirm",
reset_server_go: "Reset", reset_done: "Server reset.",
new_pw: "New password", pw_changed: "Password changed.",
users: "Users", setup_users: "Set up users",
users_solo_desc: "You are in single-user mode (no login). Create the first user to enable login and multiple users.",
@@ -262,7 +272,7 @@ const LANG = {
reorder_hint: "Drag to reorder exercises",
prev_week: "Previous week", next_week: "Next week", week_of: "Week of",
wizard_done: "All done - the app is ready.",
wizard_user_created: "User '{u}' created. Continue with 'Next'.",
wizard_user_created: "User '{u}' created.",
logged_in: "Logged in.", user_created_toast: "User created.",
app_load_fail: "Could not load the app: {m}",
},
@@ -1718,12 +1728,13 @@ function renderWizardStep() {
head.appendChild(el("h2", "auth-title", BRAND));
head.appendChild(wizardProgress());
box.appendChild(head);
const body = el("div", "wizard-body");
if (wizard.step === 0) {
// Schritt 0: Sprache (zuerst)
box.appendChild(el("h3", "wizard-title", t("lang_step")));
body.appendChild(el("h3", "wizard-title", t("lang_step")));
if (wizard.mode === "setup") {
box.appendChild(el("p", "auth-sub", t("lang_server")));
body.appendChild(el("p", "auth-sub", t("lang_server")));
}
const row = el("div", "day-picker");
for (const [code, label] of [["de", t("german")], ["en", t("english")]]) {
@@ -1736,10 +1747,10 @@ function renderWizardStep() {
});
row.appendChild(b);
}
box.appendChild(row);
body.appendChild(row);
} else if (wizard.step === 1 && wizard.mode === "setup") {
// Schritt 1: Einzel- oder Mehrbenutzermodus
box.appendChild(el("p", "auth-sub", t("welcome")));
body.appendChild(el("p", "auth-sub", t("welcome")));
const grid = el("div", "setup-options");
const multi = setupOption(t("multi_mode"), t("multi_desc"), wizard.multi === true);
multi.addEventListener("click", () => {
@@ -1748,17 +1759,18 @@ function renderWizardStep() {
});
grid.appendChild(multi);
const solo = setupOption(t("solo_mode"), t("solo_desc"), wizard.multi === false);
if (wizard.userCreated) solo.disabled = true;
solo.addEventListener("click", () => {
wizard.multi = false;
renderWizardStep();
});
grid.appendChild(solo);
box.appendChild(grid);
body.appendChild(grid);
if (wizard.multi === true) {
if (wizard.userCreated && app.me && app.me.username) {
const ok = el("div", "auth-register",
t("wizard_user_created", { u: app.me.username }));
box.appendChild(ok);
body.appendChild(ok);
} else {
const form = el("form", "auth-form");
form.appendChild(authField(t("username"), "setup-user"));
@@ -1773,14 +1785,14 @@ function renderWizardStep() {
ev.preventDefault();
wizardRegister($("#setup-user").value, $("#setup-pass").value, err);
});
box.appendChild(form);
body.appendChild(form);
}
}
} else {
// Letzter Schritt: Darstellung (Theme + OLED + Akzent)
box.appendChild(el("h3", "wizard-title", t("appearance")));
box.appendChild(el("p", "auth-sub", t("appearance_desc")));
box.appendChild(el("div", "picker-label", t("theme_label")));
body.appendChild(el("h3", "wizard-title", t("appearance")));
body.appendChild(el("p", "auth-sub", t("appearance_desc")));
body.appendChild(el("div", "picker-label", t("theme_label")));
const themeRow = el("div", "day-picker");
for (const [key, label] of [["dark", t("dark")], ["system", t("system")], ["light", t("light")]]) {
const b = el("button", "day-btn" + (wizard.theme === key ? " is-active" : ""), label);
@@ -1792,10 +1804,10 @@ function renderWizardStep() {
});
themeRow.appendChild(b);
}
box.appendChild(themeRow);
box.appendChild(wizardToggle("oled", t("oled_toggle"), t("oled_desc"), wizard.oled,
body.appendChild(themeRow);
body.appendChild(wizardToggle("oled", t("oled_toggle"), t("oled_desc"), wizard.oled,
previewWizardAppearance));
box.appendChild(el("div", "picker-label", t("accent_label")));
body.appendChild(el("div", "picker-label", t("accent_label")));
const accentRow = el("div", "accent-row");
for (const [key, color] of Object.entries(ACCENTS)) {
const sw = el("button", "accent-swatch" + (wizard.accent === key ? " is-active" : ""), "");
@@ -1809,9 +1821,11 @@ function renderWizardStep() {
});
accentRow.appendChild(sw);
}
box.appendChild(accentRow);
body.appendChild(accentRow);
}
box.appendChild(body);
// Fusszeile: Zurueck links, Weiter/Abschliessen rechts
const nav = el("div", "wizard-nav");
if (wizard.step > 0) {
@@ -1943,18 +1957,15 @@ async function doAuth(mode, username, password, errEl) {
}
}
function setupLogout() {
const btn = $("#logout-btn");
btn.addEventListener("click", async () => {
try {
await api("/api/logout", { method: "POST" });
} catch (_) {}
localStorage.removeItem("tp_token");
app.me = null;
app.models = {};
$("#view-today").innerHTML = "";
$("#today-label").textContent = "";
app.meta = await api("/api/meta");
function logout() {
api("/api/logout", { method: "POST" }).catch(() => {});
localStorage.removeItem("tp_token");
app.me = null;
app.models = {};
$("#view-today").innerHTML = "";
$("#today-label").textContent = "";
api("/api/meta").then(meta => {
app.meta = meta;
app.settings = { language: app.meta ? app.meta.language : "de" };
document.body.classList.add("no-auth");
$("#setup-gate").hidden = true;
@@ -1963,14 +1974,11 @@ function setupLogout() {
});
}
function setUserChip() {
const chip = $("#user-chip");
const btn = $("#logout-btn");
if (app.me && app.me.username) {
chip.hidden = false;
chip.textContent = app.me.username;
btn.hidden = false;
}
function logoutButton() {
const b = el("button", "btn btn-danger-ghost", t("logout_label"));
b.type = "button";
b.addEventListener("click", () => logout());
return b;
}
/* ---------------------------------------------------------------------------
@@ -2105,6 +2113,9 @@ function renderSettingsView() {
if (app.me && (!app.me.username || app.me.is_owner)) {
view.appendChild(renderUsersSection());
}
if (app.me && app.me.is_owner) {
view.appendChild(renderResetSection());
}
}
function settingsSelect(key, label, options) {
@@ -2149,6 +2160,9 @@ function renderAccountSection() {
row.appendChild(neu);
row.appendChild(btn);
body.appendChild(row);
const logoutRow = el("div", "form-row");
logoutRow.appendChild(logoutButton());
body.appendChild(logoutRow);
sec.appendChild(body);
return sec;
}
@@ -2263,6 +2277,7 @@ function renderOwnerUsersSection() {
}
};
reload();
body.appendChild(el("div", "divider"));
const row = el("div", "form-row");
const name = el("input", "file-input");
name.type = "text";
@@ -2291,6 +2306,37 @@ function renderOwnerUsersSection() {
return sec;
}
function renderResetSection() {
const sec = el("section", "data-section");
sec.appendChild(el("h3", "", t("reset_server")));
const body = el("div", "data-body");
body.appendChild(el("p", "", t("reset_server_desc")));
const row = el("div", "form-row");
const pw = el("input", "file-input");
pw.type = "password";
pw.placeholder = t("current_pw");
const conf = el("input", "file-input");
conf.type = "text";
conf.placeholder = t("reset_confirm_hint");
const btn = el("button", "btn btn-danger-ghost", t("reset_server_go"));
btn.type = "button";
btn.addEventListener("click", async () => {
try {
await api("/api/reset", jsonOpts("POST", {
password: pw.value, confirm: conf.value.trim(),
}));
toast(t("reset_done"), "accent");
await afterAuth();
} catch (e) { flashError(e); }
});
row.appendChild(pw);
row.appendChild(conf);
row.appendChild(btn);
body.appendChild(row);
sec.appendChild(body);
return sec;
}
function toggleRow(key, label, desc) {
const row = el("label", "toggle-row");
const cb = el("input", "toggle-input");
@@ -2314,7 +2360,6 @@ function setupUi() {
tab.addEventListener("click", () => switchView(tab.dataset.view));
}
setupOverlay();
setupLogout();
setupThemeWatch();
}
@@ -2334,6 +2379,20 @@ async function init() {
await loadAppData();
}
function updateTodayLabel() {
const label = $("#today-label");
if (!label) return;
const active = Object.values(app.models)
.find(m => m.id && m.date === app.meta.today);
if (active) {
const a = cfgActivity(active.activity);
const day = active.day ? " · " + active.day : "";
label.textContent = fmtDateLong(app.meta.today) + day + " " + t("session_active");
} else {
label.textContent = fmtDateLong(app.meta.today);
}
}
async function loadAppData() {
app.cfg = await api("/api/config");
app.settings = await api("/api/settings");
@@ -2342,8 +2401,7 @@ async function loadAppData() {
const key = "tab_" + tab.dataset.view;
if (LANG[lang()][key]) tab.textContent = t(key);
}
$("#today-label").textContent = t("today_header") + fmtDateLong(app.meta.today);
setUserChip();
updateTodayLabel();
await loadToday();
const startTab = app.settings.startTab || "today";
if (startTab !== "today") switchView(startTab);