|
| 1 | +# Architecture |
| 2 | + |
| 3 | +## Packages |
| 4 | + |
| 5 | +| Package | Responsibility | |
| 6 | +| --- | --- | |
| 7 | +| [`cmd/feedreader`](../cmd/feedreader) | CLI entrypoint: `serve`, `fetch`, `healthcheck` subcommands. | |
| 8 | +| [`internal/config`](../internal/config) | Env-var configuration loading and defaults. | |
| 9 | +| [`internal/db`](../internal/db) | SQLite bootstrap: schema DDL, WAL/pragma setup. | |
| 10 | +| [`internal/domain`](../internal/domain) | Plain data types shared across packages (`FeedItem`, `SyncState`, `CardView`, ...). | |
| 11 | +| [`internal/repository`](../internal/repository) | Persistence: upserts, sync-state tracking, feed queries. | |
| 12 | +| [`internal/service`](../internal/service) | Refresh orchestration, scheduler, card-building/display logic. | |
| 13 | +| [`internal/sources`](../internal/sources) | Upstream source adapters (Hacker News, GitHub Trending, Hugging Face Papers, alphaXiv). | |
| 14 | +| [`internal/web`](../internal/web) | HTTP routes, SSR page rendering, JSON APIs. | |
| 15 | + |
| 16 | +## Data model and refresh behavior |
| 17 | + |
| 18 | +- Items are upserted by `(source, external_id)`. A refresh never deletes |
| 19 | + existing rows — a failed fetch just records the failure in `sync_state` and |
| 20 | + leaves prior data in place. |
| 21 | +- The original `published_at` is preserved across re-fetches via |
| 22 | + `coalesce(items.published_at, excluded.published_at)` — a source that later |
| 23 | + starts reporting a different date for the same item doesn't reorder it. |
| 24 | +- `internal/repository/sqlite.go`'s `ListFeedItems` sorts and paginates **in |
| 25 | + application memory**, not in SQL. This is intentional: total item count |
| 26 | + across all 4 sources is small (a few hundred rows), so the simplicity of one |
| 27 | + in-memory comparator outweighs the complexity of expressing the same |
| 28 | + fallback-ordering (published date, else first-seen date, else source rank) |
| 29 | + in SQL. |
| 30 | +- The scheduler in `internal/service/service.go` wakes on N-hour wall-clock |
| 31 | + boundaries in `Asia/Ho_Chi_Minh` (UTC+7, no DST) — default hourly via |
| 32 | + `FEEDREADER_REFRESH_INTERVAL_HOURS`. It does not refresh immediately on |
| 33 | + startup. |
| 34 | + |
| 35 | +## Frontend |
| 36 | + |
| 37 | +Server-rendered HTML (`web/templates/index.html`) plus vanilla JS |
| 38 | +(`web/static/app.js`) — no frontend build step. The service worker |
| 39 | +(`web/static/service-worker.js`) caches the app shell and visited |
| 40 | +`/api/items` responses for offline reuse. |
0 commit comments