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

+31
View File
@@ -0,0 +1,31 @@
import { Skeleton } from "@/components/ui/skeleton";
export default function OnlineLoading() {
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<Skeleton className="h-9 w-40" />
<Skeleton className="h-5 w-64 mt-2" />
</div>
</div>
<div className="flex items-center gap-3">
<Skeleton className="h-10 w-64" />
<Skeleton className="h-10 w-24" />
</div>
<div className="rounded-md border">
{Array.from({ length: 8 }, (_, i) => i).map((n) => (
<div
key={`skeleton-row-${n}`}
className="flex items-center gap-6 px-4 py-3 border-b last:border-0"
>
<Skeleton className="h-4 w-12" />
<Skeleton className="h-4 w-32" />
<Skeleton className="h-4 w-24" />
<Skeleton className="h-5 w-16 rounded-full" />
</div>
))}
</div>
</div>
);
}
+137
View File
@@ -0,0 +1,137 @@
"use client";
import { Pause, Play, RefreshCw } from "lucide-react";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useState } from "react";
import { DataTable } from "@/components/admin/data-table";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { getAvatarUrl } from "@/lib/imager";
import type { DataTableColumn, PaginatedResult } from "@/types/common";
interface OnlineUserRow {
id: number;
username: string;
motto: string;
look: string;
}
const columns: DataTableColumn<OnlineUserRow>[] = [
{ key: "id", label: "ID", sortable: true },
{
key: "username",
label: "Username",
sortable: true,
render: (_, row) => (
<div className="flex items-center gap-2">
<img
src={getAvatarUrl(row.look, { size: "s", headOnly: true })}
alt=""
className="h-8 w-8 rounded"
/>
<span className="font-medium">{row.username}</span>
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-[var(--admin-success)]/70 opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-[var(--admin-success)]" />
</span>
</div>
),
},
{
key: "motto",
label: "Motto",
render: (value) => (
<span className="max-w-[250px] truncate block">
{String(value) || "-"}
</span>
),
},
{
key: "look",
label: "Avatar",
render: (_, row) => (
<img src={getAvatarUrl(row.look)} alt={row.username} className="h-16" />
),
},
];
interface OnlineTableProps {
data: PaginatedResult<OnlineUserRow>;
}
const REFRESH_INTERVAL = 15_000; // 15 seconds
export function OnlineTable({ data }: OnlineTableProps) {
const router = useRouter();
const [autoRefresh, setAutoRefresh] = useState(false);
const [lastUpdated, setLastUpdated] = useState(() => new Date());
const [isRefreshing, setIsRefreshing] = useState(false);
const doRefresh = useCallback(() => {
setIsRefreshing(true);
router.refresh();
setLastUpdated(new Date());
// Small delay to show spinner
setTimeout(() => setIsRefreshing(false), 500);
}, [router]);
useEffect(() => {
if (!autoRefresh) return;
const timer = setInterval(doRefresh, REFRESH_INTERVAL);
return () => clearInterval(timer);
}, [autoRefresh, doRefresh]);
// Update lastUpdated when data changes
// biome-ignore lint/correctness/useExhaustiveDependencies: explicitly chosen here, see surrounding code
useEffect(() => {
setLastUpdated(new Date());
}, [data.total]);
return (
<div className="space-y-4">
{/* Auto-refresh controls */}
<div className="flex items-center gap-3 text-sm">
<Button
variant="outline"
size="sm"
onClick={doRefresh}
disabled={isRefreshing}
>
<RefreshCw
className={`mr-1.5 h-3.5 w-3.5 ${isRefreshing ? "animate-spin" : ""}`}
/>
Refresh
</Button>
<Button
variant={autoRefresh ? "default" : "outline"}
size="sm"
onClick={() => setAutoRefresh(!autoRefresh)}
>
{autoRefresh ? (
<>
<Pause className="mr-1.5 h-3.5 w-3.5" /> Auto-refresh ON
</>
) : (
<>
<Play className="mr-1.5 h-3.5 w-3.5" /> Auto-refresh OFF
</>
)}
</Button>
{autoRefresh && (
<Badge variant="secondary" className="text-xs">
Every {REFRESH_INTERVAL / 1000}s
</Badge>
)}
<span className="text-xs text-muted-foreground ml-auto">
Last updated: {lastUpdated.toLocaleTimeString()}
</span>
</div>
<DataTable
data={data}
columns={columns}
searchPlaceholder="Search by username or motto..."
/>
</div>
);
}
+73
View File
@@ -0,0 +1,73 @@
import { and, count, desc, eq, like, or, type SQL } from "drizzle-orm";
import { Wifi } from "lucide-react";
import { getTranslations } from "next-intl/server";
import { AdminPageShell } from "@/components/admin/admin-page-shell";
import { fetchAdminList } from "@/lib/admin-list";
import { db, User } from "@/lib/db";
import { PERMS } from "@/lib/permissions";
import { OnlineTable } from "./online-table";
export default async function OnlineUsersPage({
params: paramsPromise,
searchParams,
}: {
params: Promise<{ locale: string }>;
searchParams: Promise<Record<string, string>>;
}) {
const result = await fetchAdminList(
{
permission: PERMS.USERS_VIEW,
defaultPerPage: 50,
fetch: async ({ search, page, perPage }) => {
const conditions: (SQL | undefined)[] = [eq(User.online, "1")];
if (search) {
conditions.push(
or(
like(User.username, `%${search}%`),
like(User.motto, `%${search}%`),
),
);
}
const where = and(...conditions);
const [rows, totals] = await Promise.all([
db
.select({
id: User.id,
username: User.username,
motto: User.motto,
look: User.look,
})
.from(User)
.where(where)
.orderBy(desc(User.id))
.limit(perPage)
.offset((page - 1) * perPage),
db.select({ total: count() }).from(User).where(where),
]);
return { rows, total: totals[0]?.total ?? 0 };
},
},
paramsPromise,
searchParams,
);
const t = await getTranslations("pages.admin.online");
return (
<AdminPageShell icon={Wifi} title={t("title")} subtitle={t("subtitle")}>
<p className="text-muted-foreground m-0">
{t("connected", { count: result.total })}
</p>
<OnlineTable
data={{
rows: result.rows,
total: result.total,
page: result.page,
perPage: result.perPage,
lastPage: result.lastPage,
}}
/>
</AdminPageShell>
);
}