MVP: IRC envelope format, long-polling, per-client queues, SPA rewrite

Major changes:
- Consolidated schema into single migration with IRC envelope format
- Messages table stores command/from/to/body(JSON)/meta(JSON) per spec
- Per-client delivery queues (client_queues table) with fan-out
- In-memory broker for long-poll notifications (no busy polling)
- GET /messages supports ?after=<queue_id>&timeout=15 long-polling
- All commands (JOIN/PART/NICK/TOPIC/QUIT/PING) broadcast events
- Channels are ephemeral (deleted when last member leaves)
- PRIVMSG to nicks (DMs) fan out to both sender and recipient
- SPA rewritten in vanilla JS (no build step needed):
  - Long-poll via recursive fetch (not setInterval)
  - IRC envelope parsing with system message display
  - /nick, /join, /part, /msg, /quit commands
  - Unread indicators on inactive tabs
  - DM tabs from user list clicks
- Removed unused models package (was for UUID-based schema)
- Removed conflicting UUID-based db methods
- Increased HTTP write timeout to 60s for long-poll support
This commit is contained in:
clawbot
2026-02-10 18:09:10 -08:00
committed by clawbot
parent 9daf836cbe
commit abc8532963
20 changed files with 1233 additions and 2753 deletions

View File

@@ -7,6 +7,7 @@ import (
"log/slog"
"net/http"
"git.eeqj.de/sneak/chat/internal/broker"
"git.eeqj.de/sneak/chat/internal/config"
"git.eeqj.de/sneak/chat/internal/db"
"git.eeqj.de/sneak/chat/internal/globals"
@@ -31,6 +32,7 @@ type Handlers struct {
params *Params
log *slog.Logger
hc *healthcheck.Healthcheck
broker *broker.Broker
}
// New creates a new Handlers instance.
@@ -39,6 +41,7 @@ func New(lc fx.Lifecycle, params Params) (*Handlers, error) {
s.params = &params
s.log = params.Logger.Get()
s.hc = params.Healthcheck
s.broker = broker.New()
lc.Append(fx.Hook{
OnStart: func(_ context.Context) error {
@@ -50,8 +53,8 @@ func New(lc fx.Lifecycle, params Params) (*Handlers, error) {
}
func (s *Handlers) respondJSON(w http.ResponseWriter, _ *http.Request, data any, status int) {
w.WriteHeader(status)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
if data != nil {
err := json.NewEncoder(w).Encode(data)