Revert "Merge pull request 'Complete Housekeeping migration and /ase cutover' (#52) from codex/housekeeping-complete into main"
CI / check (push) Successful in 27s
CI / release (push) Skipped
CI / deploy (push) Successful in 43s

This reverts commit 488b6e57c4, reversing
changes made to b506b4499a.
This commit is contained in:
Simo committed 2026-08-30 21:31:34 +02:00
1 parent 488b6e57c4
commit b1ddda66ff
802 files changed
+61370 -76659

No files matched your search

+93
View File
@@ -0,0 +1,93 @@
import { count, desc } from "drizzle-orm";
import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import { Card, CardContent } from "@/components/ui/card";
import { db, EmulatorErrors } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
export default async function EmulatorErrorsPage({
searchParams,
}: {
searchParams: Promise<Record<string, string>>;
}) {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.DEVOPS_VIEW, session.user.rank)) {
redirect("/admin");
}
const t = await getTranslations("pages.admin.devops");
const params = await searchParams;
const page = Math.max(1, Number(params.page) || 1);
const perPage = 50;
const [errors, totalRows] = await Promise.all([
db
.select()
.from(EmulatorErrors)
.orderBy(desc(EmulatorErrors.id))
.limit(perPage)
.offset((page - 1) * perPage),
db.select({ total: count() }).from(EmulatorErrors),
]);
const total = Number(totalRows[0]?.total ?? 0);
const lastPage = Math.ceil(total / perPage);
return (
<div className="space-y-6">
<p className="text-muted-foreground">
{total} total errors — showing page {page} of {lastPage}
</p>
<Card>
<CardContent className="p-0">
{errors.length === 0 ? (
<div className="admin-empty">{t("noEmulatorErrors")}</div>
) : (
<div className="divide-y">
{errors.map((error) => (
<div key={error.id} className="p-4">
<div className="flex items-center justify-between mb-2">
<span className="font-mono text-xs text-muted-foreground">
#{error.id}
</span>
<span className="text-xs text-muted-foreground">
{new Date(error.timestamp * 1000).toLocaleString()}
</span>
</div>
<pre className="text-xs whitespace-pre-wrap break-all font-mono bg-muted p-2 rounded">
{Buffer.from(error.stacktrace).toString("utf-8")}
</pre>
</div>
))}
</div>
)}
</CardContent>
</Card>
{lastPage > 1 && (
<div className="flex justify-center gap-2">
{page > 1 && (
<a
href={`?page=${page - 1}`}
className="text-sm text-primary hover:underline"
>
Previous
</a>
)}
<span className="text-sm text-muted-foreground">
Page {page} of {lastPage}
</span>
{page < lastPage && (
<a
href={`?page=${page + 1}`}
className="text-sm text-primary hover:underline"
>
Next
</a>
)}
</div>
)}
</div>
);
}
@@ -0,0 +1,65 @@
"use client";
import { RefreshCw } from "lucide-react";
import { useEffect, useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
export function HealthCheckClient() {
const [status, setStatus] = useState<"checking" | "online" | "offline">(
"checking",
);
const [checking, setChecking] = useState(false);
async function checkEmulator() {
setChecking(true);
setStatus("checking");
try {
const res = await fetch("/api/admin/devops/health");
if (res.ok) {
const data = await res.json();
setStatus(data.emulator ? "online" : "offline");
} else {
setStatus("offline");
}
} catch {
setStatus("offline");
} finally {
setChecking(false);
}
}
// biome-ignore lint/correctness/useExhaustiveDependencies: explicitly chosen here, see surrounding code
useEffect(() => {
checkEmulator();
}, []);
return (
<div className="flex items-center gap-2">
<Badge
variant={
status === "online"
? "default"
: status === "offline"
? "destructive"
: "secondary"
}
>
{status === "checking"
? "Checking..."
: status === "online"
? "Online"
: "Offline"}
</Badge>
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={checkEmulator}
disabled={checking}
>
<RefreshCw className={`h-3 w-3 ${checking ? "animate-spin" : ""}`} />
</Button>
</div>
);
}
+1
View File
@@ -0,0 +1 @@
export { default } from "../loading";
+182
View File
@@ -0,0 +1,182 @@
import { count, desc, eq, gte } from "drizzle-orm";
import { AlertTriangle, Database, HeartPulse, Server } from "lucide-react";
import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import Link from "@/components/link";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { fetchOpsHealth } from "@/lib/admin/ops-health";
import { db, EmulatorErrors, EmulatorSettings } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { HealthCheckClient } from "./health-check-client";
async function getDevOpsData() {
const now = Math.floor(Date.now() / 1000);
const dayAgo = now - 86400;
const [health, recentErrors, errorsTodayRows, emuVersionRows] =
await Promise.all([
fetchOpsHealth(),
db
.select()
.from(EmulatorErrors)
.orderBy(desc(EmulatorErrors.id))
.limit(20),
db
.select({ total: count() })
.from(EmulatorErrors)
.where(gte(EmulatorErrors.timestamp, dayAgo)),
db
.select({ value: EmulatorSettings.value })
.from(EmulatorSettings)
.where(eq(EmulatorSettings.key, "version"))
.limit(1),
]);
return {
dbStatus: health.dbOk ? ("healthy" as const) : ("error" as const),
dbLatency: health.dbLatencyMs,
redisOk: health.redisOk,
recentErrors,
errorsToday: Number(errorsTodayRows[0]?.total ?? 0),
emuVersion: emuVersionRows[0]?.value ?? "Unknown",
onlineUsers: health.onlineUsers,
};
}
export default async function DevOpsPage() {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.DEVOPS_VIEW, session.user.rank)) {
redirect("/admin");
}
const t = await getTranslations("pages.admin.devops");
const data = await getDevOpsData();
return (
<div className="space-y-6">
{/* Status cards */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-5">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Database</CardTitle>
<Database className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<Badge
variant={data.dbStatus === "healthy" ? "default" : "destructive"}
>
{data.dbStatus === "healthy" ? "Healthy" : "Error"}
</Badge>
<p className="text-xs text-muted-foreground mt-1">
Latency: {data.dbLatency}ms
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Redis</CardTitle>
<Server className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<Badge
variant={
data.redisOk === false
? "destructive"
: data.redisOk === true
? "default"
: "secondary"
}
>
{data.redisOk === true
? "Healthy"
: data.redisOk === false
? "Error"
: "Not configured"}
</Badge>
<p className="text-xs text-muted-foreground mt-1">
Cache / rate limits / sessions
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Emulator</CardTitle>
<Server className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<HealthCheckClient />
<p className="text-xs text-muted-foreground mt-1">
Version: {data.emuVersion}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Online Users</CardTitle>
<HeartPulse className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{data.onlineUsers}</div>
<p className="text-xs text-muted-foreground mt-1">
<Link
href="/admin/commandocentrum#online"
className="text-[var(--admin-accent)] underline-offset-2 hover:underline"
>
Live roster in CommandoCentrum
</Link>
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Errors (24h)</CardTitle>
<AlertTriangle className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{data.errorsToday}</div>
<p className="text-xs text-muted-foreground">
Emulator errors today
</p>
</CardContent>
</Card>
</div>
{/* Recent errors */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<AlertTriangle className="h-4 w-4" /> Recent Emulator Errors
</CardTitle>
</CardHeader>
<CardContent>
{data.recentErrors.length === 0 ? (
<div className="admin-empty">{t("noRecentErrors")}</div>
) : (
<div className="space-y-2 max-h-[500px] overflow-y-auto">
{data.recentErrors.map((error) => (
<div key={error.id} className="rounded-md border p-3 text-sm">
<div className="flex items-center justify-between mb-1">
<span className="font-mono text-xs text-muted-foreground">
#{error.id}
</span>
<span className="text-xs text-muted-foreground">
{new Date(error.timestamp * 1000).toLocaleString()}
</span>
</div>
<pre className="text-xs whitespace-pre-wrap break-all font-mono text-destructive">
{Buffer.from(error.stacktrace).toString("utf-8")}
</pre>
</div>
))}
</div>
)}
</CardContent>
</Card>
</div>
);
}