Gleichnamige Hosts in verschiedenen Kundennetzen liessen sich in der Liste nicht auseinanderhalten. Schlagworte und Notizen gab es zwar schon, aber nur im Reiter "Notizen und Tags" - und damit nutzlos zum Unterscheiden. Jetzt stehen die Schlagworte als anklickbare Abzeichen in der Liste und oben im Host-Detail, es gibt einen Filter dafuer, eine Notiz zeigt ein Zettel-Symbol mit dem Text beim Ueberfahren, und beides wird mitdurchsucht - auch von der globalen Suche. Bewusst kein zweites Feld "Kommentar": Das haette dieselbe Angabe an zwei Orten gehalten. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
174 lines
6.0 KiB
TypeScript
174 lines
6.0 KiB
TypeScript
import { prisma } from "@/lib/db";
|
||
import { sichtbarerKunde } from "@/server/auth/sitzungspflicht";
|
||
|
||
/** Frist, nach der ein Host als „meldet sich nicht“ gilt (konfigurierbar ab #011). */
|
||
export const MELDUNG_FRIST_STUNDEN = 48;
|
||
|
||
export function meldungsGrenze(jetzt: Date = new Date()): Date {
|
||
return new Date(jetzt.getTime() - MELDUNG_FRIST_STUNDEN * 3_600_000);
|
||
}
|
||
|
||
export async function hostsOhneKunde() {
|
||
// Ein Kundenkonto hat hier nichts zu suchen: Nicht zugeordnete Hosts gehören per Definition
|
||
// noch niemandem, dürfen also auch niemandem gezeigt werden.
|
||
if (await sichtbarerKunde()) return [];
|
||
|
||
return prisma.host.findMany({
|
||
where: { kundeId: null },
|
||
orderBy: { erstmalsGesehenAm: "desc" },
|
||
});
|
||
}
|
||
|
||
export async function hostAnzahl() {
|
||
const kundeId = await sichtbarerKunde();
|
||
return prisma.host.count({ where: kundeId ? { kundeId } : {} });
|
||
}
|
||
|
||
/** Kennzahlen für Dashboard-Kacheln und Sidebar-Badges. */
|
||
export async function kennzahlen(jetzt: Date = new Date()) {
|
||
const in30Tagen = new Date(jetzt.getTime() + 30 * 86_400_000);
|
||
const kundeId = await sichtbarerKunde();
|
||
const hostSicht = kundeId ? { kundeId } : {};
|
||
const laufzeitSicht = kundeId ? { host: { kundeId } } : {};
|
||
|
||
const [gesamt, aktiv, ohneKunde, baldAblaufend, ohneMeldung, offeneAlerts] = await Promise.all([
|
||
prisma.host.count({ where: hostSicht }),
|
||
prisma.host.count({ where: { ...hostSicht, subStatus: "ACTIVE" } }),
|
||
kundeId ? Promise.resolve(0) : prisma.host.count({ where: { kundeId: null } }),
|
||
prisma.host.count({ where: { ...hostSicht, subNextduedate: { gte: jetzt, lte: in30Tagen } } }),
|
||
prisma.host.count({
|
||
where: {
|
||
...hostSicht,
|
||
OR: [{ letzterReportAm: null }, { letzterReportAm: { lt: meldungsGrenze(jetzt) } }],
|
||
},
|
||
}),
|
||
prisma.alert.count({ where: { erledigtAm: null, ...(kundeId ? { host: { kundeId } } : {}) } }),
|
||
]);
|
||
|
||
const [offeneEinkaufsrechnungen, fehlendeKundenrechnungen] = await Promise.all([
|
||
// Einkaufsrechnungen gehen ein Kundenkonto nichts an.
|
||
kundeId
|
||
? Promise.resolve(0)
|
||
: prisma.laufzeit.count({
|
||
where: { status: "BESTAETIGT", proxmoxPosition: { rechnung: { bezahlt: false } } },
|
||
}),
|
||
kundeId
|
||
? Promise.resolve(0)
|
||
: prisma.laufzeit.count({
|
||
where: {
|
||
...laufzeitSicht,
|
||
status: "BESTAETIGT",
|
||
proxmoxPosition: { isNot: null },
|
||
OR: [{ kundenPosition: null }, { kundenPosition: { rechnung: { erstellt: false } } }],
|
||
},
|
||
}),
|
||
]);
|
||
|
||
return {
|
||
hostsGesamt: gesamt,
|
||
aktivLizenziert: aktiv,
|
||
ohneSubscription: gesamt - aktiv,
|
||
ohneKunde,
|
||
baldAblaufend,
|
||
ohneMeldung,
|
||
offeneAlerts,
|
||
offeneEinkaufsrechnungen,
|
||
fehlendeKundenrechnungen,
|
||
offeneRechnungspunkte: offeneEinkaufsrechnungen + fehlendeKundenrechnungen,
|
||
};
|
||
}
|
||
|
||
export async function offeneAlerts(anzahl = 20) {
|
||
const kundeId = await sichtbarerKunde();
|
||
|
||
return prisma.alert.findMany({
|
||
where: { erledigtAm: null, ...(kundeId ? { host: { kundeId } } : {}) },
|
||
orderBy: [{ schwere: "asc" }, { geoeffnetAm: "desc" }],
|
||
take: anzahl,
|
||
});
|
||
}
|
||
|
||
/** Einträge für die globale Suche (Strg+K). */
|
||
export async function suchIndex() {
|
||
const kundeId = await sichtbarerKunde();
|
||
|
||
const [hosts, kunden, proxmoxRechnungen, kundenrechnungen] = await Promise.all([
|
||
prisma.host.findMany({
|
||
where: kundeId ? { kundeId } : {},
|
||
select: {
|
||
id: true,
|
||
hostname: true,
|
||
fqdn: true,
|
||
serverId: true,
|
||
subKey: true,
|
||
ips: true,
|
||
produkt: true,
|
||
notizen: true,
|
||
kunde: { select: { name: true } },
|
||
tags: { select: { name: true } },
|
||
},
|
||
}),
|
||
prisma.kunde.findMany({
|
||
where: kundeId ? { id: kundeId } : {},
|
||
select: { id: true, name: true, kundennummer: true, ansprechpartner: true, email: true },
|
||
}),
|
||
// Proxmox-Rechnungen sind Einkauf – für ein Kundenkonto gibt es sie schlicht nicht.
|
||
kundeId
|
||
? Promise.resolve([])
|
||
: prisma.proxmoxRechnung.findMany({
|
||
select: { id: true, nummer: true, positionen: { select: { laufzeit: { select: { hostId: true } } } } },
|
||
}),
|
||
prisma.kundenrechnung.findMany({
|
||
where: { nummer: { not: null }, ...(kundeId ? { kundeId } : {}) },
|
||
select: { id: true, nummer: true, kunde: { select: { id: true, name: true } } },
|
||
}),
|
||
]);
|
||
|
||
const eintraege = [
|
||
...hosts.map((host) => ({
|
||
typ: "Host" as const,
|
||
titel: host.hostname,
|
||
untertitel: `${host.produkt} · ${host.kunde?.name ?? "Nicht zugeordnet"}${
|
||
host.subKey ? ` · ${host.subKey}` : ""
|
||
}${host.tags.length > 0 ? ` · ${host.tags.map((tag) => tag.name).join(", ")}` : ""}`,
|
||
href: `/hosts/${host.id}`,
|
||
begriffe: [
|
||
host.hostname,
|
||
host.fqdn,
|
||
host.serverId,
|
||
host.subKey,
|
||
host.notizen,
|
||
...host.tags.map((tag) => tag.name),
|
||
...host.ips,
|
||
]
|
||
.filter((w): w is string => Boolean(w))
|
||
.map((w) => w.toLowerCase()),
|
||
})),
|
||
...kunden.map((kunde) => ({
|
||
typ: "Kunde" as const,
|
||
titel: kunde.name,
|
||
untertitel: `${kunde.kundennummer}${kunde.ansprechpartner ? ` · ${kunde.ansprechpartner}` : ""}`,
|
||
href: `/kunden/${kunde.id}`,
|
||
begriffe: [kunde.name, kunde.kundennummer, kunde.ansprechpartner, kunde.email]
|
||
.filter((w): w is string => Boolean(w))
|
||
.map((w) => w.toLowerCase()),
|
||
})),
|
||
...proxmoxRechnungen.map((rechnung) => ({
|
||
typ: "Rechnung" as const,
|
||
titel: rechnung.nummer,
|
||
untertitel: `Proxmox-Rechnung · ${rechnung.positionen.length} Position(en)`,
|
||
href: `/rechnungen?q=${encodeURIComponent(rechnung.nummer)}`,
|
||
begriffe: [rechnung.nummer.toLowerCase()],
|
||
})),
|
||
...kundenrechnungen.map((rechnung) => ({
|
||
typ: "Rechnung" as const,
|
||
titel: rechnung.nummer ?? "",
|
||
untertitel: `Kundenrechnung · ${rechnung.kunde.name}`,
|
||
href: `/kunden/${rechnung.kunde.id}`,
|
||
begriffe: [(rechnung.nummer ?? "").toLowerCase()],
|
||
})),
|
||
];
|
||
|
||
return eintraege;
|
||
}
|