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

144 lines
5.5 KiB
Markdown

# 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
- [x] **Phase 1** — Go skeleton (go.mod, main.go, hub.go, queue.go, static/index.html stub)
- [x] **Phase 2** — Queue core (join, selfRemove, findBy\*, stateJSON, avgDuration)
- [x] **Phase 3** — Removal mechanics (markDone, voteRemove, restore, timerRemove, cleanupVotes)
- [x] **Phase 4** — Frontend (full render loop, join form, confirm flows, countdown, wait time)
- [x] **Phase 5** — QR code (/qr.png via skip2/go-qrcode), gzip middleware, ROADMAP.md
- [x] **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)
```bash
# 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)
```yaml
services:
queue:
build: .
restart: unless-stopped
ports:
- "8080:8080"
environment:
- APP_URL=https://queue.yourdomain.com
```
For Nginx in front:
```nginx
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