feat: simplify admin workspace

This commit is contained in:
2026-08-27 12:38:29 +00:00 Unverified
parent a630e0cc2f
commit 5d468e554e
19 changed files with 504 additions and 748 deletions
-161
View File
@@ -1,161 +0,0 @@
import { db } from "@/db";
import { forms, submissions } from "@/db/schema";
import { desc, eq, sql } from "drizzle-orm";
import { max } from "drizzle-orm/sql/functions/aggregate";
export type DashboardDateWindows = {
todayStart: Date;
tomorrowStart: Date;
last7DaysStart: Date;
};
export type AdminDashboardData = {
summary: {
totalForms: number;
openForms: number;
closedForms: number;
totalResponses: number;
responsesToday: number;
responsesLast7Days: number;
};
recentSubmissions: {
id: string;
formId: string;
formTitle: string;
userName: string | null;
userDiscordId: string | null;
submittedAt: Date;
edited: boolean;
}[];
activeForms: {
id: string;
title: string;
isOpen: boolean;
totalResponses: number;
responsesToday: number;
responsesLast7Days: number;
lastResponseAt: Date | null;
}[];
};
export function getDashboardDateWindows(now = new Date()): DashboardDateWindows {
const bangkokDate = new Intl.DateTimeFormat("en-CA", {
timeZone: "Asia/Bangkok",
year: "numeric",
month: "2-digit",
day: "2-digit",
}).format(now);
const todayStart = new Date(`${bangkokDate}T00:00:00+07:00`);
const tomorrowStart = new Date(todayStart);
tomorrowStart.setUTCDate(tomorrowStart.getUTCDate() + 1);
const last7DaysStart = new Date(todayStart);
last7DaysStart.setUTCDate(last7DaysStart.getUTCDate() - 6);
return { todayStart, tomorrowStart, last7DaysStart };
}
function toPostgresTimestamp(date: Date) {
return date.toISOString().replace("T", " ").replace("Z", "");
}
export async function getAdminDashboardData(
now = new Date()
): Promise<AdminDashboardData> {
const { todayStart, tomorrowStart, last7DaysStart } =
getDashboardDateWindows(now);
const nowSql = now.toISOString();
const todayStartSql = toPostgresTimestamp(todayStart);
const tomorrowStartSql = toPostgresTimestamp(tomorrowStart);
const last7DaysStartSql = toPostgresTimestamp(last7DaysStart);
const totalResponses = sql<number>`count(${submissions.id})::integer`;
const acceptingSubmissions = sql<boolean>`${forms.isOpen} and (
${forms.closesAt} is null or ${forms.closesAt} > ${nowSql}::timestamptz
)`;
const responsesToday = sql<number>`count(${submissions.id}) filter (
where ${submissions.submittedAt} >= ${todayStartSql}::timestamp
and ${submissions.submittedAt} < ${tomorrowStartSql}::timestamp
)::integer`;
const responsesLast7Days = sql<number>`count(${submissions.id}) filter (
where ${submissions.submittedAt} >= ${last7DaysStartSql}::timestamp
and ${submissions.submittedAt} < ${tomorrowStartSql}::timestamp
)::integer`;
const [formSummaryRows, responseSummaryRows, recentSubmissions, activeForms] =
await Promise.all([
db
.select({
totalForms: sql<number>`count(*)::integer`,
openForms: sql<number>`count(*) filter (where ${acceptingSubmissions})::integer`,
closedForms: sql<number>`count(*) filter (where not (${acceptingSubmissions}))::integer`,
})
.from(forms),
db
.select({
totalResponses: sql<number>`count(*)::integer`,
responsesToday: sql<number>`count(*) filter (
where ${submissions.submittedAt} >= ${todayStartSql}::timestamp
and ${submissions.submittedAt} < ${tomorrowStartSql}::timestamp
)::integer`,
responsesLast7Days: sql<number>`count(*) filter (
where ${submissions.submittedAt} >= ${last7DaysStartSql}::timestamp
and ${submissions.submittedAt} < ${tomorrowStartSql}::timestamp
)::integer`,
})
.from(submissions),
db
.select({
id: submissions.id,
formId: submissions.formId,
formTitle: forms.title,
userName: submissions.userName,
userDiscordId: submissions.userDiscordId,
submittedAt: submissions.submittedAt,
edited: sql<boolean>`jsonb_array_length(coalesce(${submissions.editHistory}, '[]'::jsonb)) > 0`,
})
.from(submissions)
.innerJoin(forms, eq(forms.id, submissions.formId))
.orderBy(desc(submissions.submittedAt), desc(submissions.id))
.limit(6),
db
.select({
id: forms.id,
title: forms.title,
isOpen: acceptingSubmissions,
totalResponses,
responsesToday,
responsesLast7Days,
lastResponseAt: max(submissions.submittedAt),
})
.from(forms)
.leftJoin(submissions, eq(submissions.formId, forms.id))
.groupBy(forms.id, forms.title, forms.isOpen, forms.closesAt, forms.createdAt)
.orderBy(
desc(responsesLast7Days),
desc(totalResponses),
desc(forms.createdAt),
desc(forms.id)
)
.limit(5),
]);
const formSummary = formSummaryRows[0] ?? {
totalForms: 0,
openForms: 0,
closedForms: 0,
};
const responseSummary = responseSummaryRows[0] ?? {
totalResponses: 0,
responsesToday: 0,
responsesLast7Days: 0,
};
return {
summary: {
...formSummary,
...responseSummary,
},
recentSubmissions,
activeForms,
};
}
+69
View File
@@ -0,0 +1,69 @@
import { describe, expect, test } from "bun:test";
import {
collectServiceHealth,
createCachedHealthReader,
type ServiceHealthProbe,
type ServiceHealthSnapshot,
} from "@/lib/admin/service-health-core";
function probe(
id: string,
check: ServiceHealthProbe["check"],
): ServiceHealthProbe {
return { id, label: id, category: "core", check };
}
describe("admin service health", () => {
test("reports healthy and unavailable services without rejecting the snapshot", async () => {
const snapshot = await collectServiceHealth(
[
probe("healthy", async () => true),
probe("unhealthy", async () => false),
probe("failed", async () => {
throw new Error("offline");
}),
],
{ now: () => new Date("2026-08-27T12:00:00.000Z") },
);
expect(snapshot.checkedAt).toBe("2026-08-27T12:00:00.000Z");
expect(snapshot.services.map(({ id, status }) => ({ id, status }))).toEqual([
{ id: "healthy", status: "operational" },
{ id: "unhealthy", status: "unavailable" },
{ id: "failed", status: "unavailable" },
]);
});
test("marks probes unavailable after the timeout", async () => {
const snapshot = await collectServiceHealth(
[probe("slow", () => new Promise(() => undefined))],
{ timeoutMs: 1 },
);
expect(snapshot.services[0]?.status).toBe("unavailable");
});
test("reuses snapshots until the cache expires", async () => {
let currentTime = 0;
let loads = 0;
const snapshot: ServiceHealthSnapshot = {
checkedAt: "2026-08-27T12:00:00.000Z",
services: [],
};
const read = createCachedHealthReader(
async () => {
loads += 1;
return snapshot;
},
{ ttlMs: 60_000, now: () => currentTime },
);
expect(await read()).toBe(snapshot);
expect(await read()).toBe(snapshot);
expect(loads).toBe(1);
currentTime = 60_000;
expect(await read()).toBe(snapshot);
expect(loads).toBe(2);
});
});
+79
View File
@@ -0,0 +1,79 @@
export type ServiceHealthCategory = "core" | "integration";
export type ServiceHealthStatus = "operational" | "unavailable";
export type ServiceHealthProbe = {
id: string;
label: string;
category: ServiceHealthCategory;
check: () => Promise<boolean>;
};
export type ServiceHealthSnapshot = {
checkedAt: string;
services: {
id: string;
label: string;
category: ServiceHealthCategory;
status: ServiceHealthStatus;
}[];
};
async function checkWithTimeout(check: () => Promise<boolean>, timeoutMs: number) {
let timeout: ReturnType<typeof setTimeout> | undefined;
try {
return await Promise.race([
check(),
new Promise<boolean>((resolve) => {
timeout = setTimeout(() => resolve(false), timeoutMs);
}),
]);
} catch {
return false;
} finally {
if (timeout) clearTimeout(timeout);
}
}
export async function collectServiceHealth(
probes: ServiceHealthProbe[],
options: { timeoutMs?: number; now?: () => Date } = {},
): Promise<ServiceHealthSnapshot> {
const timeoutMs = options.timeoutMs ?? 5_000;
const checkedAt = (options.now ?? (() => new Date()))().toISOString();
const services = await Promise.all(
probes.map(async ({ id, label, category, check }) => ({
id,
label,
category,
status: (await checkWithTimeout(check, timeoutMs))
? ("operational" as const)
: ("unavailable" as const),
})),
);
return { checkedAt, services };
}
export function createCachedHealthReader(
load: () => Promise<ServiceHealthSnapshot>,
options: { ttlMs?: number; now?: () => number } = {},
) {
const ttlMs = options.ttlMs ?? 60_000;
const now = options.now ?? Date.now;
let cached:
| { expiresAt: number; value: Promise<ServiceHealthSnapshot> }
| undefined;
return () => {
const currentTime = now();
if (cached && currentTime < cached.expiresAt) return cached.value;
const value = load();
cached = { expiresAt: currentTime + ttlMs, value };
void value.catch(() => {
if (cached?.value === value) cached = undefined;
});
return value;
};
}
+60
View File
@@ -0,0 +1,60 @@
import "server-only";
import { sql } from "drizzle-orm";
import { db } from "@/db";
import { getDiscordMemberCount } from "@/lib/followers/discord";
import { getRobloxFollowerCount } from "@/lib/followers/roblox";
import { getTiktokProfile } from "@/lib/followers/tiktok";
import { getYoutubeSubscriberCount } from "@/lib/followers/youtube";
import { getRedisClient } from "@/lib/redis";
import {
collectServiceHealth,
createCachedHealthReader,
type ServiceHealthProbe,
} from "./service-health-core";
const probes: ServiceHealthProbe[] = [
{
id: "database",
label: "PostgreSQL",
category: "core",
check: async () => {
await db.execute(sql`select 1`);
return true;
},
},
{
id: "redis",
label: "Redis / realtime",
category: "core",
check: async () => (await (await getRedisClient()).ping()) === "PONG",
},
{
id: "youtube",
label: "YouTube",
category: "integration",
check: async () => (await getYoutubeSubscriberCount()) !== null,
},
{
id: "tiktok",
label: "TikTok",
category: "integration",
check: async () => (await getTiktokProfile()).followerCount !== null,
},
{
id: "discord",
label: "Discord",
category: "integration",
check: async () => (await getDiscordMemberCount()) !== null,
},
{
id: "roblox",
label: "Roblox",
category: "integration",
check: async () => (await getRobloxFollowerCount()) !== null,
},
];
export const getAdminServiceHealth = createCachedHealthReader(() =>
collectServiceHealth(probes),
);