From a18d9f165f3260b9d9e6616973c01c4231bdedbe Mon Sep 17 00:00:00 2001 From: Julien Herr Date: Mon, 25 May 2026 17:33:10 +0200 Subject: [PATCH] feat(admin): native feed chips + dashboard pill Add NativeFeeds/NativeFeedChip components to admin/ui.tsx and a NativeFeedPill rendered in both list and table dashboard views when feed.hasNativeFeed is set. Co-Authored-By: Claude Sonnet 4.6 --- src/routes/admin.test.ts | 32 ++++++++++++++++++++++ src/routes/admin.tsx | 12 +++++++++ src/routes/admin/ui.tsx | 57 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+) diff --git a/src/routes/admin.test.ts b/src/routes/admin.test.ts index 122b05b..78aa513 100644 --- a/src/routes/admin.test.ts +++ b/src/routes/admin.test.ts @@ -1538,6 +1538,38 @@ describe("Admin Routes", () => { expect(await editPage.text()).toContain("checked"); }); + it("dashboard shows pill-native for feeds with hasNativeFeed", async () => { + const authCookie = await loginAndGetCookie(); + const repo = FeedRepository.from(mockEnv as unknown as Env); + const feedId = FeedId.generate(); + const mailboxId = MailboxId.unchecked("native.pill.08"); + const feed = Feed.create( + feedId, + { + title: "N", + language: "en", + allowedSenders: [], + blockedSenders: [], + }, + { mailboxId }, + ); + feed.ingest( + { key: "k1", subject: "s", receivedAt: 1, size: 10 }, + { + maxBytes: 1e9, + nativeFeeds: { + senderKey: "a@x.com", + feeds: [{ url: "https://x.com/rss", type: "rss" }], + }, + }, + ); + await repo.save(feed); + + const res = await request("/admin", { headers: { Cookie: authCookie } }); + const body = await res.text(); + expect(body).toContain("pill-native"); + }); + it("clears the toggle when the checkbox is omitted (unchecked)", async () => { const authCookie = await loginAndGetCookie(); const repo = FeedRepository.from(mockEnv as unknown as Env); diff --git a/src/routes/admin.tsx b/src/routes/admin.tsx index 983b6ec..087d589 100644 --- a/src/routes/admin.tsx +++ b/src/routes/admin.tsx @@ -229,6 +229,12 @@ const ConfirmationPill = ({ feedId }: { feedId: string }) => ( ); +const NativeFeedPill = ({ feedId }: { feedId: string }) => ( + + Native feed available + +); + // Admin dashboard route app.get("/", async (c) => { // Type assertion for environment variables @@ -639,6 +645,9 @@ app.get("/", async (c) => { {feed.pendingConfirmation && ( )} + {feed.hasNativeFeed && ( + + )} @@ -763,6 +772,9 @@ app.get("/", async (c) => { {feed.pendingConfirmation && ( )} + {feed.hasNativeFeed && ( + + )} {feed.description && (

{descDisplay} diff --git a/src/routes/admin/ui.tsx b/src/routes/admin/ui.tsx index cf0c4b7..4258d64 100644 --- a/src/routes/admin/ui.tsx +++ b/src/routes/admin/ui.tsx @@ -6,6 +6,7 @@ import { interactiveScripts } from "../../scripts/index"; import { APP_VERSION } from "../../config/version"; import { FAVICON_PATH } from "../favicon"; import { Env } from "../../types"; +import type { NativeFeed } from "../../types"; import { feedFormatUrl, feedValidatorUrl, @@ -254,6 +255,62 @@ export const FeedFormats = ({ ); +// ── Native feed chips ───────────────────────────────────────────────────────── + +const NATIVE_LABELS: Record = { + rss: "RSS", + atom: "Atom", + json: "JSON", +}; + +const NativeFeedChip = ({ feed }: { feed: NativeFeed }) => { + const label = NATIVE_LABELS[feed.type]; + return ( +

+ {label} + + + + + + + + + + + + + + +
+ ); +}; + +export const NativeFeeds = ({ feeds }: { feeds: NativeFeed[] }) => { + if (feeds.length === 0) return null; + return ( +
+ Native feeds +
+ {feeds.map((feed) => ( + + ))} +
+
+ ); +}; + // ── Expiry pill ─────────────────────────────────────────────────────────────── function formatExpiry(expiresAt: number): { label: string; expired: boolean } {