Erste Produktivversion

This commit is contained in:
2026-08-08 20:31:58 +02:00
parent d8ded2816d
commit 7b21e2d1b4
110 changed files with 18170 additions and 644 deletions

View File

@@ -0,0 +1,233 @@
// Einstellungen des angemeldeten Kontos: Anzeigename, Passwort,
// Benachrichtigungen.
"use strict";
import { del, patch, post } from "../api.js";
import { el, mount } from "../dom.js";
import * as push from "../push.js";
import { set, state } from "../store.js";
function formatDate(iso) {
return new Date(iso).toLocaleDateString("de-DE", {
day: "2-digit", month: "2-digit", year: "numeric",
});
}
export async function settingsView(root, { back, admin }) {
let banner = null;
let pushStatus = null;
let devices = [];
async function reload() {
pushStatus = await push.status();
devices = pushStatus.serverEnabled
? await push.listDevices().catch(() => [])
: [];
}
function say(message, kind = "notice") {
banner = { message, kind };
render();
setTimeout(() => { banner = null; render(); }, 8000);
}
async function guarded(fn) {
try {
await fn();
await reload();
render();
} catch (err) {
say(err.message, "error");
}
}
// -------------------------------------------------------------------
// Benachrichtigungen
// -------------------------------------------------------------------
function pushSection() {
if (!pushStatus.serverEnabled) {
return el("section.card", {},
el("h2", {}, "Benachrichtigungen"),
el("p.lead", {},
"Auf diesem Server sind Push-Benachrichtigungen nicht eingerichtet. " +
"Dafür wird ein VAPID-Schlüsselpaar in der Konfiguration benötigt."));
}
// Auf iOS gibt es Push nur in der installierten App - das ist eine
// Einschränkung von Apple, kein Fehler der Anwendung.
if (pushStatus.needsInstall) {
return el("section.card", {},
el("h2", {}, "Benachrichtigungen"),
el("p.lead.warn", {},
"Auf iPhone und iPad funktionieren Benachrichtigungen nur, wenn die " +
"App über „Teilen → Zum Home-Bildschirm“ installiert wurde. " +
"Öffne sie danach über das Symbol auf dem Startbildschirm und " +
"komm hierher zurück."));
}
if (!pushStatus.supported) {
return el("section.card", {},
el("h2", {}, "Benachrichtigungen"),
el("p.lead", {},
"Dieser Browser unterstützt keine Push-Benachrichtigungen. " +
"Über eine unverschlüsselte Verbindung sind sie ebenfalls nicht " +
"möglich dann hilft der Zugriff über HTTPS."));
}
const blocked = pushStatus.permission === "denied";
return el("section.card", {},
el("h2", {}, "Benachrichtigungen"),
el("p.lead", {},
"Wenn jemand eine geteilte Liste ändert, bekommst du eine Meldung ",
el("strong", {}, `höchstens eine je Liste alle ${pushStatus.throttleHours} Stunden`),
". Wer im Laden steht und abhakt, erzeugt sonst im Minutentakt " +
"Benachrichtigungen."),
blocked
? el("p.lead.warn", {},
"Benachrichtigungen wurden für diese Seite abgelehnt. Das lässt " +
"sich nur in den Einstellungen des Browsers wieder ändern " +
"meist über das Schloss- oder Info-Symbol in der Adresszeile.")
: null,
pushStatus.subscribed
? el("div.menu-actions", {},
el("button", {
type: "button",
onclick: () => guarded(async () => {
await push.sendTest();
say("Testnachricht unterwegs. Kommt sie nicht an, steht der " +
"Grund im Log des api-Containers.");
}),
}, "Testnachricht senden"),
el("button.danger", {
type: "button",
onclick: () => guarded(async () => {
await push.unsubscribe();
say("Dieses Gerät bekommt keine Benachrichtigungen mehr.");
}),
}, "Auf diesem Gerät abschalten"))
: el("button.primary", {
type: "button",
disabled: blocked,
onclick: () => guarded(async () => {
const result = await push.subscribe();
if (!result.ok) {
say(result.reason, "error");
return;
}
say("Benachrichtigungen für dieses Gerät eingeschaltet.");
}),
}, "Auf diesem Gerät einschalten"),
devices.length
? el("div", {},
el("h3.device-heading", {}, "Angemeldete Geräte"),
el("ul.share-list", {}, devices.map((device) =>
el("li", {},
el("div.who-block", {},
el("span.name", {}, device.label || "Unbenanntes Gerät"),
el("span.sub", {},
`seit ${formatDate(device.created_at)}`,
device.last_success_at
? ` · zuletzt erreicht ${formatDate(device.last_success_at)}`
: " · noch nichts zugestellt")),
el("div.member-actions", {},
el("button.danger", {
type: "button",
onclick: () => guarded(() =>
del(`/api/push/subscriptions/${device.id}`)),
}, "Entfernen"))))))
: null
);
}
// -------------------------------------------------------------------
// Profil und Passwort
// -------------------------------------------------------------------
function profileSection() {
const nameField = el("input", {
type: "text",
value: state.user.display_name || "",
maxLength: 80,
placeholder: "wird anderen Mitgliedern angezeigt",
});
return el("section.card", {},
el("h2", {}, "Profil"),
el("p.lead", {}, state.user.email),
el("label", {}, "Anzeigename ", el("span.hint", {}, "(freiwillig)")),
el("div.row", {}, nameField,
el("button.primary", {
type: "button",
onclick: () => guarded(async () => {
const user = await patch("/api/auth/me", {
display_name: nameField.value.trim() || null,
});
set({ user });
say("Gespeichert.");
}),
}, "Speichern"))
);
}
function passwordSection() {
const current = el("input", { type: "password", autocomplete: "current-password" });
const next = el("input", { type: "password", autocomplete: "new-password" });
const repeat = el("input", { type: "password", autocomplete: "new-password" });
return el("section.card", {},
el("h2", {}, "Passwort ändern"),
el("p.lead", {},
"Nach der Änderung werden alle anderen Geräte abgemeldet."),
el("label", {}, "Bisheriges Passwort"), current,
el("label", {}, "Neues Passwort ", el("span.hint", {}, "(mindestens 12 Zeichen)")), next,
el("label", {}, "Wiederholen"), repeat,
el("button.primary", {
type: "button",
onclick: () => guarded(async () => {
if (next.value !== repeat.value) {
throw new Error("Die beiden Eingaben stimmen nicht überein.");
}
const result = await post("/api/auth/password/change", {
current_password: current.value,
new_password: next.value,
});
current.value = next.value = repeat.value = "";
say(result.detail);
}),
}, "Passwort ändern")
);
}
function render() {
mount(root,
el("header.bar", {},
el("button.linklike", { type: "button", onclick: back }, " Zurück"),
el("span.who", {}, state.user.display_name || state.user.email)),
banner ? el(banner.kind === "error" ? "p.error" : "p.notice", {}, banner.message) : null,
el("section.card", {},
el("h1", {}, "Einstellungen"),
// Nur für Administratoren sichtbar - der Endpunkt prüft
// zusätzlich, die Oberfläche allein wäre kein Schutz.
state.user.is_admin
? el("div", {},
el("p.lead", {}, "Du hast Administratorrechte."),
el("button.secondary", { type: "button", onclick: admin },
"Benutzerverwaltung"))
: null),
pushSection(),
profileSection(),
passwordSection()
);
}
await reload();
render();
return render;
}