Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ HTML2RSS_ACCESS_TOKEN=
# Keep enabled to allow pasting any webpage URL to generate an RSS feed.
AUTO_SOURCE_ENABLED=true

# OPTIONAL: Config refinement studio (off in production unless set to true).
# Exposes POST /api/v1/feeds/validate, /preview and /suggest_selectors. All three
# require HTML2RSS_ACCESS_TOKEN; preview and suggest_selectors also fetch the
# submitted URL live and need AUTO_SOURCE_ENABLED=true.
# STUDIO_ENABLED=false

# COMPANION SCRAPER:
# URL of the companion Botasaurus scraper API service in docker-compose.
BOTASAURUS_SCRAPER_URL=http://botasaurus:4010
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,4 @@ frontend/.astro

# Local agent artifacts
/.agents/
.agents/plan
59 changes: 45 additions & 14 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,19 +58,50 @@ See [docs/design-system.md](docs/design-system.md) for visual rules.

Public feed-directory metadata for embedded and local configs.

| Item | Detail |
| --- | --- |
| Endpoint | `GET /api/v1/configs` |
| Flag | `CONFIG_CATALOG_ENABLED` (default `true`; set `false` to disable) |
| Disabled response | `404` with `{ "error": "catalog_disabled" }` |
| Embedded entries | `Html2rss::Configs::Catalog.entries` — do not re-walk YAML in the handler |
| Local entries | `Catalog::Merge` includes `feeds.yml` feeds only when `directory.title` is set |
| Starter feeds (UI) | Server `meta.starters` via `Catalog::Starters.pick`; frontend `selectStarterFeeds(entries, starters)` |
| Catalog find | `findCatalogEntries` → multi-hit list under create URL; demotes `empty`/`error`; links via `catalogFeedHref` |
| last_result | Required on every catalog entry (`ok`/`empty`/`error`/`unknown`); process-local `Feeds::LastResults` |
| catalog_version | `2` (clients fail-closed on other versions) |
| CORS | Route-scoped on `/api/v1/configs` only (`GET`, `OPTIONS`) |
| Root metadata | `GET /api/v1/` exposes `instance.catalog: { enabled, url }` |
| Contract SSOT | Request specs under `spec/html2rss/web/api/v1_spec.rb` and generated `public/openapi.yaml` |
| Item | Detail |
| ------------------ | ------------------------------------------------------------------------------------------------------------ |
| Endpoint | `GET /api/v1/configs` |
| Flag | `CONFIG_CATALOG_ENABLED` (default `true`; set `false` to disable) |
| Disabled response | `404` with `{ "error": "catalog_disabled" }` |
| Embedded entries | `Html2rss::Configs::Catalog.entries` — do not re-walk YAML in the handler |
| Local entries | `Catalog::Merge` includes `feeds.yml` feeds only when `directory.title` is set |
| Starter feeds (UI) | Server `meta.starters` via `Catalog::Starters.pick`; frontend `selectStarterFeeds(entries, starters)` |
| Catalog find | `findCatalogEntries` → multi-hit list under create URL; demotes `empty`/`error`; links via `catalogFeedHref` |
| last_result | Required on every catalog entry (`ok`/`empty`/`error`/`unknown`); process-local `Feeds::LastResults` |
| catalog_version | `2` (clients fail-closed on other versions) |
| CORS | Route-scoped on `/api/v1/configs` only (`GET`, `OPTIONS`) |
| Root metadata | `GET /api/v1/` exposes `instance.catalog: { enabled, url }` and `instance.studio: { enabled }` |
| Contract SSOT | Request specs under `spec/html2rss/web/api/v1_spec.rb` and generated `public/openapi.yaml` |

After handler or envelope changes: `make openapi` and `make ci-ready`.

## Config studio API

Token-holder refinement of the selectors subtree, then generate via create.

| Item | Detail |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Endpoints | `POST /api/v1/feeds/validate`, `POST /api/v1/feeds/preview`, `POST /api/v1/feeds/suggest_selectors` |
| Create | `POST /api/v1/feeds` accepts optional `selectors` (same access token and flag as validate; URL-only create is unaffected by the flag) |
| Flag | `STUDIO_ENABLED` (development/test default `true`; otherwise `false`); advertised as `instance.studio.enabled` on `GET /api/v1` |
| Disabled response | `403` when the flag is off |
| Auth | Access token required on validate, preview, suggest, and create |
| Live fetch | Preview and suggest also need `AUTO_SOURCE_ENABLED` |
| Frontend | Successful results refine inline on `#/result/:token`. There is no `#/refine` route. |
| Contract SSOT | Request specs under `spec/html2rss/web/api/v1_spec.rb` and generated `public/openapi.yaml` |

After handler or envelope changes: `make openapi` and `make ci-ready`.

`SelectorsDocument` is `Openapi::JsonSchema` output from `Html2rss::Config::Schema.json_schema` at generate time (full gem selectors subtree). Runtime ownership is `Html2rss::Web::SelectorsDocument`. The SPA refine draft uses `StudioSelectors` / `StudioDraft` (items, enhance, title, url, published_at only). The published document is OpenAPI 3.1. Preview sample, selector candidate, and validation-issue stamps stay runtime-owned in `Openapi::Document`.

## Generated OpenAPI SPA transport

The frontend JSON API is the generated Hey API client, not hand-written `fetch`.

| Item | Detail |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Regen | `make openapi-client` (`frontend/openapi-ts.config.ts`). Never hand-edit `frontend/src/api/generated/**` |
| Value imports | Only `frontend/src/api/http/*.ts` and `src/__tests__/apiHttp*.test.ts`. Type-only imports are allowed elsewhere |
| Layers | Generated owns wire. Adapters in `frontend/src/api/http/` own transport (`unwrap` → `{ ok, status, body }`). Parsers own domain types |
| baseUrl | `/api/v1` same-origin. Do not change public OpenAPI `servers` or `RSpec::OpenAPI.servers` |
| Preview | JSON Feed stays `fetch(json_public_url)`. Do not use `renderFeedByToken` |
11 changes: 10 additions & 1 deletion CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,17 @@ On create, `findCatalogEntries` matches the typed query against the catalog (URL
### CreateEntry remount
Visiting create (including hashbang `#!/…` → `#/…`) bumps `createEntryKey` so the create surface remounts. Remount alone does not auto-submit; auto-submit requires `prefillUrl`.

### Config Studio
Selector refinement for a token holder when studio is enabled. One result frame has two phases: **unresolved** (create returned `EXTRACTION_EMPTY`, studio on, access token present — in-memory `#/result` with no feed token; the flat editor mounts immediately; **Save and generate feed** is primary; wire `Decision#message` is the notice) and **ready** (`#/result/:token` after items mint — **Copy feed URL** primary; flat editor mounts with zero clicks to reveal; **Save and generate feed** stays demoted until the draft changes; one meadow; live studio output replaces the result preview after an edit). Studio off or no token keeps empty extraction on create. The listing URL stays the subject; **Create another feed** starts a new listing. The visual builder and YAML are one document: validate exports `channel.url` plus selectors. A successful validate echo keeps the server’s items selector and enhance flag; the client keeps the field selectors it authored. Only `studioService` unwraps the studio envelope and maps ranked `candidates` (items / title / link / published) into domain suggestion buckets. Candidates never auto-apply. Suggestion outcomes are explicit: loading, ready, valid-empty, and failed+retry stay non-blocking while the editor remains editable. Optional Title / Link / Published fields appear only for selector, candidate, issue, or manual evidence; one **Add …** activator reveals dormant fields. Chips add or remove selector parts on the authored comma-joined string; suggest does not write that string, and chips do not change Enhance. Field extractors remain fixed by the draft mapper. Save posts studio selectors through Creation IO and stays on the result view (unresolved → ready when items mint). On ready, demoted **Open Feed Directory issue** opens a prefilled html2rss-configs GitHub issue (no tokens); it is a handoff for Riley, not SPA directory authoring. There is no `#/refine` route and no collapsed Refine disclosure.

### SelectorsDocument
The full gem `selectors` subtree a token holder may attach on feed creation after the root-key gate (`CLIENT_ROOT_KEYS`) and gem schema validate. Runtime type `Html2rss::Web::SelectorsDocument`; OpenAPI component `SelectorsDocument`. Signed into the feed token and resolved on later serves. Not a full config: channel, headers, and strategy stay denied by the root-key gate (not a second web shape allowlist).

### StudioSelectors
SPA studio outbound / draft projection only: `items` (selector + optional enhance), `title`, `url`, `published_at`. Hydrate fails closed when the document carries keys outside that subset (e.g. `order`, `pagination`, `guid`). Not the full `SelectorsDocument`.

### Unmatched result
A result route whose in-memory created feed token does not match the route `feedToken`. Feed Flow replaces to create without prefill (no second journey decide elsewhere).
A result route without matching in-memory workspace: ready requires a created feed whose token matches (or leads) the route `feedToken`; unresolved requires in-memory empty-extraction workspace on bare `#/result`. Feed Flow replaces to create without prefill (no second journey decide elsewhere).

### Renderer
Backend feed HTTP assembly: `Feeds::Renderer` owns the HTTP envelope and success serialization, and orchestrates `Feeds::FormatNegotiation` (Accept/path negotiation; `FormatNegotiation::MediaRange` for Accept scoring). Empty FEED bodies dump `ErrorClassifier::Decision#message`.
Expand Down
3 changes: 1 addition & 2 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ source 'https://rubygems.org'

git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }

gem 'html2rss', '~> 0.30'
# gem 'html2rss', github: 'html2rss/html2rss', branch: 'master'
gem 'html2rss', github: 'html2rss/html2rss', branch: :master
gem 'html2rss-configs', github: 'html2rss/html2rss-configs'

# Use these instead of the two above (uncomment them) when developing locally:
Expand Down
Loading
Loading