refactor(api): remove the deprecated /api/stats endpoint

The only consumer (the marketing landing) now uses /api/v1/stats, so drop
the legacy /api/stats route and its handler. Delete src/routes/stats.ts and
its test; repoint the index CORS test at /api/v1/stats.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Julien Herr
2026-05-23 23:15:08 +02:00
parent daa93d8093
commit c2a0a68058
7 changed files with 5 additions and 87 deletions
-2
View File
@@ -34,7 +34,6 @@ Single Cloudflare Worker built with Hono. Routes:
| ------------------------------------ | ---------------------------------------------------------------------- | ------- |
| `GET /` | Public status page (monitoring counters + link to admin) |
| `POST /api/inbound` | Webhook from ForwardEmail; IP-allowlisted to their MX sources |
| `GET /api/stats` | Deprecated alias of `GET /api/v1/stats` (public monitoring counters) |
| `/api/v1/feeds*` | Versioned REST API (Bearer/proxy auth) — feeds + emails CRUD |
| `GET /api/v1/stats` | Public monitoring counters (JSON, CORS); canonical stats endpoint |
| `GET /api/openapi.json` | OpenAPI 3.1 spec (public) |
@@ -65,7 +64,6 @@ src/
files.ts # R2 attachment serving
hub.ts # WebSub hub
home.tsx # Public status page (GET /)
stats.ts # Monitoring counters API (GET /api/stats)
admin.tsx # Admin UI entrypoint (hono/jsx)
admin/ # Admin sub-modules
feeds.tsx # Feeds CRUD UI
+1 -1
View File
@@ -156,7 +156,7 @@ This feature is **optional**. If no R2 bucket is bound, attachments are silently
Attachments are deleted from R2 automatically when the corresponding email is deleted from the admin UI, or when an email is dropped during feed size trimming.
**Monitoring storage / free tier:** the status page (`/`) and `/api/stats` report R2 space used (against the **10 GB** R2 free tier) and an estimate of KV space used (against the **1 GB** KV free tier). The figures are refreshed hourly by the cron trigger. KV usage is an estimate based on stored email sizes, so treat it as a lower bound.
**Monitoring storage / free tier:** the status page (`/`) and `/api/v1/stats` report R2 space used (against the **10 GB** R2 free tier) and an estimate of KV space used (against the **1 GB** KV free tier). The figures are refreshed hourly by the cron trigger. KV usage is an estimate based on stored email sizes, so treat it as a lower bound.
### External auth provider (Authelia / Authentik / reverse proxy)
+1 -3
View File
@@ -57,13 +57,11 @@ Main routes:
- `src/routes/api/`: versioned REST API + OpenAPI spec/docs (`/api/v1/*`, `/api/openapi.json`, `/api/docs`)
- `src/lib/feed-service.ts`: shared feed create/update/delete (used by the admin UI and the REST API)
- `src/routes/home.tsx`: public status page (`GET /`)
- `src/routes/stats.ts`: monitoring counters API (`GET /api/stats`)
### Monitoring
`GET /api/v1/stats` returns JSON counters (public, no auth, CORS-enabled) for
uptime/monitoring tools and the landing page. `GET /api/stats` is a deprecated alias kept
for backward compatibility — prefer the versioned path. Both expose the same fields:
uptime/monitoring tools and the landing page:
| Field | Meaning |
| ----------------------------- | -------------------------------------------------------- |
+2 -2
View File
@@ -45,9 +45,9 @@ describe("CORS middleware", () => {
);
});
it("makes /api/stats readable from any origin", async () => {
it("makes /api/v1/stats readable from any origin", async () => {
const res = await worker.fetch(
req("/api/stats", { headers: { Origin: "https://example.com" } }),
req("/api/v1/stats", { headers: { Origin: "https://example.com" } }),
env as unknown as Env,
);
expect(res.status).toBe(200);
+1 -6
View File
@@ -6,7 +6,6 @@ import { handle as handleAtom } from "./routes/atom";
import { handle as handleAdmin } from "./routes/admin";
import { handle as handleEntry } from "./routes/entries";
import { handle as handleFiles } from "./routes/files";
import { handle as handleStats } from "./routes/stats";
import { handle as handleHome } from "./routes/home";
import { handle as handleFavicon, handleFeedFavicon } from "./routes/favicon";
import { hubRouter } from "./routes/hub";
@@ -148,10 +147,6 @@ api.use("/inbound", async (c, next) => {
// API routes (inbound webhook)
api.post("/inbound", handleInbound);
// Public monitoring stats (JSON) — readable from any origin (landing page, embeds)
api.use("/stats", cors({ origin: "*" }));
api.get("/stats", handleStats);
// RSS feed routes (public)
rss.get("/:feedId", handleRSS);
@@ -223,7 +218,7 @@ export default {
logger.info("Feed TTL cleanup", { deleted: expiredIds.length });
}
// Refresh the cached storage-usage snapshot for the status page / /api/stats.
// Refresh the cached storage-usage snapshot for the status page / /api/v1/stats.
try {
const r2 = attachmentBucket
? await scanR2Usage(attachmentBucket)
-66
View File
@@ -1,66 +0,0 @@
import { describe, it, expect } from "vitest";
import worker from "../index";
import { createMockEnv } from "../test/setup";
import { bumpCounters } from "../utils/stats";
import { FEEDS_LIST_KEY } from "../config/constants";
import type { Env, StatsResponse } from "../types";
function req(path: string, init: RequestInit = {}): Request {
return new Request(`https://test.getmynews.app${path}`, init);
}
describe("GET /api/stats", () => {
it("returns zeroed stats for a fresh instance", async () => {
const env = createMockEnv() as unknown as Env;
const res = await worker.fetch(req("/api/stats"), env);
expect(res.status).toBe(200);
const body = (await res.json()) as StatsResponse;
expect(body).toMatchObject({
feeds_created: 0,
feeds_deleted: 0,
emails_received: 0,
emails_rejected: 0,
active_feeds: 0,
websub_subscriptions_active: 0,
});
});
it("reflects persisted counters and live values", async () => {
const env = createMockEnv() as unknown as Env;
await env.EMAIL_STORAGE.put(
FEEDS_LIST_KEY,
JSON.stringify({ feeds: [{ id: "a", title: "A" }] }),
);
await env.EMAIL_STORAGE.put("websub:a:hash", "{}");
await bumpCounters(env.EMAIL_STORAGE, {
emails_received: 3,
emails_rejected: 1,
feeds_created: 1,
});
const res = await worker.fetch(req("/api/stats"), env);
const body = (await res.json()) as StatsResponse;
expect(body.active_feeds).toBe(1);
expect(body.websub_subscriptions_active).toBe(1);
expect(body.emails_received).toBe(3);
expect(body.emails_rejected).toBe(1);
expect(body.feeds_created).toBe(1);
});
});
describe("GET / (public status page)", () => {
it("returns an HTML status page with counters and an admin link", async () => {
const env = createMockEnv() as unknown as Env;
await bumpCounters(env.EMAIL_STORAGE, { emails_received: 7 });
const res = await worker.fetch(req("/"), env);
expect(res.status).toBe(200);
expect(res.headers.get("Content-Type")).toContain("text/html");
const html = await res.text();
expect(html).toContain('href="/admin"');
expect(html).toContain("Active feeds");
expect(html).toContain("Emails received");
expect(html).toContain("7");
});
});
-7
View File
@@ -1,7 +0,0 @@
import { Context } from "hono";
import { Env } from "../types";
import { getStats } from "../utils/stats";
export async function handle(c: Context<{ Bindings: Env }>): Promise<Response> {
return c.json(await getStats(c.env));
}