Add read-only web UI

Serves Pete's classified-story archive over HTTP alongside the Matrix
bot. Three sections (gaming/tech/politics), Animal-Crossing-vibe Tailwind
templates, day/night palette driven by the visitor's browser clock.
Web port configurable via web.listen_addr and ${PETE_WEB_PORT} in
docker-compose. Tailwind built in a node stage in the Dockerfile so
deployments don't need node at runtime.
This commit is contained in:
prosolis
2026-05-24 21:51:49 -07:00
parent 87906719fa
commit fbd4b84eaf
20 changed files with 1695 additions and 1 deletions

View File

@@ -270,6 +270,41 @@ func GetNewestPostableStoryByChannel(channel string) (*Story, error) {
return &s, nil
}
// ListClassifiedByChannel returns up to `limit` classified stories routed to a
// real channel, newest first, with optional offset for pagination. Sentinel
// channels (_discarded, _duplicate) are excluded.
func ListClassifiedByChannel(channel string, limit, offset int) ([]Story, error) {
rows, err := Get().Query(
`SELECT guid, headline, lede, image_url, article_url, source, feed_hint, platforms, channel, seen_at
FROM stories
WHERE classified = 1
AND channel = ?
ORDER BY seen_at DESC
LIMIT ? OFFSET ?`, channel, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
var out []Story
for rows.Next() {
var s Story
if err := rows.Scan(&s.GUID, &s.Headline, &s.Lede, &s.ImageURL, &s.ArticleURL, &s.Source, &s.FeedHint, &s.Platforms, &s.Channel, &s.SeenAt); err != nil {
return nil, err
}
out = append(out, s)
}
return out, rows.Err()
}
// CountClassifiedByChannel returns how many classified stories exist for a channel.
func CountClassifiedByChannel(channel string) (int, error) {
var n int
err := Get().QueryRow(
`SELECT COUNT(*) FROM stories WHERE classified = 1 AND channel = ?`,
channel).Scan(&n)
return n, err
}
// GetRoundRobinState returns the last channel posted by the round-robin
// scheduler and the timestamp of that tick. Empty string + 0 if no state yet.
func GetRoundRobinState() (lastChannel string, lastTickAt int64, err error) {