This commit was merged in pull request #91.
This commit is contained in:
@@ -17,11 +17,13 @@ import relayRouter from "./relay.js";
|
||||
import adminRelayRouter from "./admin-relay.js";
|
||||
import adminRelayQueueRouter from "./admin-relay-queue.js";
|
||||
import geminiRouter from "./gemini.js";
|
||||
import statsRouter from "./stats.js";
|
||||
|
||||
const router: IRouter = Router();
|
||||
|
||||
router.use(healthRouter);
|
||||
router.use(metricsRouter);
|
||||
router.use(statsRouter);
|
||||
router.use(jobsRouter);
|
||||
router.use(estimateRouter);
|
||||
router.use(bootstrapRouter);
|
||||
|
||||
59
artifacts/api-server/src/routes/stats.ts
Normal file
59
artifacts/api-server/src/routes/stats.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Router, type Request, type Response } from "express";
|
||||
import { db, jobs } from "@workspace/db";
|
||||
import { sql, gte } from "drizzle-orm";
|
||||
import { makeLogger } from "../lib/logger.js";
|
||||
|
||||
const router = Router();
|
||||
const logger = makeLogger("stats");
|
||||
|
||||
/**
|
||||
* GET /api/stats/activity
|
||||
*
|
||||
* Returns job counts bucketed by hour for the past 24 hours.
|
||||
* Each bucket represents a UTC hour (0–23).
|
||||
* Hours with no activity are included as 0.
|
||||
*
|
||||
* Response shape:
|
||||
* { hours: number[24], generatedAt: string }
|
||||
* hours[0] = oldest hour (24h ago), hours[23] = current hour
|
||||
*/
|
||||
router.get("/api/stats/activity", async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const now = new Date();
|
||||
const windowStart = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
|
||||
// Count completed jobs grouped by the hour they were created,
|
||||
// within the last 24h window.
|
||||
const rows = await db
|
||||
.select({
|
||||
hour: sql<number>`cast(extract(epoch from date_trunc('hour', created_at)) as bigint)`,
|
||||
count: sql<number>`cast(count(*) as int)`,
|
||||
})
|
||||
.from(jobs)
|
||||
.where(gte(jobs.createdAt, windowStart))
|
||||
.groupBy(sql`date_trunc('hour', created_at)`);
|
||||
|
||||
// Build a map: epoch-hour → count
|
||||
const byEpochHour = new Map<number, number>();
|
||||
for (const row of rows) {
|
||||
byEpochHour.set(Number(row.hour), Number(row.count));
|
||||
}
|
||||
|
||||
// Build 24-slot array aligned to whole hours, oldest first.
|
||||
// slot 0 = floor(now - 24h), slot 23 = floor(now)
|
||||
const currentHourEpoch = Math.floor(now.getTime() / (3600 * 1000)) * 3600;
|
||||
const hours: number[] = [];
|
||||
for (let i = 23; i >= 0; i--) {
|
||||
const slotEpoch = currentHourEpoch - i * 3600;
|
||||
hours.push(byEpochHour.get(slotEpoch) ?? 0);
|
||||
}
|
||||
|
||||
res.json({ hours, generatedAt: now.toISOString() });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Failed to fetch activity stats";
|
||||
logger.error("activity stats failed", { error: message });
|
||||
res.status(500).json({ error: message });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user