round22: Startseite kompakter, Ping-Zeitstempel-Bug behoben, echte Favicon-Deduplizierung per Bildinhalt, automatischer Favicon-Abgleich aus Icon-Datenbank + Nachruest-Button

This commit is contained in:
2026-07-23 20:58:58 +02:00
parent 6a66128172
commit b098007742
8 changed files with 181 additions and 49 deletions

View File

@@ -25,14 +25,17 @@ function mapRow(row: typeof devices.$inferSelect): Device {
}
/**
* Aktualisiert NUR online-Status + lastPing (Zeitstempel des Live-Status-
* Checks) - bewusst getrennt von jedem "echten" Scan-Upsert. Wird
* ausschließlich vom optionalen Ping-Heartbeat genutzt (siehe
* liveStatus.ts), niemals von einem Scan-Endpunkt.
* Aktualisiert online-Status + (nur bei online=true) lastPing. Ein
* fehlgeschlagener Ping ist keine "Sichtung" - lastPing soll ausschließlich
* den Zeitpunkt zeigen, an dem das Gerät zuletzt tatsächlich geantwortet
* hat, nicht wann zuletzt VERSUCHT wurde, es zu erreichen. Sonst würde
* "zuletzt gesehen" bei einem dauerhaft offline/entfernten Gerät trotzdem
* ständig auf "gerade eben" weiterrücken, nur weil der Heartbeat es
* regelmäßig (erfolglos) anpingt.
*/
export function updateDeviceOnlineStatus(id: string, online: boolean): void {
db.update(devices)
.set({ online, lastPing: nowIso() })
.set(online ? { online, lastPing: nowIso() } : { online })
.where(eq(devices.id, id))
.run();
}

View File

@@ -2,6 +2,7 @@ import type { FastifyInstance } from "fastify";
import { ServiceCreateSchema, ServiceReorderSchema, ServiceUpdateSchema } from "@launchpad/shared";
import * as deviceRepo from "../db/repositories/devices.js";
import * as serviceRepo from "../db/repositories/services.js";
import { findBestIconMatch } from "../scanner/iconDb.js";
interface ServiceListQuery {
deviceId?: string;
@@ -73,4 +74,24 @@ export async function serviceRoutes(app: FastifyInstance): Promise<void> {
}
return reply.code(204).send();
});
// Nachträgliches Ergänzen fehlender Favicons für bereits bestehende
// Dienste (z. B. von vor diesem Feature) - versucht für jeden Dienst OHNE
// Favicon einen passenden Treffer in der externen Icon-Datenbank zu
// finden (siehe scanner/iconDb.ts), anhand des aktuellen Anzeigenamens.
// Rein additiv: bestehende Favicons werden nie überschrieben, nur leere
// Felder aufgefüllt.
app.post("/api/services/backfill-favicons", async () => {
const services = serviceRepo.listServices();
let updated = 0;
for (const service of services) {
if (service.favicon) continue;
const match = await findBestIconMatch(service.displayName);
if (match) {
serviceRepo.updateService(service.id, { favicon: match });
updated++;
}
}
return { checked: services.length, updated };
});
}

View File

@@ -79,3 +79,32 @@ export async function searchIconDb(query: string, limit = 24): Promise<IconSearc
const matches = names.filter((n) => n.includes(q)).slice(0, limit);
return matches.map((name) => ({ name, url: `${CDN_BASE}/${name}.png` }));
}
/**
* Sucht den besten Treffer in der Icon-Datenbank für einen Dienst-/
* Software-Namen ("Plex", "Pi-hole", "Adguard Home") - für den Fall, dass
* der Scanner selbst kein Favicon gefunden hat (siehe networkScanner.ts).
* Bewusst konservativ: nur ein Treffer, wenn der normalisierte Name exakt
* (oder nach Entfernen von Bindestrichen) übereinstimmt - eine reine
* Teilstring-Suche würde bei kurzen/generischen Namen zu viele falsche
* Zuordnungen liefern (z. B. "nas" -> "nas4free" wäre noch plausibel, aber
* "web" könnte alles Mögliche treffen).
*/
export async function findBestIconMatch(name: string): Promise<string | null> {
const normalized = name
.toLowerCase()
.trim()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
if (normalized.length < 2) return null;
const names = await getIconNames();
const nameSet = new Set(names);
if (nameSet.has(normalized)) return `${CDN_BASE}/${normalized}.png`;
const collapsed = normalized.replace(/-/g, "");
const exactCollapsed = names.find((n) => n.replace(/-/g, "") === collapsed);
if (exactCollapsed) return `${CDN_BASE}/${exactCollapsed}.png`;
return null;
}

View File

@@ -2,6 +2,7 @@ import { resolveHostname, reverseLookup } from "./dns.js";
import { isPortOpen, TYPICAL_PORTS } from "./ports.js";
import { probeHttp } from "./http.js";
import { detectSoftware } from "./softwareDetection.js";
import { findBestIconMatch } from "./iconDb.js";
export interface ScanTarget {
hostname: string;
@@ -151,5 +152,23 @@ export async function scanDeviceServices(
}
}
// Bleibt danach IMMER NOCH kein Favicon übrig (kein bestätigtes eigenes,
// kein Sibling), zuletzt versuchen, per erkanntem Software-/Anzeigenamen
// ein passendes Icon aus der externen Icon-Datenbank zu finden (siehe
// iconDb.ts). Nur bei eindeutigem Treffer - lieber gar kein Favicon als
// ein falsch zugeordnetes.
for (const service of found) {
if (service.favicon) continue;
const nameToMatch = service.category ? service.suggestedDisplayName : null;
if (!nameToMatch) continue;
try {
const match = await findBestIconMatch(nameToMatch);
if (match) service.favicon = match;
} catch {
// Icon-Datenbank nicht erreichbar (kein Internetzugang o. ä.) - der
// Scan soll deswegen nicht fehlschlagen, einfach ohne Favicon weiter.
}
}
return { services: found, suggestedHostname };
}