Local mode, Makefile, image fixes, Pete avatar

- Add -local flag: web/RSS-only mode that skips Matrix login and posting,
  so the web UI can be exercised against live feeds without credentials
- Add Makefile (build/local/seed/test/clean) that handles Tailwind +
  go build in one shot
- Fix Guardian thumbnails: NormalizeImageURL was rewriting width=1200
  onto signed i.guim.co.uk URLs, invalidating the s= signature and
  returning 401. Leave signed URLs alone and pick the widest
  media:content variant up front instead
- Use pete.avif as the header logo, favicon, and footer mark; drop the
  unused leaf.svg
This commit is contained in:
prosolis
2026-05-24 22:43:03 -07:00
parent 9d5db63c56
commit 0111a1b06d
10 changed files with 115 additions and 16 deletions

48
main.go
View File

@@ -26,6 +26,7 @@ func main() {
seed := flag.Bool("seed", false, "ingest current feed items as seen without posting, then exit")
test := flag.Bool("test", false, "post one story to verify the full pipeline, then exit")
testSource := flag.String("test-source", "", "source name to use for -test (default: first enabled)")
local := flag.Bool("local", false, "web/RSS-only mode: poll feeds and serve the web UI, no Matrix login or posting")
flag.Parse()
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
@@ -62,6 +63,12 @@ func main() {
return
}
// Local mode: poll feeds + serve web UI only. No Matrix, no posting queue.
if *local {
runLocal(cfg)
return
}
// Create Matrix client
mx, err := matrix.New(cfg.Matrix)
if err != nil {
@@ -209,6 +216,47 @@ func main() {
slog.Info("pete stopped")
}
func runLocal(cfg *config.Config) {
cfg.Web.Enabled = true
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
processItem := func(_ context.Context, item *ingestion.FeedItem) {
if item.DirectRoute == "" {
slog.Info("local: item has no direct_route, discarding", "guid", item.GUID, "source", item.Source)
storage.MarkClassified(item.GUID, "_discarded", "[]")
return
}
storage.MarkClassified(item.GUID, item.DirectRoute, "[]")
slog.Info("local: story classified",
"guid", item.GUID, "source", item.Source, "channel", item.DirectRoute, "headline", item.Headline)
}
poller := ingestion.NewPoller(cfg.Sources, processItem)
poller.Start(ctx)
slog.Info("local: pollers started")
ws, err := web.New(cfg.Web)
if err != nil {
slog.Error("local: web server init failed", "err", err)
os.Exit(1)
}
go ws.Start(ctx)
slog.Info("local: web UI listening", "addr", cfg.Web.ListenAddr)
storage.RunMaintenance()
sig := <-sigCh
slog.Info("local: shutdown signal received", "signal", sig)
cancel()
poller.Wait()
slog.Info("pete stopped")
}
func runSeed(cfg *config.Config) {
total := 0
for _, src := range cfg.Sources {