# HN Reader A lightweight, keyboard-driven Hacker News reader. Navigate like Superhuman/Linear: `j`/`k` to move, `o`/`Enter` to read, `e` to archive, `s` to star, numbers to switch feeds. Feed data is cached in `localStorage` for instant loads, then revalidated in the background. Press `?` in the app for the full shortcut list. ## Stack - **Frontend**: Vite + React + TypeScript, [shadcn/ui](https://ui.shadcn.com) (Radix primitives, Tailwind v4, Nova preset) - **Backend**: Express (`server/`), polls the [HN Firebase API](https://github.com/HackerNews/API) every 5 minutes and caches stories in Postgres - **Database**: Postgres 16 (Docker), schema in `scripts/init.sql` - **Observability**: OpenTelemetry traces/metrics/logs shipped via OTLP/HTTP ## Architecture ``` src/ React SPA (feeds, keyboard nav, comments dialog) server/ Express API + HN sync job + OTel bootstrap scripts/ DB schema, backup script docker-compose.yml Postgres container for this app .gitea/workflows/ CI: lint, typecheck, build ``` The API caches HN's top/new/best/ask/show/job feeds (100 items each) in Postgres so the frontend never talks to Hacker News directly. Per-story read/starred/archived state lives in `story_state` and is returned inline with each feed response. ## Local development ```bash cp .env.example .env # fill in POSTGRES_PASSWORD docker compose up -d # start Postgres npm install npm run dev:server # API on :3012 (loaded from .env) npm run dev # Vite dev server on :3012, proxies /api ``` ## Production Runs as the `app-54d6877c` systemd unit: builds are not automated on deploy, so after pulling changes run `npm run build` then restart the service (`sudo systemctl restart app-54d6877c`). The compiled SPA is served as static files by the same Express process that serves `/api`, on port **3012**. Nightly Postgres backups run via the `app-54d6877c-backup.timer` systemd timer (`scripts/backup.sh`), keeping 7 days of `pg_dump` snapshots in `~/backups`. ## Design system Built with `shadcn/ui` (`components.json`, Radix base, Nova preset). Components live under `src/components/ui` and are added with `npx shadcn@latest add ` — nothing here is hand-rolled, so swapping in a custom registry later (e.g. a `privatecloud` registry) is a matter of updating `components.json`'s `registries` field and re-running `add` against it. ## Observability The API sends traces, metrics, and logs to an OTLP/HTTP collector at `OTEL_EXPORTER_OTLP_ENDPOINT` (defaults to `http://localhost:4318`), instrumenting HTTP, Express, and Postgres calls automatically via `@opentelemetry/auto-instrumentations-node`. View it in Grafana at `localhost:3400`.