CI / pruefen (pull_request) Canceled after 0s
Zwanzig feste Abzeichen plus dynamische fuer vollstaendig gesehene Filmreihen und abgeschlossene Universen. Sie tauchen auf, wenn etwas erreicht ist, und bleiben — auch wenn die Bedingung spaeter wegfaellt. Eine zurueckgenommene Sichtung soll kein Abzeichen kosten. Alle Regeln arbeiten auf *einem* Datensatz, der einmal geholt wird. Sonst stellte jede Regel ihre eigene Abfrage, und aus zwanzig Abzeichen wuerden zwanzig Abfragen bei jedem Abhaken. Ausgewertet wird nach Sichtungsaktionen und einmal taeglich; der erste Lauf eines Kontos ist stumm, weil eine gewachsene Bibliothek sonst zwei Dutzend Meldungen auf einmal ausloest. Fuer Filmreihen braucht es keine gepflegte Liste: TMDB gruppiert sie selbst, und tmdbCollectionId liegt beim Import schon am Titel. Eigennamen wie "Spider Bro" kommen aus einer kleinen Datendatei obendrauf; fehlt ein Eintrag, greift der generische Name. Die kuratierten Eintraege tragen jeweils ein Stueck des erwarteten TMDB-Namens. Ich habe die Collection-Kennungen aus dem Gedaechtnis geschrieben und kann sie hier nicht gegen TMDB pruefen — passt der Name nicht, wird der Eintrag ignoriert und der generische greift. Eine falsch abgeschriebene Kennung bleibt damit folgenlos statt jemandem das falsche Abzeichen zu geben. Die Uhrzeit-Regel liest die Stunde ueber formatToParts statt ueber die formatierte Zeichenkette: auf Deutsch liefert Intl fuer eine einzelne Stunde "03 Uhr", und daraus wird beim Umwandeln NaN. Ein Test hat das aufgedeckt. Beim ersten Oeffnen der Uebersicht wird einmal ausgewertet — sonst bliebe sie bei einem Bestandskonto leer, bis nachts der Hintergrundlauf kommt. Zehn Tests, Anleitung mit Screenshot. Symbole als eigene SVG auf einem gemeinsamen Raster; erreicht und offen unterscheiden sich nicht nur durch Farbe, sondern auch im Text. Refs #15
103 lines
3.6 KiB
TypeScript
103 lines
3.6 KiB
TypeScript
import type { Metadata } from "next";
|
|
import { requireUser } from "@/lib/auth";
|
|
import { getSettings } from "@/lib/settings";
|
|
import { prisma } from "@/lib/db";
|
|
import { PasswordForm, ProfileForm } from "@/components/settings/Forms";
|
|
import { PasskeyList } from "@/components/settings/Passkeys";
|
|
import { listPasskeys } from "@/lib/passkeys";
|
|
import { MediaServerSettings } from "@/components/settings/MediaServer";
|
|
import { listConnections } from "@/lib/mediaserver";
|
|
import { env } from "@/lib/env";
|
|
import pkg from "../../../package.json";
|
|
|
|
export const metadata: Metadata = { title: "Einstellungen" };
|
|
|
|
export default async function SettingsPage() {
|
|
const session = await requireUser("/einstellungen");
|
|
const settings = await getSettings();
|
|
|
|
const user = await prisma.user.findUniqueOrThrow({
|
|
where: { id: session.id },
|
|
select: {
|
|
name: true,
|
|
email: true,
|
|
region: true,
|
|
publicProfile: true,
|
|
profileSlug: true,
|
|
showAchievements: true,
|
|
notifyNewEpisode: true,
|
|
notifyRelease: true,
|
|
notifyAvailability: true,
|
|
passwordHash: true,
|
|
},
|
|
});
|
|
|
|
const { passwordHash, ...profile } = user;
|
|
|
|
const passkeys = env.passkeys.enabled ? await listPasskeys(session.id) : [];
|
|
const medienserver = await listConnections(session.id);
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<section className="fl-card p-5">
|
|
<h2 className="mb-4 text-lg font-semibold">Profil</h2>
|
|
<ProfileForm user={profile} publicProfilesEnabled={settings.publicProfilesEnabled} />
|
|
</section>
|
|
|
|
{passwordHash && (
|
|
<section className="fl-card p-5">
|
|
<h2 className="mb-4 text-lg font-semibold">Passwort</h2>
|
|
<PasswordForm />
|
|
</section>
|
|
)}
|
|
|
|
{env.passkeys.enabled && (
|
|
<section className="fl-card p-5">
|
|
<h2 className="mb-1 text-lg font-semibold">Passkeys</h2>
|
|
<p className="mb-4 text-sm text-[var(--fl-muted)]">
|
|
Anmelden ohne Passwort — per Fingerabdruck, Gesicht oder Sicherheitsschlüssel.
|
|
</p>
|
|
<PasskeyList
|
|
passkeys={passkeys.map((passkey) => ({
|
|
...passkey,
|
|
createdAt: passkey.createdAt.toISOString(),
|
|
lastUsedAt: passkey.lastUsedAt?.toISOString() ?? null,
|
|
}))}
|
|
available={env.passkeys.isAvailable}
|
|
hinweis={
|
|
env.passkeys.isAvailable
|
|
? null
|
|
: "Passkeys brauchen eine verschlüsselte Verbindung. Diese Instanz läuft über " +
|
|
"einfaches HTTP — richte HTTPS ein, dann erscheint hier die Verwaltung."
|
|
}
|
|
/>
|
|
</section>
|
|
)}
|
|
|
|
<section className="fl-card p-5">
|
|
<h2 className="mb-1 text-lg font-semibold">Medienserver</h2>
|
|
<p className="mb-4 text-sm text-[var(--fl-muted)]">
|
|
Bestand und Sichtungen aus Jellyfin übernehmen. Der Abgleich ergänzt nur — er löscht
|
|
nie etwas, das in Flimmer steht.
|
|
</p>
|
|
<MediaServerSettings
|
|
verbindungen={medienserver.map((verbindung) => ({
|
|
id: verbindung.id,
|
|
name: verbindung.name,
|
|
baseUrl: verbindung.baseUrl,
|
|
serverUserName: verbindung.serverUserName,
|
|
enabled: verbindung.enabled,
|
|
syncOwned: verbindung.syncOwned,
|
|
syncWatched: verbindung.syncWatched,
|
|
lastSyncAt: verbindung.lastSyncAt?.toISOString() ?? null,
|
|
}))}
|
|
/>
|
|
</section>
|
|
|
|
<p className="text-xs text-[var(--fl-faint)]">
|
|
Flimmer {pkg.version} · Instanz {settings.instanceName}
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|