generated from Dicken/dickendock
round30: alle drei Scanner als Hintergrund-Job, Dienst-Duplikate strukturell verhindert (Migration+Constraint+Geraete-Sperre), zweiter Erkennungsversuch bei leerem Ersttreffer
This commit is contained in:
@@ -145,6 +145,55 @@ export function ensureSchema(): void {
|
||||
ensureColumn("services", "last_suggested_display_name", "TEXT");
|
||||
ensureColumn("services", "last_suggested_category", "TEXT");
|
||||
ensureColumn("devices", "last_ping", "TEXT");
|
||||
|
||||
deduplicateServicesByDeviceAndPort();
|
||||
|
||||
// Verhindert künftig auf Datenbankebene, dass zwei Dienste mit demselben
|
||||
// (Gerät, Port) gleichzeitig existieren - kann sonst passieren, wenn zwei
|
||||
// Scans desselben Geräts überlappen (z. B. "Alle Geräte scannen" läuft
|
||||
// noch, während gleichzeitig auch "Jetzt scannen" für genau dieses eine
|
||||
// Gerät geklickt wird) und beide parallel einen "existiert noch nicht"-Fund
|
||||
// zu einem CREATE statt UPDATE machen. Erst NACH der Bereinigung oben
|
||||
// anlegen, sonst würde das Erstellen des Index an vorhandenen Duplikaten
|
||||
// scheitern.
|
||||
sqlite.exec(
|
||||
`CREATE UNIQUE INDEX IF NOT EXISTS idx_services_device_port ON services(device_id, port)`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt doppelte Dienst-Einträge für dasselbe (Gerät, Port) - kann bei
|
||||
* älteren Datenbanken vorkommen, wenn zwei überlappende Scans vor Einführung
|
||||
* des UNIQUE-Index (s. o.) beide gleichzeitig einen neuen Dienst angelegt
|
||||
* haben. Behält den Eintrag mit den meisten ausgefüllten Feldern (Favicon,
|
||||
* Kategorie, ...), bei Gleichstand den ältesten (kleinste created_at).
|
||||
*/
|
||||
function deduplicateServicesByDeviceAndPort(): void {
|
||||
const rows = sqlite
|
||||
.prepare(`SELECT * FROM services ORDER BY created_at ASC`)
|
||||
.all() as Array<Record<string, unknown>>;
|
||||
|
||||
const groups = new Map<string, Array<Record<string, unknown>>>();
|
||||
for (const row of rows) {
|
||||
const key = `${row.device_id}::${row.port}`;
|
||||
const list = groups.get(key) ?? [];
|
||||
list.push(row);
|
||||
groups.set(key, list);
|
||||
}
|
||||
|
||||
function richness(row: Record<string, unknown>): number {
|
||||
return ["favicon", "category", "description", "icon"].filter((f) => !!row[f]).length;
|
||||
}
|
||||
|
||||
for (const group of groups.values()) {
|
||||
if (group.length <= 1) continue;
|
||||
|
||||
const keeper = [...group].sort((a, b) => richness(b) - richness(a))[0];
|
||||
const toDelete = group.filter((r) => r.id !== keeper.id);
|
||||
for (const row of toDelete) {
|
||||
sqlite.prepare(`DELETE FROM services WHERE id = ?`).run(row.id as string);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ensureColumn(table: string, column: string, definition: string): void {
|
||||
|
||||
Reference in New Issue
Block a user