Fotoupload im Personenformular ergänzen und Initialen-Platzhalter aussortieren

Fotofeld mit Dateiauswahl, Einfügen per Strg+V, Zwischenablage-Schaltfläche
und Drag & Drop; sichtbarer Foto-Button je Zeile. Foto wird beim Anlegen
oder Speichern übernommen bzw. entfernt.

Nextcloud meldet generierte Initialen-Avatare teils als eigene Profilbilder.
Der Import erkennt Bilder mit dominanter Hintergrundfarbe und lässt sie aus;
„Platzhalter-Fotos entfernen" bereinigt bereits importierte.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
ferdi2goandClaude Fable 5.1 committed 2026-09-16 09:57:54 +00:00
1 parent 8a0f903a40
commit 9ed59bcf56
6 files changed
+295 -7

No files matched your search

+26 -2
View File
@@ -4,6 +4,7 @@ import { randomUUID, timingSafeEqual } from 'node:crypto';
import { fileURLToPath } from 'node:url';
import { loadEnvFile } from './env.js';
import { createStore, imageMime } from './store.js';
import { looksLikePlaceholder } from './image.js';
import { workingDays, generateSchedule, isValidDate, countAssignments, addDays } from './scheduler.js';
import { germanHolidays, BUNDESLAENDER } from './holidays.js';
import { buildEventLines, buildCalendar, eventUid } from './ics.js';
@@ -178,6 +179,26 @@ app.delete('/api/persons/:id/photo', (req, res) => {
res.status(204).end();
});
/** Bereits gespeicherte Fotos, die wie Initialen-Platzhalter aussehen. */
function placeholderPhotoIds() {
return S().persons.filter((p) => { const buf = store.readPhoto(p.id); return buf && looksLikePlaceholder(buf); }).map((p) => p.id);
}
app.get('/api/persons/placeholder-photos', (req, res) => {
res.json({ ids: placeholderPhotoIds() });
});
app.post('/api/persons/placeholder-photos/remove', (req, res) => {
const ids = placeholderPhotoIds();
for (const id of ids) {
const p = S().persons.find((x) => x.id === id);
store.deletePhoto(id);
if (p) p.photoVersion = (p.photoVersion || 0) + 1;
}
store.save();
res.json({ removed: ids.length, persons: S().persons.map(publicPerson) });
});
app.get('/photos/:id', (req, res) => {
const id = String(req.params.id).replace(/[^a-f0-9-]/g, '');
const buf = store.readPhoto(id);
@@ -579,6 +600,7 @@ app.post('/api/nextcloud/contacts/import', async (req, res) => {
const created = [];
const photos = [];
const skipped = [];
let placeholders = 0;
for (const uid of uids) {
const c = cards.find((x) => x.uid === uid);
if (!c || (!c.firstName && !c.lastName)) { skipped.push(uid); continue; }
@@ -591,10 +613,12 @@ app.post('/api/nextcloud/contacts/import', async (req, res) => {
if (book.system) img = await client.fetchAvatar(uid).catch(() => null);
if (!img && c.photo?.kind === 'inline') { const buffer = Buffer.from(c.photo.base64, 'base64'); if (imageMime(buffer)) img = { buffer }; }
if (!img && c.photo?.kind === 'url') img = await client.fetchBinary(c.photo.url).catch(() => null);
if (img && imageMime(img.buffer)) { store.savePhoto(person.id, img.buffer); person.photoVersion = 1; photos.push(person.id); }
// Generierte Initialen-Avatare zählen nicht als Foto, auch wenn Nextcloud sie als „eigen" meldet
if (img && imageMime(img.buffer) && !looksLikePlaceholder(img.buffer)) { store.savePhoto(person.id, img.buffer); person.photoVersion = 1; photos.push(person.id); }
else if (img) placeholders += 1;
}
store.save();
res.json({ created: created.length, withPhoto: photos.length, skipped: skipped.length, persons: st.persons.map(publicPerson) });
res.json({ created: created.length, withPhoto: photos.length, placeholders, skipped: skipped.length, persons: st.persons.map(publicPerson) });
} catch (err) { ncError(res, err); }
});