Build hn-reader: keyboard-driven Hacker News reader
CI / build (push) Successful in 2m0s

Vite/React + shadcn/ui frontend with Superhuman-style keyboard nav
(j/k/o/v/e/s/u, feed switching, localStorage caching for instant
loads) backed by an Express API that polls the HN Firebase API into
Postgres and serves cached feeds/comments. Instrumented with
OpenTelemetry (traces/metrics/logs via OTLP/HTTP).
This commit is contained in:
privatecloud builder
2026-07-25 12:16:08 +00:00
commit 95bef8bf7b
47 changed files with 12198 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
# Nightly Postgres backup for the HN Reader app. Invoked by app-54d6877c-backup.timer.
set -euo pipefail
BACKUP_DIR="/home/exedev/backups"
RETENTION_DAYS=7
TIMESTAMP="$(date -u +%Y%m%dT%H%M%SZ)"
FILE="$BACKUP_DIR/hn-reader-$TIMESTAMP.sql.gz"
mkdir -p "$BACKUP_DIR"
docker exec hn-reader-postgres pg_dump -U hnreader -d hnreader | gzip > "$FILE"
find "$BACKUP_DIR" -name 'hn-reader-*.sql.gz' -mtime +"$RETENTION_DAYS" -delete
echo "Backed up hn-reader database to $FILE"
+38
View File
@@ -0,0 +1,38 @@
-- Schema for hn-reader: cached Hacker News stories + local read state.
-- Single-user app: no user table, state is keyed directly by story id.
CREATE TABLE IF NOT EXISTS stories (
id BIGINT PRIMARY KEY, -- Hacker News item id
type TEXT NOT NULL DEFAULT 'story',
title TEXT NOT NULL,
url TEXT,
domain TEXT,
by_user TEXT,
score INTEGER NOT NULL DEFAULT 0,
descendants INTEGER NOT NULL DEFAULT 0, -- comment count
story_text TEXT, -- self-text for Ask HN / Show HN
hn_created_at TIMESTAMPTZ NOT NULL,
fetched_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Which feed(s) a story currently ranks in, and at what rank, so the
-- frontend can render top/new/best/ask/show/job lists from one table.
CREATE TABLE IF NOT EXISTS feed_items (
feed TEXT NOT NULL, -- top | new | best | ask | show | job
rank INTEGER NOT NULL,
story_id BIGINT NOT NULL REFERENCES stories(id) ON DELETE CASCADE,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (feed, rank)
);
CREATE INDEX IF NOT EXISTS feed_items_story_idx ON feed_items(story_id);
-- Per-story local state: read/starred/archived, superhuman-style.
CREATE TABLE IF NOT EXISTS story_state (
story_id BIGINT PRIMARY KEY REFERENCES stories(id) ON DELETE CASCADE,
read BOOLEAN NOT NULL DEFAULT false,
starred BOOLEAN NOT NULL DEFAULT false,
archived BOOLEAN NOT NULL DEFAULT false,
read_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);