diff --git a/TODO.md b/TODO.md index 2e941bc..1eb5f39 100644 --- a/TODO.md +++ b/TODO.md @@ -72,6 +72,8 @@ Ideas from competitors (Feedbin, Readwise Reader, Inoreader, Omnivore, LetterFee - [x] `P2·S` **JSON Feed endpoint** `GET /json/:feedId` **[differentiating, cheap]** — the `feed` lib's `.json1()` (emits JSON Feed v1) wired via `generateJsonFeed` in `src/infrastructure/feed-generator.ts`, served at `/json/:feedId` (`src/routes/json.ts`) with `Content-Type: application/feed+json` + WebSub hub `Link`. All three formats cross-link via `feedLinks`. Natively consumed by NetNewsWire, Reeder, NewsBlur, Feedly. — _origin: [JSON Feed 1.1 spec](https://www.jsonfeed.org/version/1.1/) (reader ecosystem)_ +- [ ] `P3·S` **Upgrade JSON Feed output to v1.1** **[correctness, niche]** — our `/json/:feedId` emits `version: "https://jsonfeed.org/version/1"` because the `feed` lib's `.json1()` only implements v1, and the upstream request to bump it was **closed as _not planned_** ([jpmonette/feed#139](https://github.com/jpmonette/feed/issues/139)). So a true v1.1 feed needs a small post-process pass on the `.json1()` object in `generateJsonFeed` (`src/infrastructure/feed-generator.ts`): set `version` to `https://jsonfeed.org/version/1.1`, and apply the [v1.1 changes](https://www.jsonfeed.org/version/1.1/#changes-a-name-changes-a) — promote the deprecated top-level/item `author` to `authors` (array), and add the top-level `language` field. Low value (every reader still parses v1) but cheap and removes a spec-compliance footnote. — _origin: [jpmonette/feed#139 (closed, not planned)](https://github.com/jpmonette/feed/issues/139); [JSON Feed 1.1 spec](https://www.jsonfeed.org/version/1.1/)_ + - [ ] `P2·M` **Per-item `` + per-feed tags/categories** **[differentiating]** — we set no categories today. Tag entries by sender (or a user-set feed category) so readers (Inoreader, Feedly, NewsBlur) can filter/mute subsets. Pairs with the filtering item below; touches `FeedState`, `feed-generator.ts`. — _origin: [RSS best practices (kevincox)](https://kevincox.ca/2022/05/06/rss-feed-best-practices/); Inoreader/Feedly filtering_ - [ ] `P3·S` **Reader cadence hints: `` + `sy:updatePeriod`/`sy:updateFrequency`** **[table-stakes, niche]** — advertise the feed's real update rhythm so pollers (FreshRSS, Miniflux, Inoreader) back off; complements our WebSub push. Support is uneven, so keep it as a hint alongside WebSub. Also advertise the WebSub hub link _inside_ the XML (``), not only the HTTP `Link` header. — _origin: [FreshRSS TTL #6721](https://github.com/FreshRSS/FreshRSS/issues/6721)_ @@ -178,6 +180,16 @@ Breakdown of the _"Per-feed favicon from the last sender's domain"_ item above ( - [x] `P2·S` **Failure handling** — missing/blocked favicons degrade gracefully to the project favicon fallback (negative cache entry); icon fetch errors never surface to ingestion or feed rendering. +## Operability, versioning & ecosystem (2026-05-24) + +Self-host operational quality-of-life: knowing which version you run, when to update, and how many people run KTN. + +- [ ] `P3·S` **Display the running version** **[table-stakes, easy]** — surface the deployed app version (from `package.json` `version`, currently `0.2.1`) somewhere visible: the admin UI footer and/or the public status page (`src/routes/home.tsx`), and ideally the `/health` JSON. Bundle the version at build time (inline the `package.json` version into the Worker, since there's no filesystem at runtime) and render it. Foundation for the update-notification item below. — _origin: internal_ + +- [ ] `P3·M` **Notify when an update is available** **[differentiating for self-hosters]** — compare the running version against the latest GitHub Release tag and show a discreet "update available → vX.Y.Z" banner in the admin UI when behind. Fetch `https://api.github.com/repos///releases/latest` (cache aggressively — Cache API / KV with a long TTL — to respect GitHub rate limits and avoid a call per page load), compare semver against the bundled version. Depends on the "display version" item. Keep it opt-out-able (it makes one outbound call). — _origin: internal_ + +- [ ] `P3·L` **Public instances directory and/or instance counter (opt-in telemetry)** **[differentiating, ecosystem]** — let a self-hosted instance optionally announce itself to a central registry so we can show a count of live instances (and, if the operator opts in to being listed, a public directory of instances). Each instance periodically pings a central endpoint (on the existing cron) with minimal, **opt-in** data (e.g. an anonymous instance id + version; a public listing would additionally need a name/URL the operator explicitly provides). ⚠ Privacy-first: **off by default**, clearly documented, no PII/feed data ever sent; respect "count me but don't list me". Needs a central collector (a separate tiny Worker + KV/DO) plus an `INSTANCE_TELEMETRY`/`INSTANCE_DIRECTORY` opt-in env on the client side, fired from `index.ts`'s `scheduled` handler. — _origin: internal_ + ## Epic: Pluggable runtime, storage & ingestion (off-Cloudflare support) `P2·XL` **Run KTN off Cloudflare from one codebase, adapter-selected by config.** Reference non-CF target: **Clever Cloud** (container + Cellar S3 + a KV/SQL add-on) with **Sweego** inbound for email. — _origin: internal (broader audience / reduced lock-in)_