From c2a0a680581f1a5910bde04adc78d4c10fac19b2 Mon Sep 17 00:00:00 2001 From: Julien Herr Date: Sat, 23 May 2026 23:15:08 +0200 Subject: [PATCH] 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 --- CLAUDE.md | 2 -- INSTALL.md | 2 +- README.md | 4 +-- src/index.test.ts | 4 +-- src/index.ts | 7 +---- src/routes/stats.test.ts | 66 ---------------------------------------- src/routes/stats.ts | 7 ----- 7 files changed, 5 insertions(+), 87 deletions(-) delete mode 100644 src/routes/stats.test.ts delete mode 100644 src/routes/stats.ts diff --git a/CLAUDE.md b/CLAUDE.md index b8afe68..40ee93e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/INSTALL.md b/INSTALL.md index 16129e0..fa090c2 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -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) diff --git a/README.md b/README.md index 61f57f9..60e488a 100644 --- a/README.md +++ b/README.md @@ -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 | | ----------------------------- | -------------------------------------------------------- | diff --git a/src/index.test.ts b/src/index.test.ts index c6d0ff9..d6c316c 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -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); diff --git a/src/index.ts b/src/index.ts index 812d730..41ca3be 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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) diff --git a/src/routes/stats.test.ts b/src/routes/stats.test.ts deleted file mode 100644 index 06fd7b2..0000000 --- a/src/routes/stats.test.ts +++ /dev/null @@ -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"); - }); -}); diff --git a/src/routes/stats.ts b/src/routes/stats.ts deleted file mode 100644 index c072f7a..0000000 --- a/src/routes/stats.ts +++ /dev/null @@ -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 { - return c.json(await getStats(c.env)); -}