Cumulative MIT copyright reflecting the substantial rewrite while
preserving the original author's notice as required by the license.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add a "Status" link in the admin header pointing to the public status page
(/), mirroring the existing "Go to admin" link on that page. Add a gap to
.header-actions so the new link and Logout button are spaced apart.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wrap the "Create New Feed" form in a native <details> accordion, collapsed
by default and auto-opened when no feeds exist. After creating a feed,
redirect to the "Your Feeds" anchor so the new feed is immediately visible.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Keep sanitizeElement single-purpose and run the cid: rewrite as a
separate guarded pass over [src] elements. Use a type-only import for
AttachmentData.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Capture each attachment's Content-ID at ingestion (postal-mime and
mailparser paths) and rewrite cid: image refs to the stored /files URL
in processEmailContent, shared by the entry view and RSS/Atom feeds.
Bodyless HTML fragments are now serialized so sanitization and the cid
rewrite apply to them too.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The admin email detail view loaded the full email but never rendered its
attachments, so there was no way to download them from the admin UI (only
the public entry view and the feed enclosure exposed them).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The form-based bulk-delete fallback removed KV entries but left R2
attachments orphaned. Extract a shared deleteAttachmentsForEmails helper
and use it across single, JSON bulk, and form bulk delete paths.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add an ATTACHMENTS_ENABLED switch (default on when R2 is bound) via a
central getAttachmentBucket helper, surface R2 + estimated KV usage
against the free tier on the status page and /api/stats (refreshed by the
hourly cron), let setup.sh create and wire the R2 bucket, and bind the
demo bucket so the deployed demo has attachments.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Show an inline paperclip icon before the subject in the admin email
list when an email has attachments, with the count in a tooltip. Uses
the attachmentIds already stored in metadata, so no extra fetch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The email detail page loaded the full EmailData (including attachments)
but never rendered them, so attachments were invisible. Add a conditional
"Attachments" section linking each file to /files/:id/:filename with name
and human-readable size.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Capture each sender's List-Unsubscribe one-click URL during ingestion
(stored per sender in feed metadata, mirroring the iconDomain pattern) and
fire one-click POSTs via ctx.waitUntil when a feed is deleted, so newsletters
stop mailing the now-dead address. Tracked with a new unsubscribes_sent
counter surfaced on the status page and /api/stats.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Resolve each feed's most recent sender domain and serve its favicon at
GET /favicon/:feedId, falling back to the project icon. Icons are fetched
in the background on ingestion (direct /favicon.ico then a DuckDuckGo
fallback), cached base64 in KV keyed by domain with a 1-week TTL so the
fetch only fires when absent. Exposed via RSS <image> / Atom <icon>/<logo>
and rendered in the admin feed list, plus a landing-page feature card.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Serve an inline SVG icon at /favicon.svg and /favicon.ico and link it
from the shared Layout and the standalone entry view, so the admin UI,
status page, and entry pages stop emitting /favicon.ico 404s. Doubles
as the fallback for the upcoming per-feed favicon feature.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add a native details/summary accordion FAQ inspired by kill-the-newsletter,
rewritten for self-hosted differentiators; drop /admin from the demo URL.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Rework the public / status page from a flat uniform grid into a hero
featured metric plus four themed sections (Feeds, Emails, Distribution,
Instance). Add semantic colors (green success, red rejects/deletes),
relative timestamps with UTC tooltips, and derived metrics (net feeds,
acceptance rate, avg emails/feed, humanized uptime). Grid is fluid above
640px (auto-fit) and locks to two columns on mobile.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Surface the live stats counter directly under the hero, ahead of the
"Try it live" banner. Demo CTAs (hero + banner) now open the demo root
instead of /admin so visitors land on the public status page first.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Remove unused import flagged by CI lint, then harden the toolchain so
such issues are caught before push:
- lint-staged now also matches .tsx/.jsx (previously .tsx files skipped
the pre-commit eslint pass, which is how the error reached CI)
- eslint ignores generated client bundles (gitignored, not worth linting)
- typecheck now also runs the client tsconfig; the hand-written browser
source was excluded from the root config and never type-checked
- consolidate the window global augmentations (showToast,
parseJsonResponseOrThrow) into a single client globals.d.ts; the inline
declare-global blocks failed (non-module files) and masked real errors
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Fix the install step grid track (48px minmax(0,1fr)) so wide code blocks
and the WAF table no longer blow out the page width on mobile. Transpose
the WAF rate-limit table to a vertical layout (endpoints as columns,
settings as rows) and reclaim horizontal space with tighter mobile
padding.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add a "Live from the demo instance" section to the landing page that
fetches feeds_created and emails_received from the demo /api/stats and
counts them up on scroll into view. Make /api/stats publicly readable
(CORS *) and refresh the stale allowlist origins to kill-the.news.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add backlog items for a project favicon (also used as the per-feed
fallback), per-feed favicons resolved from the last sender's domain with
aggressive caching, and RFC 8058 one-click unsubscribe on feed deletion.
Include a detailed design breakdown for the per-feed favicon feature.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add GET /api/stats exposing cumulative counters (feeds created/deleted,
emails received/rejected, recent date-times) plus live values (active
feeds, active WebSub subscriptions). Counters persist in a stats:counters
KV singleton and are incremented at the email-processing chokepoint and
feed create/delete paths. Replace the / → /admin redirect with a public
status page rendering these figures with a link to the admin.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the demo nightly KV wipe with a per-feed expiry. Feeds can be
given a lifetime at creation (and edited later); FEED_TTL_HOURS locks the
value server-side and greys out the UI field. Expired feeds stay visible
in admin (greyed, actions disabled), return 410 on rss/atom/entries, and
reject inbound emails. The scheduled handler now purges only expired
feeds (KV + R2 attachments) on an hourly global cron.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wrangler 4.94+ introduced --experimental-autoconfig (default: true) which
fails in non-interactive CI environments. Without a committed wrangler.toml,
the release action build was broken.
- Add wrangler.build.toml with minimal config (placeholder KV ID, no secrets)
- Update build script to use wrangler.build.toml + --no-experimental-autoconfig
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix menu path: Security → Security rules (not Security → WAF)
- Add free tier limitations note: 1 rule max, 10s period/block cap
- Show recommended vs free tier limits side by side in table
- Remove HTTP method filter from conditions (not available in rate limiting rules)
- Note Terraform supports method filtering and longer periods (paid plan)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a minimal header with a branded link to kill-the.news and an
"admin" badge, plus a discreet footer with site link and GitHub
Sponsors link.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add src/utils/urls.ts with baseUrl, feedRssUrl, feedAtomUrl, feedUrl,
feedEmailAddress, feedTopicPattern
- Add optional EMAIL_DOMAIN env var so web domain and email domain can
differ (e.g. demo.kill-the.news serves feeds, @kill-the.news receives mail)
- Replace all inline domain template literals with the new helpers
- Remove unused site_url/feed_url fields from FeedConfig
- Remove unused feedPath param from fetchFeedData
- Extract verifyCallback() to deduplicate verifyAndStoreSubscription /
verifyAndDeleteSubscription
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- docs/index.html: nav links (Features/How it works/Install), hero CTAs
(Try demo primary, Self-host, GitHub), demo banner with credentials,
full 7-step installation section with WAF rate limiting guide (dashboard
+ Terraform) integrated as step 7
- wrangler-example.toml: cron trigger on demo env for nightly KV reset at 03:00 UTC
- src/index.ts: scheduled handler that wipes all EMAIL_STORAGE KV keys
- TODO.md: mark WAF rate limiting as done
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WebSub / PubSubHubbub:
- Hub now accepts both /rss/:id and /atom/:id topic URLs
- WebSubSubscription stores format ("rss" | "atom")
- notifySubscribers sends RSS or Atom XML with correct Content-Type
- verifyAndStoreSubscription sends correct topic URL per format
- CI paths-ignore docs/** to skip deploy on docs-only changes
HTML processing (linkedom + escape-html):
- New html-processor.ts: body extraction, script/iframe/object removal,
event handler + javascript: URL stripping, mso-* style cleanup,
plain text → <pre> with HTML escaping via escape-html
- feed-generator.ts and entries.ts use processEmailContent
Admin UI:
- W3C validation badges (Atom + RSS) on feed detail page
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Entry <id> was a non-URL string (timestamp + base64 snippet), which
is invalid per the Atom spec; now uses the entry permalink URL which
is both valid and stable across feed regeneration
- Strip mso-* properties from inline style attributes in extracted body
content to eliminate the feed validator DangerousStyleAttr warning
caused by Microsoft Office HTML in newsletter emails
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Entry <id> was a non-URL string (timestamp + base64 snippet), which
is invalid per the Atom spec; now uses the entry permalink URL which
is both valid and stable across feed regeneration
- Strip mso-* properties from inline style attributes in extracted body
content to eliminate the feed validator DangerousStyleAttr warning
caused by Microsoft Office HTML in newsletter emails
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add Atom Feed URL to the Feed Details card in the emails page
- Fix extractBodyContent to handle emails without a closing </body> tag
(regex now falls back to capturing everything after the opening <body>)
- Use the actual request URL origin for atom:link rel="self" in RSS/Atom
feeds, guaranteeing it always matches the document location regardless
of how DOMAIN is configured
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- link: computed as /admin/feeds/:id/emails instead of stale site_url from KV
- id: computed dynamically from baseUrl instead of stale feed_url from KV
- item description/content: strip <html><head><body> wrapper via extractBodyContent()
so feed readers receive a body fragment, not a full HTML document
Fixes RSS validator warnings: SelfDoesntMatchLocation (stale KV domain) and
InvalidHTML (full HTML document inside <description>/<content:encoded>).
Adds 8 tests covering extractBodyContent and the new feed/atom link assertions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Atom feed URL shown in both list and table views (new Atom column)
- Remove container-wide toggle — both views now use max-width 1200px
- Update dashboard title and login title to kill-the-news
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Imports Inter 400/500/600/700 from Google Fonts to match the landing
page typography. Updates browser tab title format.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- variables.css: orange primary (#f6821f), dark bg (#0a0a0a), Inter font
- layout.css: orange radial glow, unified container 1200px (no width jump)
- components.css: orange buttons, remove backdrop-filter on inputs/cards
Fixes blurred form fields (double backdrop-filter), jarring width shift
between list/table views, and mismatched blue iOS aesthetic vs orange
Cloudflare identity of the site.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The [env.demo] section pointed DOMAIN to kill-the.news while the
custom_domain route was demo.kill-the.news, causing feed/email URLs
to show the wrong domain in the admin UI.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>