Per-sender storage (latest-wins, like unsubscribe), pure domain detector
(strict 3 MIME types), admin surfaces (detail/badge/pill/dismiss) and a
read-only REST API field, mirroring the confirmation-detection feature.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Document the -develop/tag-driven release contract in CLAUDE.md so a
release is cut correctly: main stays *-develop, the tag is the source of
truth, never commit a bare version, and re-bump the next cycle after.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
The release job built whatever version package.json held at the tagged
commit — but main always carries a -develop suffix, so a vX.Y.Z bundle
would have reported X.Y.Z-develop. Make the tag the source of truth:
strip the suffix in the ephemeral CI checkout before building (never
committed), and fail fast when the tag base doesn't match package.json's
base (wrong-commit guard). Update CONTRIBUTING with the tag-driven flow.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
package.json now holds a -develop pre-release suffix so the version
reported in the footer/health/stats distinguishes a dev build from a
shipped one (0.3.0-develop sorts below 0.3.0 per SemVer). Document the
release flow in CONTRIBUTING.md: strip the suffix at tag time, re-bump
to the next -develop afterward.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Push the "Name <addr>" display-name parsing onto the EmailAddress VO
(displayName field + label() = displayName ?? normalized) and delete the
ad-hoc parseFromAddress helper in feed-generator. The feed builder now
parses the from-address once via EmailAddress and reuses it for the site
base URL, the [Sender] title prefix, and the entry author — one parser,
on the type that owns the data.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
The sender-in-title rendering reads the FeedConfig DTO via the read
model, so the Feed.senderInTitle getter had no consumer — remove it.
In buildFeed, parse the from-address once and reuse it for both the
title prefix and the author; drop the dead `?? email.from` fallback
(parseFromAddress already returns the address as the name).
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Add version to StatsResponse and getStats so the canonical public
monitoring endpoint reports the running build alongside the footer
and /health.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Add a per-feed senderInTitle flag (domain FeedState.senderInTitle ↔
FeedConfig.sender_in_title). When set, the feed generator prefixes each
entry title with [Sender] (display name, falling back to the address).
Exposed as an admin edit-form checkbox and across the REST API
create/update/response schemas.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Inline package.json version at bundle time via src/config/version.ts
(resolveJsonModule), surface it in the shared admin/status footer and
add it to the /health JSON so self-hosters can tell which build runs.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Hoist the shared format chips, expiry pill, and copy icons into admin/ui.tsx
so the feed detail (emails) page renders the same Email + Subscribe block as
the dashboard list, dropping the old per-format rows, W3C validator images,
and the now-dead .feed-validate CSS.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
A "manage subscription" / "subscribe" footer link is now a weak (+1) URL
signal instead of strong (+2), so an ordinary newsletter with a stray body
keyword (active/valid) no longer crosses the detection threshold. A genuine
"confirm your subscription" subject + a bare /subscribe link still passes.
Also dedupe surfaced links. Adds false-positive + recall + dedupe tests.
Return the ranked links directly (string[] | null) instead of an unused
{score, links} wrapper, and drop the redundant hasKeyword helper in favor
of matchesAny(_, KEYWORDS). No behavior change.
saveMetadata now also upserts the list entry so the pendingConfirmation
flag is reflected in the dashboard without an extra per-feed KV read.
toListItemDTO gains an optional third parameter for the flag.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Brainstormed design for detecting newsletter confirmation emails at
ingestion, marking them, and surfacing the confirmation link in the admin
(detail section, list badge, dashboard pill, emails-page banner). v1 does
no outbound request; server on-detect actions deferred.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Add a "Public page" link next to the Rendered/Raw toggle in the admin
email view, opening the standalone /entries/:feedId/:entryId render.
Centralize the entry route shape in a pure entryPath() builder, used by
both the admin link and the RSS/Atom/JSON feed generator.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Replace the stacked RSS/Atom URL rows in the dashboard with a compact
"Subscribe" chip block exposing all three feed formats — including JSON
Feed, previously absent from the admin UI. Each chip carries copy, open,
and validate actions; validation links to the W3C Feed Validator (RSS/Atom)
and validator.jsonfeed.org (JSON). The Table view's RSS+Atom columns fold
into a single Formats column.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Separate the two feed identities so the public read URL never reveals the
inbound address and vice-versa:
- FeedId becomes an opaque high-entropy token (read id + KV key); MailboxId
(noun.noun.NN) owns the inbound address and the untrusted-input boundary
via MailboxId.parse. They map only through the inbound:<mailbox> secondary
index, resolved solely at reception.
- inbound index lifecycle is owned by FeedRepository: written by save/saveConfig,
dropped by removeFromList(Bulk) — symmetric, never mirrored by hand (removes the
manual delete in feed-service + the cron loop, and a silent empty-catch).
- Feed.mailboxId exposes a MailboxId VO (symmetry with Feed.id); the
mailbox@domain shape lives on MailboxId.emailAddress(domain).
- Distinguish mailbox_unknown (no feed claims the address) from feed_not_found
(dangling index) for observability; both forwardable, both 404.
- Drop the redundant EmailParser.extractMailbox pass-through so MailboxId.parse
is the single parse boundary.
Docs (README/INSTALL/CLAUDE.md/landing) and tests updated; 439 tests green,
tsc clean, build dry-run OK.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
- JSON Feed v1.1: post-process .json1() output (lib issue #139 closed
not-planned, stays on version/1) — set version 1.1, authors[], language
- Display running version (admin/status/health), bundled from package.json
- Notify when a newer GitHub Release exists (cached check, opt-out-able)
- Opt-in instance telemetry: central counter and/or public directory,
privacy-first (off by default, no PII, "count me but don't list me")
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Batch of four reader-facing improvements (TODO "Compat lecteurs + dedup"):
- JSON Feed at /json/:feedId (feed lib .json1()); all formats cross-link
- OPML export at /admin/opml (admin-protected; the registry lists every
feed URL, so it must not be public)
- Conditional GET on /rss + /atom: strong ETag + Last-Modified, 304 on
If-None-Match/If-Modified-Since, validators shared via http-cache.ts
- Duplicate-send dedup in ingestion: match by Message-ID, fall back to a
SHA-256 of normalized subject+content; a duplicate is a no-op and bumps
the new emails_deduplicated counter (status page + /api/v1/stats)
429 tests green, tsc clean, build dry-run OK. Docs (README/CLAUDE/TODO +
landing cards) updated.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Catalog the Clever Cloud + Sweego portability epic: adapter matrix,
seven independently-deliverable sub-tasks, open questions, scope, and
acceptance criteria for keeping CF-native and self-host profiles
behavior-equivalent from one codebase.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Close the five open P1·S items from TODO.md:
- X-Robots-Tag: noindex on rss/atom/entries/files + a /robots.txt
- absolutize relative content URLs against the sender's site
- promote lazy-loaded images (data-src → src, strip loading="lazy")
- strip XML-illegal control chars from generated feeds (keep emoji)
- plain-text feed <title> (strip HTML, decode entities)
Sender-base derivation lives on the EmailAddress value object
(siteBaseUrl) instead of a misplaced favicon helper. Bump to 0.2.1
and document the changes in README + CLAUDE.md.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Adds an emails_forwarded counter (a subset of emails_rejected) bumped on a
successful FALLBACK_FORWARD_ADDRESS forward. Dropped = rejected − forwarded.
Surfaced in the /api/v1/stats response and the public status page.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Lets you point a domain's catch-all at the worker without losing personal
mail: inbound mail that isn't a feed (invalid_address / feed_not_found) is
forwarded to an optional verified destination instead of being dropped.
Expired feeds and blocked senders are still dropped so newsletters never
leak to the fallback inbox. Unset env keeps the original drop-and-log path.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Survey of kill-the-newsletter issues/PRs, competitors, RSS readers, and a
code audit. Each idea carries an origin reference (so we can notify the
requester on ship) and a Pn·Size badge (user value × implementation effort).
Adds the FALLBACK_FORWARD_ADDRESS catch-all fallback-forwarding idea.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Move the hardcoded <style> blocks from the single-email view and the admin
email preview iframe into src/styles/*.css so they benefit from Prettier,
linting, and syntax highlighting like the rest of the design system.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Inline images (referenced by src="cid:…") are now classified at ingest and
kept out of the downloadable attachment lists, RSS/Atom enclosures, and the
API — while still stored in R2 and cleaned up with the email. Fixes the admin
email preview, which injected raw HTML into the data: iframe so cid refs never
resolved; it now rewrites them to absolute /files URLs.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Surface the feed TTL / auto-deletion capability on the landing page,
framed as a disposable temporary-mailbox use case.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Move four DDD tensions on the Feed aggregate to ground:
- #1 The aggregate now holds a domain FeedState (camelCase) instead of the
snake_case FeedConfig DTO; infrastructure/feed-mapper.ts owns the
FeedState<->FeedConfig/FeedListItem translation as the sole snake_case site
outside the HTTP edge.
- #3 Replace the edit() recomputeExpiry control flag with a Lifetime VO:
passing a lifetime recomputes expiry, omitting it preserves the current one
(the dashboard quick-edit path).
- #4 Domain events carry their own feedId; dispatchFeedEvents centralizes the
drain+dispatch in the application layer (no more manual pullEvents at call
sites), keeping infra->application dependency direction intact.
- #6 Rename FeedId.fromTrusted to FeedId.unchecked to make the absence of
revalidation explicit.
Adds Lifetime + feed-mapper round-trip tests. 353 tests green, tsc clean,
wrangler dry-run OK. Docs (CLAUDE.md) synced.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
The aggregate no longer exposes raw config/metadata (intention-revealing
accessors + snapshots), feeds:list is maintained by the repository from
feed.summary(), edit() is the single config mutation, and FeedId now flows
through the application and infrastructure layers.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Address five modeling tensions in one pass:
- Encapsulation: the Feed aggregate no longer exposes raw config/metadata
(a shallow Readonly still leaked mutable arrays). It now offers
intention-revealing accessors that return copies, plus
toConfigSnapshot/toMetadataSnapshot for the repository and summary() for
the global registry.
- feeds:list consistency: FeedRepository.save/saveConfig upsert the registry
entry from feed.summary(), so services no longer mirror title/description/
expiry by hand (the old add/updateInList footgun is gone).
- domain/feed.ts: drop the dead applySenderPolicy, internalise resolveExpiresAt
and trimToByteBudget into the aggregate; feed.ts keeps only the shared
isExpired predicate used by the read-model routes.
- Single edit path: remove editDetails; edit(patch, deps) is the sole config
mutation, with a systematic expired guard. Renaming an expired feed now 403s.
- FeedId flows through the application and infrastructure signatures;
fromTrusted/parse happen once at the edge, .value only at the serialisation
boundaries (urls, feed-generator, feed-keys, logs, JSON).
347 tests green, tsc clean, Worker bundle builds.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Light "collect + dispatch" variant: the Feed aggregate records FeedEvents
(FeedCreated, EmailIngested) on the mutations that have consequences, exposed via
pullEvents(). A new application dispatcher (feed-events.applyFeedEvents) maps
those events to their side effects — counters (awaited) plus WebSub pings and
favicon fetches handed to a BackgroundScheduler. This removes the inline,
scattered side effects from the ingest hot path (email-processor) and from
createFeedRecord; the aggregate is now the source of truth for "what happened".
Side effects with no aggregate mutation (rejected email, feed deletion bypassing
the aggregate, bulk admin ops, the cron, unsubscribes-sent) stay imperative by
design — there is no aggregate event for them to ride on.
BackgroundScheduler type moved to infrastructure/worker.ts (shared). CLAUDE.md
updated. 355 tests pass (+4 event tests); tsc --noEmit clean.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
- Point 3: move the feed/email storage-cleanup helpers (purgeFeedKeysStep,
collectUnsubscribeUrls, purgeExpiredFeeds, deleteKeysWithConcurrency,
deleteAttachmentsForEmails) out of routes/admin/helpers.ts into
src/application/feed-cleanup.ts, so the application layer no longer imports
from routes/. deleteFeedRecord no longer takes a Hono Context: it accepts a
BackgroundScheduler ((task) => void) and the HTTP edge passes
(p) => waitUntilSafe(c, p). Application/domain are now Hono-Context-free.
- Point 6a: rename the misleadingly-named Feed.rename → Feed.editDetails (it
edits title + description), and feed-service.renameFeed → editFeedDetails.
CLAUDE.md source layout updated. 351 tests pass; tsc --noEmit clean.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Make the domain stop depending on infrastructure ("imports point inward").
- Point 2: relocate the four KV adapters (FeedRepository, IconRepository,
WebSubSubscriptionRepository, CountersRepository) from domain/ to
infrastructure/, where the logger import is legitimate. The domain now keeps
only the pure key schema (feed-keys.ts), the Feed aggregate and value objects;
it imports nothing outward. Deliberately no hand-rolled 24-method port
interface (YAGNI without DI) — relocation alone fixes the direction.
- Point 6c: EmailParser.extractFeedId now returns a validated FeedId value
object instead of a raw string, so the most untrusted input (an inbound
recipient address) is guarded at the parse boundary and no longer round-trips
through FeedId.fromTrusted in the ingest path.
All import paths updated; CLAUDE.md source layout/KV-schema notes updated.
351 tests pass; tsc --noEmit clean.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Remove the infrastructure Env leak and ambient time from the domain core, and
model the sender policy as a value object.
- Point 1: Feed.create/edit no longer receive Env. The application layer resolves
the effective lifetime (parsing FEED_TTL_HOURS and applying the server override)
via feed-service.resolveTtlHours and hands the domain a plain ttlHours.
resolveExpiresAt(ttlHours, now) is now pure.
- Point 4: introduce a Clock port (systemClock default), injected at
create/reconstitute. The aggregate uses clock.now() instead of Date.now().
The isExpired edge helper keeps its Date.now() default for routes.
- Point 6b: extract SenderPolicy value object built once from the lists
(decide(senders)) instead of re-parsing per sender; applySenderPolicy is now a
thin wrapper over it.
Coverage moved with the logic: the FEED_TTL_HOURS override is now pinned by
feed-service.test.ts; aggregate tests use an injected fixed clock.
351 tests pass; tsc --noEmit clean.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Refresh the source layout for domain/application/infrastructure, replace
the single-repository rule with the four-repository split + feed-keys,
and add domain rules: the Feed aggregate is the only writer of config +
the email index, and FeedId circulates through domain and repository.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
FeedId is now the type of Feed.id and of every single-feed method on
FeedRepository; callers wrap raw strings via FeedId.fromTrusted at the
repository boundary. String-medium operations (URLs, logs, JSON,
list registry, email keys) stay string. Drop the redundant
generateFeedId wrapper in favour of FeedId.generate.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
The inPlace boolean hid two distinct intentions. Replace it with two
intention-revealing operations backed by Feed.rename (presentational,
never touches expiry) and Feed.edit (full edit, recomputes expiry,
rejects expired). Add FeedRepository.saveConfig so these config-only
edits don't re-write (and risk clobbering) the email index.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Add a Feed aggregate class owning config + the email index, with create,
ingest, removeEmails, isExpired and accepts delegating to the existing
pure invariant functions. FeedRepository gains load/save/saveMetadata
that reconstitute and persist the aggregate.
All write paths now go through it: createFeedRecord (Feed.create),
email ingestion (feed.ingest), and every email deletion in the admin UI
and REST API (feed.removeEmails) — no route mutates metadata.emails
directly anymore. KV key strings unchanged.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
FeedRepository no longer owns favicons, WebSub subscriber lists or the
monitoring counters singleton. Each concern gets its own repository
(IconRepository, WebSubSubscriptionRepository, CountersRepository),
sharing the key schema via feed-keys. KV key strings are unchanged;
counters increment policy stays in utils/stats.ts.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Move the feed:/icon:/websub: key builders out of FeedRepository into a
pure feed-keys module so the wire format lives in one place, shared by
the repositories to come. Strings are byte-identical; behaviour unchanged.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Reflect the refactor: add the src/domain/ tree (feed-repository, feed, value
objects), drop the deleted storage.ts, and update the KV-schema note to point at
FeedRepository as the single key-access layer. Correct the websub key shape and
add the icon: key.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Encapsulate the email/domain/feed-id parsing that was scattered as ad-hoc
regexes and split("@") calls into three small immutable value objects under
src/domain/value-objects/. EmailParser.extractFeedId and generateFeedId now
delegate to FeedId; the sender policy, favicon domain extraction and the admin
SenderField parse through EmailAddress/Domain.
Left as-is on purpose: forwardemail's multi-address free-text extraction and the
admin allow/block list normaliser, which operate on mixed email-or-domain input
that the single-address value objects would reject.
Behaviour-preserving; adds unit tests for each value object.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
processEmail/validateEmail now return an IngestResult discriminated union
({ ok } | { ok: false; reason }) instead of an HTTP Response. The status mapping
moves to the edge (ingestResultToResponse in forwardemail.ts), and the Cloudflare
email handler now logs the rejection reason instead of silently discarding it.
The ingestion core is transport-agnostic. End-to-end status mapping stays covered
by inbound.test.ts (now incl. 410 expired); email-processor.test asserts on the
domain result directly.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Gather the feed's scattered business rules — expiry, sender allow/block policy,
and the email byte-size budget — into one framework-agnostic module. Expiry was
duplicated across feed-service, email-processor and the rss/atom/entries routes;
the sender policy and trim loop lived inline in email-processor. Each now calls
a single function (isExpired, applySenderPolicy, trimToByteBudget,
resolveExpiresAt). Drops the now-unused MAX_METADATA_EMAILS constant.
Behaviour-preserving; adds feed.test.ts covering every invariant.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Centralise the KV key schema and all get/put access behind a FeedRepository
class under src/domain/. Every feed/email/list/icon/websub/counter key was
previously inlined across ~12 modules with two divergent storeEmail and
addFeedToList implementations; the dead src/utils/storage.ts write path is
removed and the email key convention unified on feed:<id>:<ts>.
Behaviour-preserving: existing tests pass unchanged in logic, plus a new
feed-repository.test.ts covering CRUD, key builders, list ops and counters.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Surface the new versioned REST API on the marketing landing, linking to the
live Scalar reference on the demo.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
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 <[email protected]>
Unify the monitoring stats on the versioned API: /api/v1/stats is now public
(no auth) and CORS-enabled, mirroring the legacy /api/stats. The marketing
landing (docs/index.html) now fetches /api/v1/stats; /api/stats is kept as a
deprecated alias for existing monitors. Feed/email routes remain token-gated.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Expose /api/v1/* for feed and email management (feeds CRUD, email
list/get/delete, stats) so the service can be automated without scraping
the admin UI. Built on @hono/zod-openapi; the OpenAPI 3.1 spec is served at
/api/openapi.json with a Scalar reference at /api/docs.
Auth is token-based (Authorization: Bearer <ADMIN_PASSWORD>) plus the
existing reverse-proxy headers — no cookie, no CSRF. Extracted the auth
primitives into src/lib/auth.ts and the feed create/update/delete
orchestration into src/lib/feed-service.ts so the admin UI and the REST API
share a single source of truth.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Add a security policy with private reporting channels and project-specific
scope, plus a contributor guide covering dev setup, testing, and commit
conventions. Drop the stale AGENTS.md reference from CLAUDE.md.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Slim README down to project overview (why, features, architecture,
security) with a short Installation quick-start that links to the new
INSTALL.md. Repoint setup.sh references and CLAUDE.md maintenance list.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>