qkeeper/ROADMAP.md
Aleksandr Berkuta 5806fe84c4 init commit
2026-03-27 20:27:56 +03:00

5.5 KiB

Guerrilla Queue — Roadmap

Self-hosted, zero-admin digital queue. Single URL, QR code, no login. Spec: queue-spec.md | Plan: ~/.claude/plans/peaceful-sprouting-squid.md


Stack

  • Backend: Go 1.22 · gorilla/websocket · skip2/go-qrcode
  • Frontend: Vanilla JS + CSS, single static/index.html (~15 KB uncompressed)
  • Deploy: Docker multi-stage (→ scratch) · docker-compose · Traefik

File map

File Purpose
main.go HTTP server, embed wiring, gzip middleware, routes
hub.go WebSocket hub, client registry, message dispatch
queue.go QueueState, Entry, all business logic + 60s timers
static/index.html Entire frontend: HTML + CSS + JS inline
Dockerfile Multi-stage build → scratch image
docker-compose.yml Traefik labels, APP_URL / QUEUE_HOSTNAME vars

Phases

  • Phase 1 — Go skeleton (go.mod, main.go, hub.go, queue.go, static/index.html stub)
  • Phase 2 — Queue core (join, selfRemove, findBy*, stateJSON, avgDuration)
  • Phase 3 — Removal mechanics (markDone, voteRemove, restore, timerRemove, cleanupVotes)
  • Phase 4 — Frontend (full render loop, join form, confirm flows, countdown, wait time)
  • Phase 5 — QR code (/qr.png via skip2/go-qrcode), gzip middleware, ROADMAP.md
  • Phase 6 — Docker & deployment files (Dockerfile, docker-compose.yml)

Deployment

Environment variables

Variable Default Notes
APP_URL derived from request Full public URL encoded in the QR code. Always set this in production.
QUEUE_HOSTNAME queue.example.com Traefik Host() routing rule

docker-compose (quick start)

# 1. Create .env next to docker-compose.yml:
APP_URL=https://queue.yourdomain.com
QUEUE_HOSTNAME=queue.yourdomain.com

# 2. Make sure the Traefik network exists:
docker network create traefik

# 3. Build and start:
docker compose up -d --build

Without Traefik (VPS, direct port)

services:
  queue:
    build: .
    restart: unless-stopped
    ports:
      - "8080:8080"
    environment:
      - APP_URL=https://queue.yourdomain.com

For Nginx in front:

location /ws {
    proxy_pass http://localhost:8080;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "Upgrade";
    proxy_set_header Host $host;
}
location / {
    proxy_pass http://localhost:8080;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
}

WebSocket message protocol

Client → Server

type extra fields when
init token (empty string if none) on every connect
join name (optional) user taps "Get my number"
done user confirms "I'm done"
mark_done entryId mark #1 as done (any queue member)
vote_remove entryId vote to remove non-#1 entry
restore entry owner cancels pending_removal

Server → Client

type extra fields when
hello yourEntryId (null if not in queue) response to init
joined yourToken, yourEntryId response to join
state entries[], counter, avgDuration broadcast on every queue change
error reason, onNumber e.g. already_voting

Key design decisions

Timer safety: The 60 s removal timer fires by sending an entry ID to hub.timerC (a buffered channel). The hub's single goroutine reads from timerC and calls q.timerRemove() — this serialises all mutations through one goroutine with no queue-level mutex needed.

cleanupVotes logic: When an entry is removed, its queue number is purged from all PendingBy lists. Entries in pending_removal whose vote count drops below threshold are restored to active (Case B: quorum lost). Entries with empty PendingBy after cleanup are NOT restored (Case A: the single marker left, let the timer run).

Token flow: Token is generated by server on join, returned once, stored in localStorage. On reconnect the client sends it in init; server re-associates the *Client with the existing entry. Token is never broadcast to other clients.

Frontend XSS: entry.name is the only user-supplied string that other browsers render. It is HTML-escaped via esc() on every insertion into innerHTML. All other dynamic values are server-generated hex IDs or integers.


Out of scope (per spec)

Admin panel · push notifications · persistence across server restarts · multiple queues · auth/accounts · entry editing · vote retraction