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
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:
1 parent
0ebaa8b132
commit
655316278a
6 files changed
+183
-52
No files matched your search
@@ -114,6 +114,11 @@ const LANG = {
|
|||||||
setting_saved: "Einstellung gespeichert.",
|
setting_saved: "Einstellung gespeichert.",
|
||||||
account: "Konto", account_desc: "Angemeldet als '{u}'. Kennwort ändern:",
|
account: "Konto", account_desc: "Angemeldet als '{u}'. Kennwort ändern:",
|
||||||
change_pw: "Kennwort ändern", current_pw: "Aktuelles Kennwort",
|
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.",
|
new_pw: "Neues Kennwort", pw_changed: "Kennwort geändert.",
|
||||||
users: "Benutzer", setup_users: "Benutzer einrichten",
|
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.",
|
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",
|
reorder_hint: "Übungen per Ziehen sortieren",
|
||||||
prev_week: "Vorige Woche", next_week: "Nächste Woche", week_of: "Woche vom",
|
prev_week: "Vorige Woche", next_week: "Nächste Woche", week_of: "Woche vom",
|
||||||
wizard_done: "Alles erledigt - die App ist bereit.",
|
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.",
|
logged_in: "Angemeldet.", user_created_toast: "Benutzer angelegt.",
|
||||||
app_load_fail: "App konnte nicht geladen werden: {m}",
|
app_load_fail: "App konnte nicht geladen werden: {m}",
|
||||||
},
|
},
|
||||||
@@ -224,6 +229,11 @@ const LANG = {
|
|||||||
setting_saved: "Setting saved.",
|
setting_saved: "Setting saved.",
|
||||||
account: "Account", account_desc: "Logged in as '{u}'. Change password:",
|
account: "Account", account_desc: "Logged in as '{u}'. Change password:",
|
||||||
change_pw: "Change password", current_pw: "Current 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.",
|
new_pw: "New password", pw_changed: "Password changed.",
|
||||||
users: "Users", setup_users: "Set up users",
|
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.",
|
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",
|
reorder_hint: "Drag to reorder exercises",
|
||||||
prev_week: "Previous week", next_week: "Next week", week_of: "Week of",
|
prev_week: "Previous week", next_week: "Next week", week_of: "Week of",
|
||||||
wizard_done: "All done - the app is ready.",
|
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.",
|
logged_in: "Logged in.", user_created_toast: "User created.",
|
||||||
app_load_fail: "Could not load the app: {m}",
|
app_load_fail: "Could not load the app: {m}",
|
||||||
},
|
},
|
||||||
@@ -1718,12 +1728,13 @@ function renderWizardStep() {
|
|||||||
head.appendChild(el("h2", "auth-title", BRAND));
|
head.appendChild(el("h2", "auth-title", BRAND));
|
||||||
head.appendChild(wizardProgress());
|
head.appendChild(wizardProgress());
|
||||||
box.appendChild(head);
|
box.appendChild(head);
|
||||||
|
const body = el("div", "wizard-body");
|
||||||
|
|
||||||
if (wizard.step === 0) {
|
if (wizard.step === 0) {
|
||||||
// Schritt 0: Sprache (zuerst)
|
// 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") {
|
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");
|
const row = el("div", "day-picker");
|
||||||
for (const [code, label] of [["de", t("german")], ["en", t("english")]]) {
|
for (const [code, label] of [["de", t("german")], ["en", t("english")]]) {
|
||||||
@@ -1736,10 +1747,10 @@ function renderWizardStep() {
|
|||||||
});
|
});
|
||||||
row.appendChild(b);
|
row.appendChild(b);
|
||||||
}
|
}
|
||||||
box.appendChild(row);
|
body.appendChild(row);
|
||||||
} else if (wizard.step === 1 && wizard.mode === "setup") {
|
} else if (wizard.step === 1 && wizard.mode === "setup") {
|
||||||
// Schritt 1: Einzel- oder Mehrbenutzermodus
|
// 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 grid = el("div", "setup-options");
|
||||||
const multi = setupOption(t("multi_mode"), t("multi_desc"), wizard.multi === true);
|
const multi = setupOption(t("multi_mode"), t("multi_desc"), wizard.multi === true);
|
||||||
multi.addEventListener("click", () => {
|
multi.addEventListener("click", () => {
|
||||||
@@ -1748,17 +1759,18 @@ function renderWizardStep() {
|
|||||||
});
|
});
|
||||||
grid.appendChild(multi);
|
grid.appendChild(multi);
|
||||||
const solo = setupOption(t("solo_mode"), t("solo_desc"), wizard.multi === false);
|
const solo = setupOption(t("solo_mode"), t("solo_desc"), wizard.multi === false);
|
||||||
|
if (wizard.userCreated) solo.disabled = true;
|
||||||
solo.addEventListener("click", () => {
|
solo.addEventListener("click", () => {
|
||||||
wizard.multi = false;
|
wizard.multi = false;
|
||||||
renderWizardStep();
|
renderWizardStep();
|
||||||
});
|
});
|
||||||
grid.appendChild(solo);
|
grid.appendChild(solo);
|
||||||
box.appendChild(grid);
|
body.appendChild(grid);
|
||||||
if (wizard.multi === true) {
|
if (wizard.multi === true) {
|
||||||
if (wizard.userCreated && app.me && app.me.username) {
|
if (wizard.userCreated && app.me && app.me.username) {
|
||||||
const ok = el("div", "auth-register",
|
const ok = el("div", "auth-register",
|
||||||
t("wizard_user_created", { u: app.me.username }));
|
t("wizard_user_created", { u: app.me.username }));
|
||||||
box.appendChild(ok);
|
body.appendChild(ok);
|
||||||
} else {
|
} else {
|
||||||
const form = el("form", "auth-form");
|
const form = el("form", "auth-form");
|
||||||
form.appendChild(authField(t("username"), "setup-user"));
|
form.appendChild(authField(t("username"), "setup-user"));
|
||||||
@@ -1773,14 +1785,14 @@ function renderWizardStep() {
|
|||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
wizardRegister($("#setup-user").value, $("#setup-pass").value, err);
|
wizardRegister($("#setup-user").value, $("#setup-pass").value, err);
|
||||||
});
|
});
|
||||||
box.appendChild(form);
|
body.appendChild(form);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Letzter Schritt: Darstellung (Theme + OLED + Akzent)
|
// Letzter Schritt: Darstellung (Theme + OLED + Akzent)
|
||||||
box.appendChild(el("h3", "wizard-title", t("appearance")));
|
body.appendChild(el("h3", "wizard-title", t("appearance")));
|
||||||
box.appendChild(el("p", "auth-sub", t("appearance_desc")));
|
body.appendChild(el("p", "auth-sub", t("appearance_desc")));
|
||||||
box.appendChild(el("div", "picker-label", t("theme_label")));
|
body.appendChild(el("div", "picker-label", t("theme_label")));
|
||||||
const themeRow = el("div", "day-picker");
|
const themeRow = el("div", "day-picker");
|
||||||
for (const [key, label] of [["dark", t("dark")], ["system", t("system")], ["light", t("light")]]) {
|
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);
|
const b = el("button", "day-btn" + (wizard.theme === key ? " is-active" : ""), label);
|
||||||
@@ -1792,10 +1804,10 @@ function renderWizardStep() {
|
|||||||
});
|
});
|
||||||
themeRow.appendChild(b);
|
themeRow.appendChild(b);
|
||||||
}
|
}
|
||||||
box.appendChild(themeRow);
|
body.appendChild(themeRow);
|
||||||
box.appendChild(wizardToggle("oled", t("oled_toggle"), t("oled_desc"), wizard.oled,
|
body.appendChild(wizardToggle("oled", t("oled_toggle"), t("oled_desc"), wizard.oled,
|
||||||
previewWizardAppearance));
|
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");
|
const accentRow = el("div", "accent-row");
|
||||||
for (const [key, color] of Object.entries(ACCENTS)) {
|
for (const [key, color] of Object.entries(ACCENTS)) {
|
||||||
const sw = el("button", "accent-swatch" + (wizard.accent === key ? " is-active" : ""), "");
|
const sw = el("button", "accent-swatch" + (wizard.accent === key ? " is-active" : ""), "");
|
||||||
@@ -1809,9 +1821,11 @@ function renderWizardStep() {
|
|||||||
});
|
});
|
||||||
accentRow.appendChild(sw);
|
accentRow.appendChild(sw);
|
||||||
}
|
}
|
||||||
box.appendChild(accentRow);
|
body.appendChild(accentRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
box.appendChild(body);
|
||||||
|
|
||||||
// Fusszeile: Zurueck links, Weiter/Abschliessen rechts
|
// Fusszeile: Zurueck links, Weiter/Abschliessen rechts
|
||||||
const nav = el("div", "wizard-nav");
|
const nav = el("div", "wizard-nav");
|
||||||
if (wizard.step > 0) {
|
if (wizard.step > 0) {
|
||||||
@@ -1943,18 +1957,15 @@ async function doAuth(mode, username, password, errEl) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupLogout() {
|
function logout() {
|
||||||
const btn = $("#logout-btn");
|
api("/api/logout", { method: "POST" }).catch(() => {});
|
||||||
btn.addEventListener("click", async () => {
|
localStorage.removeItem("tp_token");
|
||||||
try {
|
app.me = null;
|
||||||
await api("/api/logout", { method: "POST" });
|
app.models = {};
|
||||||
} catch (_) {}
|
$("#view-today").innerHTML = "";
|
||||||
localStorage.removeItem("tp_token");
|
$("#today-label").textContent = "";
|
||||||
app.me = null;
|
api("/api/meta").then(meta => {
|
||||||
app.models = {};
|
app.meta = meta;
|
||||||
$("#view-today").innerHTML = "";
|
|
||||||
$("#today-label").textContent = "";
|
|
||||||
app.meta = await api("/api/meta");
|
|
||||||
app.settings = { language: app.meta ? app.meta.language : "de" };
|
app.settings = { language: app.meta ? app.meta.language : "de" };
|
||||||
document.body.classList.add("no-auth");
|
document.body.classList.add("no-auth");
|
||||||
$("#setup-gate").hidden = true;
|
$("#setup-gate").hidden = true;
|
||||||
@@ -1963,14 +1974,11 @@ function setupLogout() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setUserChip() {
|
function logoutButton() {
|
||||||
const chip = $("#user-chip");
|
const b = el("button", "btn btn-danger-ghost", t("logout_label"));
|
||||||
const btn = $("#logout-btn");
|
b.type = "button";
|
||||||
if (app.me && app.me.username) {
|
b.addEventListener("click", () => logout());
|
||||||
chip.hidden = false;
|
return b;
|
||||||
chip.textContent = app.me.username;
|
|
||||||
btn.hidden = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------------------------------------------------------------------
|
/* ---------------------------------------------------------------------------
|
||||||
@@ -2105,6 +2113,9 @@ function renderSettingsView() {
|
|||||||
if (app.me && (!app.me.username || app.me.is_owner)) {
|
if (app.me && (!app.me.username || app.me.is_owner)) {
|
||||||
view.appendChild(renderUsersSection());
|
view.appendChild(renderUsersSection());
|
||||||
}
|
}
|
||||||
|
if (app.me && app.me.is_owner) {
|
||||||
|
view.appendChild(renderResetSection());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function settingsSelect(key, label, options) {
|
function settingsSelect(key, label, options) {
|
||||||
@@ -2149,6 +2160,9 @@ function renderAccountSection() {
|
|||||||
row.appendChild(neu);
|
row.appendChild(neu);
|
||||||
row.appendChild(btn);
|
row.appendChild(btn);
|
||||||
body.appendChild(row);
|
body.appendChild(row);
|
||||||
|
const logoutRow = el("div", "form-row");
|
||||||
|
logoutRow.appendChild(logoutButton());
|
||||||
|
body.appendChild(logoutRow);
|
||||||
sec.appendChild(body);
|
sec.appendChild(body);
|
||||||
return sec;
|
return sec;
|
||||||
}
|
}
|
||||||
@@ -2263,6 +2277,7 @@ function renderOwnerUsersSection() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
reload();
|
reload();
|
||||||
|
body.appendChild(el("div", "divider"));
|
||||||
const row = el("div", "form-row");
|
const row = el("div", "form-row");
|
||||||
const name = el("input", "file-input");
|
const name = el("input", "file-input");
|
||||||
name.type = "text";
|
name.type = "text";
|
||||||
@@ -2291,6 +2306,37 @@ function renderOwnerUsersSection() {
|
|||||||
return sec;
|
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) {
|
function toggleRow(key, label, desc) {
|
||||||
const row = el("label", "toggle-row");
|
const row = el("label", "toggle-row");
|
||||||
const cb = el("input", "toggle-input");
|
const cb = el("input", "toggle-input");
|
||||||
@@ -2314,7 +2360,6 @@ function setupUi() {
|
|||||||
tab.addEventListener("click", () => switchView(tab.dataset.view));
|
tab.addEventListener("click", () => switchView(tab.dataset.view));
|
||||||
}
|
}
|
||||||
setupOverlay();
|
setupOverlay();
|
||||||
setupLogout();
|
|
||||||
setupThemeWatch();
|
setupThemeWatch();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2334,6 +2379,20 @@ async function init() {
|
|||||||
await loadAppData();
|
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() {
|
async function loadAppData() {
|
||||||
app.cfg = await api("/api/config");
|
app.cfg = await api("/api/config");
|
||||||
app.settings = await api("/api/settings");
|
app.settings = await api("/api/settings");
|
||||||
@@ -2342,8 +2401,7 @@ async function loadAppData() {
|
|||||||
const key = "tab_" + tab.dataset.view;
|
const key = "tab_" + tab.dataset.view;
|
||||||
if (LANG[lang()][key]) tab.textContent = t(key);
|
if (LANG[lang()][key]) tab.textContent = t(key);
|
||||||
}
|
}
|
||||||
$("#today-label").textContent = t("today_header") + fmtDateLong(app.meta.today);
|
updateTodayLabel();
|
||||||
setUserChip();
|
|
||||||
await loadToday();
|
await loadToday();
|
||||||
const startTab = app.settings.startTab || "today";
|
const startTab = app.settings.startTab || "today";
|
||||||
if (startTab !== "today") switchView(startTab);
|
if (startTab !== "today") switchView(startTab);
|
||||||
|
|||||||
@@ -19,8 +19,6 @@
|
|||||||
<button class="tab" data-view="data" type="button">Daten</button>
|
<button class="tab" data-view="data" type="button">Daten</button>
|
||||||
<button class="tab" data-view="settings" type="button">Einstellungen</button>
|
<button class="tab" data-view="settings" type="button">Einstellungen</button>
|
||||||
</nav>
|
</nav>
|
||||||
<span class="user-chip" id="user-chip" hidden></span>
|
|
||||||
<button class="btn btn-small" id="logout-btn" hidden type="button">Abmelden</button>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
|
|||||||
@@ -770,6 +770,16 @@ def set_user_password(uid, new):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def reset_server():
|
||||||
|
"""Kompletter Reset: alle Sessions, Settings, Tokens und Benutzer loeschen."""
|
||||||
|
con = db()
|
||||||
|
for t in ("entries", "sessions", "settings", "tokens", "users"):
|
||||||
|
con.execute(f"DELETE FROM {t}")
|
||||||
|
con.execute("DELETE FROM sqlite_sequence")
|
||||||
|
con.commit()
|
||||||
|
con.close()
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Auto-Reset: letztes Workout einer Kategorie laden
|
# Auto-Reset: letztes Workout einer Kategorie laden
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -1415,6 +1425,8 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self._send(400, json.dumps(
|
self._send(400, json.dumps(
|
||||||
{"error": "Aktuelles Passwort ist falsch"}),
|
{"error": "Aktuelles Passwort ist falsch"}),
|
||||||
"application/json; charset=utf-8")
|
"application/json; charset=utf-8")
|
||||||
|
elif path in ("/api/reset", "/api/reset/"):
|
||||||
|
self.do_reset()
|
||||||
else:
|
else:
|
||||||
uid = self._require_user()
|
uid = self._require_user()
|
||||||
if uid is None:
|
if uid is None:
|
||||||
@@ -1450,6 +1462,33 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self._send(500, json.dumps({"error": str(exc)}),
|
self._send(500, json.dumps({"error": str(exc)}),
|
||||||
"application/json; charset=utf-8")
|
"application/json; charset=utf-8")
|
||||||
|
|
||||||
|
def do_reset(self):
|
||||||
|
uid = self._require_user()
|
||||||
|
if uid is None:
|
||||||
|
return
|
||||||
|
if uid != 1:
|
||||||
|
self._send(403, json.dumps({"error": "nur der erste Benutzer"}),
|
||||||
|
"application/json; charset=utf-8")
|
||||||
|
return
|
||||||
|
data = self._read_json()
|
||||||
|
con = db()
|
||||||
|
row = con.execute("SELECT * FROM users WHERE id=?",
|
||||||
|
(uid,)).fetchone()
|
||||||
|
con.close()
|
||||||
|
if not row or not _verify_pw(data.get("password", ""),
|
||||||
|
row["salt"], row["password_hash"]):
|
||||||
|
self._send(400, json.dumps(
|
||||||
|
{"error": "Aktuelles Passwort ist falsch"}),
|
||||||
|
"application/json; charset=utf-8")
|
||||||
|
return
|
||||||
|
if _clean_str(data.get("confirm")) != "RESET":
|
||||||
|
self._send(400, json.dumps(
|
||||||
|
{"error": "Bestätigungstext ist falsch"}),
|
||||||
|
"application/json; charset=utf-8")
|
||||||
|
return
|
||||||
|
reset_server()
|
||||||
|
self._send_json(200, {"ok": True})
|
||||||
|
|
||||||
def do_register(self):
|
def do_register(self):
|
||||||
if user_count() > 0:
|
if user_count() > 0:
|
||||||
self._send(403, json.dumps(
|
self._send(403, json.dumps(
|
||||||
|
|||||||
+22
-3
@@ -139,10 +139,10 @@ html, body {
|
|||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
border: 2px solid var(--border);
|
border: 2px solid var(--border);
|
||||||
overflow-x: auto;
|
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,7 +153,9 @@ html, body {
|
|||||||
|
|
||||||
@media (max-width: 700px) {
|
@media (max-width: 700px) {
|
||||||
.topbar { padding: 10px 12px; }
|
.topbar { padding: 10px 12px; }
|
||||||
.tab { padding: 6px 12px; }
|
.tabs { margin-left: 0; width: 100%; }
|
||||||
|
.tab { padding: 6px 10px; font-size: 13px; }
|
||||||
|
.brand-name { font-size: 15px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
@@ -945,7 +947,19 @@ body.boot #setup-gate { visibility: hidden; }
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
#setup-gate .auth-box { min-height: 560px; }
|
#setup-gate .auth-box {
|
||||||
|
height: 600px;
|
||||||
|
max-height: calc(100vh - 48px);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-body {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.auth-head {
|
.auth-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1099,6 +1113,11 @@ button:disabled {
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
border-top: 2px solid var(--border);
|
||||||
|
margin: 14px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.settings-select-row {
|
.settings-select-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -270,6 +270,25 @@ def main():
|
|||||||
st, done_a = req("GET", "/api/sessions?status=done", token=token_a)
|
st, done_a = req("GET", "/api/sessions?status=done", token=token_a)
|
||||||
check("A-Daten nach Loeschung von B intakt", len(done_a) == 1, len(done_a))
|
check("A-Daten nach Loeschung von B intakt", len(done_a) == 1, len(done_a))
|
||||||
|
|
||||||
|
# --- Server-Reset (doppelt gesichert) -----------------------------------
|
||||||
|
st, _ = req("POST", "/api/reset",
|
||||||
|
{"password": "falsch", "confirm": "RESET"}, token=token_a)
|
||||||
|
check("Reset: falsches Passwort -> 400", st == 400, st)
|
||||||
|
st, _ = req("POST", "/api/reset",
|
||||||
|
{"password": "neu123", "confirm": "NEIN"}, token=token_a)
|
||||||
|
check("Reset: falscher Bestaetigungstext -> 400", st == 400, st)
|
||||||
|
st, _ = req("POST", "/api/reset",
|
||||||
|
{"password": "neu123", "confirm": "RESET"}, token=token_a)
|
||||||
|
check("Reset: korrekt -> 200", st == 200, st)
|
||||||
|
st, meta = req("GET", "/api/meta")
|
||||||
|
check("Reset: offener Modus (kein Benutzer mehr)",
|
||||||
|
meta["users"] == 0 and meta["auth_required"] is False, meta)
|
||||||
|
st, me = req("GET", "/api/me", token=token_a)
|
||||||
|
check("Reset: im offenen Modus kein Benutzername",
|
||||||
|
st == 200 and me.get("username") is None, me)
|
||||||
|
st, _ = req("GET", "/api/sessions")
|
||||||
|
check("Reset: Sessions ohne Anmeldung wieder sichtbar (offen)", st == 200, st)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
proc.terminate()
|
proc.terminate()
|
||||||
proc.wait(timeout=5)
|
proc.wait(timeout=5)
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ check("Setup: Sprache kommt zuerst",
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (winNoToken.document.querySelectorAll(".act-card").length === 3
|
if (winNoToken.document.querySelectorAll(".act-card").length === 3
|
||||||
&& winNoToken.document.querySelector("#user-chip").textContent === "alex") {
|
&& winNoToken.document.querySelector("#logout-btn") === null) {
|
||||||
loggedIn = true;
|
loggedIn = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -174,10 +174,7 @@ check("Setup: Sprache kommt zuerst",
|
|||||||
}
|
}
|
||||||
check("Login: Onboarding abgeschlossen, Heute-Ansicht sichtbar",
|
check("Login: Onboarding abgeschlossen, Heute-Ansicht sichtbar",
|
||||||
ogate() && ogate().hidden
|
ogate() && ogate().hidden
|
||||||
&& winNoToken.document.querySelectorAll(".act-card").length === 3
|
&& winNoToken.document.querySelectorAll(".act-card").length === 3);
|
||||||
&& winNoToken.document.querySelector("#user-chip").textContent === "alex");
|
|
||||||
check("Login: Abmelden-Knopf sichtbar",
|
|
||||||
!winNoToken.document.querySelector("#logout-btn").hidden);
|
|
||||||
check("Login: kein Auth-Gate mehr",
|
check("Login: kein Auth-Gate mehr",
|
||||||
winNoToken.document.querySelector("#auth-gate").hidden);
|
winNoToken.document.querySelector("#auth-gate").hidden);
|
||||||
|
|
||||||
@@ -186,8 +183,7 @@ check("Setup: Sprache kommt zuerst",
|
|||||||
await sleep(1200);
|
await sleep(1200);
|
||||||
let tokOk = false;
|
let tokOk = false;
|
||||||
for (let i = 0; i < 50; i++) {
|
for (let i = 0; i < 50; i++) {
|
||||||
if (winTok.document.querySelectorAll(".act-card").length === 3
|
if (winTok.document.querySelectorAll(".act-card").length === 3) {
|
||||||
&& winTok.document.querySelector("#user-chip").textContent === "alex") {
|
|
||||||
tokOk = true;
|
tokOk = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -210,7 +206,9 @@ check("Setup: Sprache kommt zuerst",
|
|||||||
&& Array.from(winTok.document.querySelectorAll(".data-section"))
|
&& Array.from(winTok.document.querySelectorAll(".data-section"))
|
||||||
.some(s => s.querySelector("h3").textContent === "Konto"
|
.some(s => s.querySelector("h3").textContent === "Konto"
|
||||||
&& !!s.querySelector('input[placeholder="Aktuelles Kennwort"]')
|
&& !!s.querySelector('input[placeholder="Aktuelles Kennwort"]')
|
||||||
&& !!s.querySelector('input[placeholder="Neues Kennwort"]')));
|
&& !!s.querySelector('input[placeholder="Neues Kennwort"]')
|
||||||
|
&& Array.from(s.querySelectorAll(".btn"))
|
||||||
|
.some(b => b.textContent === "Abmelden")));
|
||||||
check("Einstellungen: nur ein Benutzer -> Hinweis statt Liste", await (async () => {
|
check("Einstellungen: nur ein Benutzer -> Hinweis statt Liste", await (async () => {
|
||||||
for (let i = 0; i < 40; i++) {
|
for (let i = 0; i < 40; i++) {
|
||||||
if (Array.from(winTok.document.querySelectorAll(".user-row"))
|
if (Array.from(winTok.document.querySelectorAll(".user-row"))
|
||||||
|
|||||||
Reference in new issue
Block a user