diff --git a/CHANGELOG.md b/CHANGELOG.md index b573d1c..8182114 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to this project will be documented in this file. +## Beta 2 (v0.2.0) +- Web UI overhaul: fixed header, single-scroll layout, auto pin-to-bottom, infinite scroll history +- Channel selector moved into menubar brand; commit hash shown subtly +- SSE realtime updates and JSON APIs for info, channels, tail, history, stream +- Link cards: robust OG/Twitter parsing, X (Twitter) oEmbed embeds, YouTube oEmbed, direct image previews +- On-demand link summaries with caching; inline actions (🌚/🌝, β–Ύ) and collapse/expand +- Dedicated link-only LLM prompt; improved fetch headers; readability extraction; image-to-vision support +- Mention detection/rate limiting/quiet hours retained and refined +- Store: case-insensitive channel queries, msgid de-dup, WAL, history paging +- Metrics: messages ingested, notifications sent, pruned, connection gauge +- Docker: multi-arch build, healthcheck; version/commit/build injected +- Numerous bug fixes (template rendering, static assets path, scroll duplicates, initial bottom snap) + ## Beta 1 (initial release) - soju-specific raw connector with event playback and CHATHISTORY fallback - Message storage (SQLite, WAL), msgid de-dup, retention job diff --git a/README.md b/README.md index 8ed4575..05587a2 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ An IRC bouncer companion service for soju that: - Notifies you on mentions via Pushover (default) - Stores messages in SQLite for summaries and on-demand inspection - Generates AI digests (OpenAI by default) on schedule or on demand -- Exposes a small HTTP API for health, tailing messages, metrics, and triggering digests +- Exposes a small HTTP API and a minimal Web UI (Pico.css) for status, tail, history, link cards, and on-demand summaries Note: this is not a bot and never replies in IRC. It passively attaches as a soju multi-client on your main account. @@ -18,20 +18,20 @@ If you use soju as a bouncer, you may want per-client alerts and AI summaries wi - Language: Go (single static binary, low memory footprint) - Long-lived IRC client: raw IRC using a lightweight parser (sorcix/irc) with an irssi-style handshake tailored for soju -- Message storage: SQLite via modernc.org/sqlite +- Message storage: SQLite via modernc.org/sqlite (WAL enabled) - Scheduling: github.com/robfig/cron/v3 - Notifications: github.com/gregdel/pushover - Summarization (LLM): github.com/sashabaranov/go-openai -- HTTP API: Go stdlib `net/http` +- HTTP API + Web UI: Go stdlib `net/http` + `html/template` + embedded static assets Runtime modules: - `internal/soju`: soju connection, capability negotiation, irssi-style PASS/USER auth, joins, message ingestion, event playback, CHATHISTORY fallback - `internal/store`: SQLite schema and queries - `internal/notifier`: Pushover notifier (pluggable interface) -- `internal/summarizer`: OpenAI client with GPT-5 defaults, GPT-4o-mini fallback +- `internal/summarizer`: OpenAI client with GPT-5 defaults, GPT-4o-mini fallback; separate link-summarization prompt - `internal/scheduler`: cron-based digest scheduling and daily retention job -- `internal/httpapi`: `/healthz`, `/ready`, `/tail`, `/trigger`, `/metrics` +- `internal/httpapi`: `/healthz`, `/ready`, `/tail`, `/trigger`, `/metrics`, Web UI and JSON APIs - `internal/config`: env config loader and helpers ## Features @@ -41,7 +41,11 @@ Runtime modules: - AI digest generation: concise natural summaries (no rigid sections); integrates pasted multi-line posts and referenced link context; image links sent to GPT‑5 as vision inputs - Configurable schedules (cron), quiet hours, and summary parameters - Local persistence with retention pruning (daily at 03:00) -- HTTP endpoints: health, tail, metrics, on-demand digests +- Web UI with: + - Realtime chat tail via SSE; auto-scroll to bottom; preload older history with infinite scroll-up + - Link cards with OG/Twitter metadata (X posts via oEmbed), YouTube oEmbed embeds, direct image previews + - Inline on-demand link summarization with caching (24h), and a single summarize toggle (🌚/🌝) + - Channel selector in the menubar, login interstitial using `HTTP_TOKEN` ## How it works @@ -60,20 +64,21 @@ Runtime modules: 4) Messages and mentions: - Each `PRIVMSG` is stored with server-time when available - Mentions trigger Pushover notifications subject to quiet hours, urgency, and rate limits - - Debug logs include: mention delivered or suppression reason (backfill, quiet hours, rate limit) 5) Summarization: - - `/trigger` or the scheduler loads a window and calls OpenAI - - GPT‑5 context: ~272k input tokens + up to 128k output tokens (400k total) - - Summaries are concise/natural and integrate multi-line posts, article text (readability-extracted), and image links (vision) + - Digests: `/trigger` or the scheduler loads a window and calls OpenAI with a conversation-focused prompt + - Link summaries: dedicated prompt that ignores chat context; fetches page content with readability; includes oEmbed hints for YouTube and X; passes images to vision models -6) HTTP API: +6) HTTP + JSON API: - `/healthz` β†’ `200 ok` - `/ready` β†’ `200` only when connected to soju - - `/tail?channel=#chan&limit=N` β†’ plaintext tail (chronological) - - `/trigger?channel=#chan&window=6h` β†’ returns digest and sends via notifier + - `/tail?channel=#chan&limit=N` β†’ JSON tail for UI + - `/history?channel=#chan&before=&limit=N` β†’ JSON older messages (infinite scroll) + - `/trigger?channel=#chan&window=6h` β†’ returns digest JSON and (optionally) pushes via notifier + - `/linkcard?url=...` β†’ card JSON (title/desc/image or embed HTML) + - `/linksummary?url=...` β†’ brief AI summary of a single URL (cached 24h) - `/metrics` β†’ Prometheus text metrics - - Protect `/tail` and `/trigger` with `HTTP_TOKEN` via Bearer, `token` query, `X-Auth-Token`, or basic auth (`token:`) + - Protect UI + JSON with `HTTP_TOKEN` cookie; APIs also allow Bearer/query token ## Health and readiness @@ -222,7 +227,7 @@ Compose (with localhost bind suitable for Synology reverse proxy): ```yaml services: sojuboy: - image: code.cravey.net/your-user/sojuboy:v0.1.0-beta1 + image: code.cravey.net/your-user/sojuboy:v0.2.0-beta2 restart: unless-stopped env_file: .env ports: @@ -241,7 +246,7 @@ services: ```yaml services: sojuboy: - image: code.cravey.net/your-user/sojuboy:v0.1.0-beta1 + image: code.cravey.net/your-user/sojuboy:v0.2.0-beta2 restart: unless-stopped ports: - "127.0.0.1:8080:8080" # bind only to localhost; fronted by DSM Reverse Proxy @@ -316,16 +321,16 @@ services: | OPENAI_API_KEY | (empty) | | OPENAI_BASE_URL | (empty) | | OPENAI_MODEL | gpt-5 | -| OPENAI_MAX_TOKENS | 700 | +| OPENAI_MAX_TOKENS | 128000 | | SUMM_FOLLOW_LINKS | true | -| SUMM_LINK_TIMEOUT | 6s | -| SUMM_LINK_MAX_BYTES | 262144 | -| SUMM_GROUP_WINDOW | 90s | -| SUMM_MAX_LINKS | 5 | +| SUMM_LINK_TIMEOUT | 20s | +| SUMM_LINK_MAX_BYTES | 1048576 | +| SUMM_GROUP_WINDOW | 120s | +| SUMM_MAX_LINKS | 20 | | SUMM_MAX_GROUPS | 0 | -| SUMM_TIMEOUT | 5m | +| SUMM_TIMEOUT | 10m | | DIGEST_CRON | 0 */6 * * * | -| DIGEST_WINDOW | 6h | +| DIGEST_WINDOW | 24h | | QUIET_HOURS | (empty) | | NOTIFY_BACKFILL | false | | MENTION_MIN_INTERVAL | 30s | @@ -335,7 +340,7 @@ services: | HTTP_LISTEN | :8080 | | HTTP_TOKEN | (empty) | | STORE_PATH | /data/app.db | -| STORE_RETENTION_DAYS | 7 | +| STORE_RETENTION_DAYS | 365 | | LOG_LEVEL | info | ## Pushover setup @@ -355,25 +360,22 @@ services: ## HTTP API - `GET /healthz` β†’ `200 ok` -- `GET /tail?channel=%23chan&limit=50` - - Returns plaintext messages (chronological) - - Auth: provide `HTTP_TOKEN` as a Bearer token (or query param `token=`) -- `GET /trigger?channel=%23chan&window=6h` - - Returns plaintext digest - - Also sends via notifier when configured - - Auth as above +- `GET /tail?channel=%23chan&limit=50` (JSON) +- `GET /history?channel=%23chan&before=&limit=50` (JSON) +- `GET /trigger?channel=%23chan&window=6h` (JSON) +- `GET /linkcard?url=…` (JSON) +- `GET /linksummary?url=…` (JSON) - `GET /metrics` - - Prometheus metrics: `sojuboy_messages_ingested_total`, `sojuboy_notifications_sent_total`, `sojuboy_messages_pruned_total`, `sojuboy_connected` ## Troubleshooting - Empty tail while there’s activity - - Ensure the service logs `join requested:` followed by `joined` for your channels + - Ensure the service logs readiness and joins for your channels - Confirm `.env` `CHANNELS` contains your channels - - Check for `/metrics` and logs for recent message ingestion + - Check `/metrics` and logs for recent message ingestion -- 401 Unauthorized from `/tail` or `/trigger` - - Provide `Authorization: Bearer $HTTP_TOKEN` or `?token=$HTTP_TOKEN` +- 401 Unauthorized from UI/API + - Log in at `/login` with `HTTP_TOKEN`, or pass it via Bearer/`token=` - OpenAI 502/URL errors - Ensure `OPENAI_BASE_URL=https://api.openai.com/v1` @@ -394,7 +396,7 @@ Project layout (selected): - `internal/store` – SQLite schema and queries - `internal/notifier` – Pushover notifier - `internal/summarizer` – OpenAI client and prompts -- `internal/httpapi` – health, tail, trigger, metrics endpoints +- `internal/httpapi` – UI and endpoints - `internal/scheduler` – cron jobs Go toolchain: see `go.mod` (Go 1.23), Dockerfile builds static binary for a distroless image. diff --git a/internal/summarizer/openai.go b/internal/summarizer/openai.go index 6374eb8..0fc5d10 100644 --- a/internal/summarizer/openai.go +++ b/internal/summarizer/openai.go @@ -294,11 +294,17 @@ func (o *OpenAI) SummarizeLink(ctx context.Context, rawURL string) (string, erro }, MaxCompletionTokens: o.maxTokens, } - if !reasoningLike { req.Temperature = 0.2 } + if !reasoningLike { + req.Temperature = 0.2 + } resp, err := client.CreateChatCompletion(ctx, req) - if err != nil { return "", err } - if len(resp.Choices) == 0 { return "", nil } + if err != nil { + return "", err + } + if len(resp.Choices) == 0 { + return "", nil + } return strings.TrimSpace(resp.Choices[0].Message.Content), nil }