revert(admin): Knopf zum Neuerzeugen der Vorschaubilder wieder raus

130 Zeilen fuer einen einmaligen Bedarf - die Haelfte davon eine Karte
im Admin-Bereich, die sonst dauerhaft dort steht. Die EXIF-Behebung
selbst bleibt: Neu hochgeladene Bilder stehen richtig herum.

Bestehende Vorschaubilder werden damit nicht mehr nachgezogen. Wer sie
gerade ruecken will, laedt die Datei erneut hoch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-08-25 14:15:00 +02:00
1 parent 4bd9a6ca56
commit 5b3658640e
6 files changed
+7 -166

No files matched your search

+7 -5
View File
@@ -1463,12 +1463,14 @@ Alle acht Fälle, beide Byte-Reihenfolgen (II und MM):
- Bei Drehung 1 entsteht keine Kopie des Bildes. - Bei Drehung 1 entsteht keine Kopie des Bildes.
Bestehende Vorschaubilder ändern sich davon **nicht** – sie liegen fertig im Bestehende Vorschaubilder ändern sich davon **nicht** – sie liegen fertig im
Speicher. Dafür neu: Admin → Audit → „Rebuild image previews". Speicher und werden nicht neu erzeugt. Ein Foto, das heute quer liegt, liegt
Gemessen: 6 Bilder, 6 neu erzeugt, 0 Fehler; das quer liegende Testbild war auch morgen quer, bis es erneut hochgeladen wird.
danach hochkant.
- [ ] Auf der produktiven Instanz einmal „Rebuild" drücken und mit echten Ein Knopf zum Neuerzeugen war kurz da und wurde wieder entfernt (130 Zeilen
Handy-Fotos gegenprüfen für einen einmaligen Bedarf).
- [ ] Auf der produktiven Instanz mit echten Handy-Fotos gegenprüfen –
**neu hochgeladene**, nicht die bestehenden
## Automatisiert geprüft (22.08.2026) ## Automatisiert geprüft (22.08.2026)
-3
View File
@@ -529,9 +529,6 @@ func main() {
r.Get("/admin/audit", adminHandler.ListAudit) r.Get("/admin/audit", adminHandler.ListAudit)
r.Get("/admin/audit/actions", adminHandler.ListAuditActions) r.Get("/admin/audit/actions", adminHandler.ListAuditActions)
r.Post("/admin/gc", adminHandler.RunGC) r.Post("/admin/gc", adminHandler.RunGC)
// Vorschaubilder neu erzeugen – nötig für alles, was vor der
// Auswertung der EXIF-Drehung hochgeladen wurde.
r.Post("/admin/thumbnails/rebuild", adminHandler.RebuildThumbnails)
// Immer erreichbar, auch wenn OCR aus ist – der Handler antwortet dann // Immer erreichbar, auch wenn OCR aus ist – der Handler antwortet dann
// mit enabled:false. Eine Route, die je nach Konfiguration 404 liefert, // mit enabled:false. Eine Route, die je nach Konfiguration 404 liefert,
// wäre für die Oberfläche nicht von einem Fehler zu unterscheiden. // wäre für die Oberfläche nicht von einem Fehler zu unterscheiden.
@@ -1,71 +0,0 @@
package admin
import (
"fmt"
"net/http"
"aether/internal/audit"
"aether/internal/thumbnails"
"aether/pkg/response"
)
/*
POST /api/v1/admin/thumbnails/rebuild – Vorschaubilder neu erzeugen.
Anlass: Bis heute wurde die EXIF-Drehung beim Erzeugen nicht ausgewertet.
Jedes hochkant aufgenommene Foto liegt im bestehenden Bestand deshalb quer,
während dasselbe Bild in der großen Ansicht aufrecht steht – der Browser
befolgt die Notiz von selbst, der Server tat es nicht.
Beim Hochladen entstehen Vorschaubilder von allein; für das, was schon da ist,
braucht es diesen einen Anstoß. Er läuft im Vordergrund und meldet Zahlen:
Ein Vorgang, der den halben Bestand neu schreibt, soll nicht stumm im
Hintergrund verschwinden.
*/
func (h *Handler) RebuildThumbnails(w http.ResponseWriter, r *http.Request) {
if _, ok := requireAdmin(r); !ok {
response.Error(w, http.StatusForbidden, "admin required")
return
}
list, err := h.files.ImageFiles(r.Context())
if err != nil {
response.Error(w, http.StatusInternalServerError, "could not list images")
return
}
var rebuilt, failed int
for _, f := range list {
if !thumbnails.SupportedMIME(f.MimeType) {
continue
}
rc, err := h.storage.Get(r.Context(), f.StorageKey)
if err != nil {
failed++
continue
}
err = thumbnails.Generate(r.Context(), h.storage, f.ID.String(), f.MimeType, rc)
rc.Close()
if err != nil {
failed++
continue
}
// Auch setzen, wenn es schon stand: Dateien, bei denen die Erzeugung
// beim Hochladen fehlschlug, bekommen so nachträglich eines.
if err := h.files.UpdateThumbnail(r.Context(), f.ID, true); err != nil {
failed++
continue
}
rebuilt++
}
h.logAdmin(r, audit.ActionGarbageCollect, "storage", "", "",
fmt.Sprintf("thumbnails rebuilt: %d of %d images, %d failed",
rebuilt, len(list), failed))
response.JSON(w, http.StatusOK, map[string]any{
"images": len(list),
"rebuilt": rebuilt,
"failed": failed,
})
}
-26
View File
@@ -533,29 +533,3 @@ func isUniqueViolation(err error) bool {
} }
return false return false
} }
// ImageFiles nennt alle nicht gelöschten Dateien, aus denen ein Vorschaubild
// entstehen kann – für das Neuerzeugen von Hand.
//
// Ohne Papierkorb: Für eine Datei, die man weggeworfen hat, lohnt keine Runde
// durch den Dekodierer.
func (s *Store) ImageFiles(ctx context.Context) ([]*File, error) {
rows, err := s.db.Query(ctx,
`SELECT `+fileCols+`
FROM files
WHERE is_deleted = false AND mime_type LIKE 'image/%'
ORDER BY created_at`)
if err != nil {
return nil, fmt.Errorf("query image files: %w", err)
}
defer rows.Close()
var out []*File
for rows.Next() {
var f File
if err := scanFile(rows, &f); err != nil {
return nil, fmt.Errorf("scan image file: %w", err)
}
out = append(out, &f)
}
return out, rows.Err()
}
-6
View File
@@ -830,12 +830,6 @@ export const adminApi = {
freed_bytes: number; skipped_young: number freed_bytes: number; skipped_young: number
}>('POST', '/admin/gc'), }>('POST', '/admin/gc'),
/** Vorschaubilder neu erzeugen – für alles, was vor der Auswertung der
* EXIF-Drehung hochgeladen wurde und deshalb quer liegt. */
rebuildThumbnails: () => request<{
images: number; rebuilt: number; failed: number
}>('POST', '/admin/thumbnails/rebuild'),
/** Stand der Texterkennung. `enabled: false` heißt: abgeschaltet oder die /** Stand der Texterkennung. `enabled: false` heißt: abgeschaltet oder die
* Programme fehlen im Image – kein Fehler, sondern eine Auskunft. */ * Programme fehlen im Image – kein Fehler, sondern eine Auskunft. */
ocrStatus: () => request<OcrStatus>('GET', '/admin/ocr'), ocrStatus: () => request<OcrStatus>('GET', '/admin/ocr'),
-55
View File
@@ -195,60 +195,6 @@ function StorageCleanup() {
) )
} }
/** Vorschaubilder entstehen beim Hochladen von selbst. Dieser Knopf ist für
* den Bestand: Bis heute wurde die EXIF-Drehung nicht ausgewertet, hochkant
* aufgenommene Fotos liegen als Vorschaubild deshalb quer. */
function RebuildThumbnails() {
const [running, setRunning] = useState(false)
const [result, setResult] = useState<string | null>(null)
async function run() {
setRunning(true)
setResult(null)
try {
const r = await adminApi.rebuildThumbnails()
setResult(
r.images === 0
? 'No images stored yet'
: `${r.rebuilt} of ${r.images} previews rebuilt` + (r.failed ? `, ${r.failed} failed` : ''),
)
} catch {
setResult('Rebuild failed')
} finally {
setRunning(false)
}
}
return (
<div style={{
display: 'flex', alignItems: 'center', gap: 12, marginBottom: 16,
padding: '12px 14px', borderRadius: 10,
border: '1px solid var(--color-border)', background: 'var(--color-surface-2)',
}}>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--color-text)' }}>
Rebuild image previews
</div>
<div style={{ fontSize: 11, color: 'var(--color-text-muted)', marginTop: 2 }}>
{result ?? 'Regenerates every stored preview. Needed once: photos taken in portrait used to end up sideways, because the rotation flag was ignored.'}
</div>
</div>
<button
onClick={run}
disabled={running}
style={{
padding: '7px 16px', borderRadius: 8, flexShrink: 0,
border: '1px solid var(--color-border)', background: 'var(--color-surface)',
color: 'var(--color-text)', fontSize: 12, fontWeight: 600,
cursor: running ? 'default' : 'pointer', opacity: running ? 0.6 : 1,
}}
>
{running ? 'Rebuilding…' : 'Rebuild'}
</button>
</div>
)
}
export default function AuditTab() { export default function AuditTab() {
const [entries, setEntries] = useState<AuditEntry[]>([]) const [entries, setEntries] = useState<AuditEntry[]>([])
const [total, setTotal] = useState(0) const [total, setTotal] = useState(0)
@@ -275,7 +221,6 @@ export default function AuditTab() {
<div> <div>
<IndexingStatus /> <IndexingStatus />
<StorageCleanup /> <StorageCleanup />
<RebuildThumbnails />
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 16 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 16 }}>
<select <select