diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 5054736..e9e2f1e 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -19,7 +19,7 @@ export async function generateMetadata(): Promise { export default async function AdminPage() { const admin = await requireAdminPage(); - const [settings, users, eligible] = await Promise.all([ + const [settings, users, eligible, staleListeners] = await Promise.all([ getAppSettings(), prisma.user.findMany({ select: userSafeSelect, @@ -36,6 +36,9 @@ export default async function AdminPage() { ], }, }), + prisma.notificationTarget.count({ + where: { isActive: true, events: { has: "PROJECT_STALE" } }, + }), ]); const data: AdminData = { @@ -54,6 +57,11 @@ export default async function AdminPage() { lastReport: (settings.lastAutoSyncReport as AutoSyncReport | null) ?? null, eligible, }, + stale: { + afterDays: settings.staleAfterDays, + lastCheckAt: settings.lastStaleCheckAt, + listeners: staleListeners, + }, }; return ( diff --git a/src/app/api/admin/settings/route.ts b/src/app/api/admin/settings/route.ts index 72a707d..a1c3ecd 100644 --- a/src/app/api/admin/settings/route.ts +++ b/src/app/api/admin/settings/route.ts @@ -37,6 +37,7 @@ export async function PATCH(request: Request) { autoSyncIntervalMinutes, autoSyncActiveOnly, autoSyncBatchSize, + staleAfterDays, } = parsed.data; // Der Wechsel in den Einzelbetrieb wuerde weitere Konten aussperren – @@ -57,6 +58,11 @@ export async function PATCH(request: Request) { ...(autoSyncIntervalMinutes !== undefined ? { autoSyncIntervalMinutes } : {}), ...(autoSyncActiveOnly !== undefined ? { autoSyncActiveOnly } : {}), ...(autoSyncBatchSize !== undefined ? { autoSyncBatchSize } : {}), + // Neue Schwelle: die Pruefung soll beim naechsten Tick greifen, nicht + // erst in 24 Stunden. + ...(staleAfterDays !== undefined + ? { staleAfterDays, lastStaleCheckAt: null } + : {}), }; const settings = await prisma.appSettings.upsert({ diff --git a/src/components/admin/AdminManager.tsx b/src/components/admin/AdminManager.tsx index d07d989..fdeb0d0 100644 --- a/src/components/admin/AdminManager.tsx +++ b/src/components/admin/AdminManager.tsx @@ -9,6 +9,7 @@ import { Download, Fingerprint, FolderGit2, + Hourglass, Loader2, Lock, Plus, @@ -67,6 +68,12 @@ export interface AdminData { /** Projekte mit Repository, die grundsaetzlich in Frage kommen. */ eligible: number; }; + stale: { + afterDays: number; + lastCheckAt: string | Date | null; + /** Wie viele Konten ueberhaupt auf Stillstand hoeren. */ + listeners: number; + }; } type Notice = { ok: boolean; text: string } | null; @@ -187,6 +194,7 @@ export function AdminManager({ data }: { data: AdminData }) { + {/* ── Instanz ─────────────────────────────────────── */}
@@ -574,6 +582,124 @@ function AutoSyncSection({ data }: { data: AdminData["autoSync"] }) { ); } +const STALE_OPTIONS = [14, 30, 60, 90, 180, 365]; + +function StaleSection({ data }: { data: AdminData["stale"] }) { + const router = useRouter(); + const [days, setDays] = useState(data.afterDays); + const [busy, setBusy] = useState(false); + const [notice, setNotice] = useState(null); + + async function save(value: number) { + setBusy(true); + setNotice(null); + try { + const response = await fetch("/api/admin/settings", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ staleAfterDays: value }), + }); + const body = await response.json().catch(() => ({})); + if (!response.ok) throw new Error(body.error ?? "Speichern fehlgeschlagen."); + setNotice({ ok: true, text: "Gespeichert." }); + router.refresh(); + } catch (caught) { + setDays(data.afterDays); + setNotice({ ok: false, text: caught instanceof Error ? caught.message : String(caught) }); + } finally { + setBusy(false); + } + } + + return ( +
+
+ + + +
+

+ Stillstand +

+

+ Ab wann ein Projekt als eingeschlafen gilt. Massstab ist der letzte Commit im + verknuepften Repository – das Aenderungsdatum taugt nicht, weil jeder Abgleich + es anfasst. Projekte ohne Repository bleiben aussen vor, ebenso fertige und + archivierte. +

+
+
+ +
+
+ + +
+ +
+
+
+ Letzte Pruefung +
+
+ {data.lastCheckAt ? formatRelative(data.lastCheckAt) : "noch keine"} +
+
+
+
+ Empfaenger +
+
{data.listeners}
+
+
+
+ + {data.listeners === 0 ? ( +

+ Niemand hoert derzeit auf diesen Anlass – die Pruefung laeuft dann gar nicht + erst. Einzurichten unter „Mein Konto → Benachrichtigungen“. +

+ ) : ( +

+ Die Pruefung laeuft hoechstens einmal am Tag. Je Projekt wird einmal gemeldet; + erneut erst, wenn danach wieder ein Commit kam und es wieder still wurde. +

+ )} + + {notice ? ( +

+ {notice.text} +

+ ) : null} +
+ ); +} + function ModeCard({ active, disabled,