feat: Sicherung, Anmeldebremse, Vorschaubilder -- und Schritt 5

SICHERUNG (tools/sichern.ps1, taeglich 04:30 als geplante Aufgabe).
Die Datenbank wird nicht kopiert, sondern ueber SQLites eigene
Sicherungsschnittstelle herausgeholt (server/src/backup.ts): im WAL-Modus
liegt das Zuletzte noch nicht in media.db, und selbst alle drei Dateien zu
kopieren ist nicht sicher, wenn waehrenddessen geschrieben wird. Die Bilder
kommen aus einem NUR LESEND eingehaengten Volume dazu.

Und sie prueft sich selbst: auspacken, Datenbank oeffnen, Medien/Token/
Benutzer zaehlen, mit dem laufenden Dienst vergleichen, sonst Fehler. Einmal
wirklich zurueckgespielt -- leeres Volume, zweiter Dienst, Anmeldung mit dem
echten Passwort, Bild abgerufen, Byte fuer Byte identisch. Eine Sicherung, die
nie zurueckgespielt wurde, ist eine Hoffnung.

ANMELDEBREMSE. Das Formular steht oeffentlich, und scrypt macht einen Versuch
teuer -- aber teuer ist nicht selten. Fuenf freie Versuche je Adresse, dann
Sperre ab 30 s mit Verdopplung bis 15 min, 429 samt Retry-After und einem Text,
der sagt wie lange. Erfolg setzt zurueck. Nur im Speicher: wer sich aussperrt,
startet den Container neu. Durchgemessen bis zur Erholung.

VORSCHAUBILDER. sharp erzeugt beim Upload eine 320er WebP-Fassung unter
/data/thumbs, ausgeliefert unter /t/<pfad>. Gemessen: 131502 -> 13078 Bytes,
Faktor 10; eine Galerieseite faellt von 7,5 MB auf 766 KB. KEINE Spalte in der
Datenbank -- ob es eine Vorschau gibt, sagt das Dateisystem, und die Galerie
faellt bei 404 aufs Vollbild zurueck. Eine zweite Wahrheit, die auseinander-
laufen kann, gibt es damit gar nicht erst. Ein Knopf zieht Fehlendes nach und
nennt Zahlen statt "fertig".

SCHRITT 5. Die Fivemanage-Dateien liegen in legacy/ mit Erklaerung; unsere
docker-compose.media.yml heisst jetzt docker-compose.yml. Drei Volumes und drei
Abbilder geloescht, rund 670 MB -- nachgezaehlt war vorher, dass kein Bild
darin lag.

Nebenbefund: `npm i sharp` scheitert auf diesem Rechner nicht an sharp, sondern
an better-sqlite3 -- der Host laeuft auf Node 24 (ABI 137), node-gyp uebernimmt
und findet keine Bauwerkzeuge. Genau die Falle aus dem Dockerfile. Eingetragen
wurde mit --package-lock-only, gebaut wird im Abbild auf Node 22.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 18:00:14 +02:00
co-authored by Claude Opus 5
parent 6c8ead8e15
commit aa2575bdf4
18 changed files with 1450 additions and 229 deletions
+15 -2
View File
@@ -12,7 +12,7 @@ import {
Trash2,
X,
} from 'lucide-react'
import { api, type MediaItem, type MediaPage } from '../api'
import { api, vorschauUrl, type MediaItem, type MediaPage } from '../api'
import { run, useStore } from '../store'
import { bytes, istBild, zeit } from '../format'
import { Knopf, Leer, Nachfrage, useKopieren } from '../components/Bausteine'
@@ -213,7 +213,20 @@ export function Galerie() {
<div className="flex aspect-square items-center justify-center bg-grund">
{istBild(item.mime) ? (
<img
src={item.url}
// Erst die Vorschau: 60 Vollbilder
// sind 7,2 MB, nur damit jemand
// sieht, WELCHE Bilder da sind.
// Fehlt sie (404), nimmt der
// Browser das Vollbild — deshalb
// braucht es dafuer weder eine
// Spalte noch eine Nachfrage.
src={vorschauUrl(item.path)}
onError={(e) => {
const bild = e.currentTarget
if (bild.dataset.gefallen) return
bild.dataset.gefallen = 'ja'
bild.src = item.url
}}
alt={item.path}
loading="lazy"
className="h-full w-full object-contain"
+35 -3
View File
@@ -1,14 +1,15 @@
import { useEffect, useState } from 'react'
import { Check, Copy, Folder, HardDrive, KeyRound, Loader2 } from 'lucide-react'
import { Check, Copy, Folder, HardDrive, Images, KeyRound, Loader2 } from 'lucide-react'
import { api, type Stats } from '../api'
import { useStore } from '../store'
import { run, useStore } from '../store'
import { bytes } from '../format'
import { Karte, Leer, useKopieren } from '../components/Bausteine'
import { Karte, Knopf, Leer, useKopieren } from '../components/Bausteine'
export function Speicher() {
const note = useStore((s) => s.note)
const [stats, setStats] = useState<Stats | null>(null)
const [laedt, setLaedt] = useState(true)
const [zieht, setZieht] = useState(false)
const { kopiert, kopieren } = useKopieren()
useEffect(() => {
@@ -69,6 +70,37 @@ export function Speicher() {
</p>
</Karte>
<Karte titel="Wartung">
<div className="flex flex-wrap items-center gap-3">
<Knopf
wartet={zieht}
onClick={async () => {
setZieht(true)
await run(
() => api.thumbsNachziehen(),
(r) =>
r.erzeugt === 0
? `Nichts zu tun — ${r.vorhanden} Vorschauen sind schon da.`
: `${r.erzeugt} Vorschauen erzeugt` +
(r.fehler ? `, ${r.fehler} fehlgeschlagen` : '') +
`. ${r.vorhanden} waren schon da.`,
)
setZieht(false)
}}
kind={
<>
<Images size={15} />
Fehlende Vorschaubilder erzeugen
</>
}
/>
<p className="text-xs text-schrift-leise">
Die Galerie lädt Vorschauen statt Vollbilder. Was vor dieser Funktion
hochgeladen wurde, hat noch keine.
</p>
</div>
</Karte>
<Karte titel="Größte Ordner">
{stats.folders.length === 0 ? (
<Leer text="Noch nichts abgelegt." />