feat: seed script, forgot-password, hover cards, shortcuts, OpenAPI docs, runbook (phase 12)
- scripts/seed.go: idempotent demo data per §11.11 (make seed) - forgot/reset password: SMTP-gated, one-shot TTL tokens, uniform responses against enumeration, sessions revoked on reset; login page link + pages - profile hover cards on [data-user-card] elements (§11.13) - keyboard shortcuts: g b/m/t/h navigation, / focuses search (§10) - bulk archive endpoint (§11.9) - hand-written OpenAPI 3.1 covering §6, served at /api/docs + yaml download - make backup / make restore (mongodump archive via the mongo container) - README: quick start, demo data, runbook, breaker/job operations, working Caddy + nginx reverse-proxy samples (WS block, client_max_body_size), documented later-stubs (§11.24) - smoke.sh now exercises register → logout → login → me → board → pages Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
# Bounty Board
|
||||
|
||||
A consulting work-management platform: imports tickets from Jira / Azure
|
||||
DevOps / YouTrack (or an offline `demo` source), lets **consultants** atomize
|
||||
them into small developer tasks with AI assistance, publishes them on a
|
||||
**bounty board** where **developers** claim work (or an **AI work performer**
|
||||
does it), and tracks review, approval, and bounty-based metrics.
|
||||
|
||||
Built per [specification.md](specification.md): Go ≥ 1.22 + stdlib `net/http`,
|
||||
MongoDB, server-rendered templates + vanilla ES modules (no build step),
|
||||
Docker Compose deployment. Architectural decisions are logged in
|
||||
[DECISIONS.md](DECISIONS.md); build history in [PROGRESS.md](PROGRESS.md).
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# then edit .env:
|
||||
# SESSION_SECRET=$(openssl rand -base64 32)
|
||||
# CREDENTIALS_ENC_KEY=$(openssl rand -base64 32)
|
||||
# ADMIN_INITIAL_PASSWORD=<something strong>
|
||||
# ATOMIZER_TOKEN / WORK_PERFORMER_TOKEN = random strings
|
||||
# ANTHROPIC_API_KEY=<your key> # optional; offline fallback without it
|
||||
|
||||
docker compose --profile mocks up -d --build
|
||||
open http://localhost:8787
|
||||
```
|
||||
|
||||
First login: `ADMIN_EMAIL` / `ADMIN_INITIAL_PASSWORD` from `.env` — a
|
||||
password change is forced immediately.
|
||||
|
||||
Without `--profile mocks` only `app` + `mongo` start and the external
|
||||
service URLs in `.env` must point at real implementations of the §5
|
||||
contracts.
|
||||
|
||||
### Demo data
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yml -f docker-compose.test.yml up -d # publishes mongo on loopback
|
||||
make seed
|
||||
```
|
||||
|
||||
Seeds a demo customer (offline `demo` ticketing — tickets appear within one
|
||||
poll), consultants `clara@example.com` / `carlos@example.com`, developers
|
||||
`dev1@example.com` … `dev6@example.com` (password `demo-pass-123` for all),
|
||||
pools, and sample conversations.
|
||||
|
||||
## Services
|
||||
|
||||
| Service | Port (loopback) | What |
|
||||
|---|---|---|
|
||||
| `app` | 8787 | Bounty Board (UI + API + WS) |
|
||||
| `mongo` | — (never published; test overlay adds 27017) | database |
|
||||
| `atomizer-mock` | 8090 (profile `mocks`) | §5.1 Atomization Service — Anthropic chat completions (or native `/v1/messages` via `LLM_API_STYLE=anthropic`), deterministic fallback without an API key |
|
||||
| `work-performer` | 8091 (profile `mocks`) | §5.2 Work Performer — runs Claude Code with the host's `~/.claude` mounted read-only; simulated result when the CLI is unavailable |
|
||||
|
||||
The two external services are intentionally independent: separate codebases
|
||||
(`services/atomizer`, `services/work-performer`), containers, ports, and
|
||||
bearer tokens. Swap either by changing its base URL + token in `.env`.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
make build # app + atomizer mock
|
||||
make test # unit tests (no Mongo needed)
|
||||
make test-integration # needs mongo on loopback (test overlay)
|
||||
make test-contract # needs the mocks profile running
|
||||
make lint # gofmt + go vet
|
||||
make run # run the app locally (expects Mongo + .env)
|
||||
make smoke # curl smoke test against a running stack
|
||||
```
|
||||
|
||||
API reference: `GET /api/docs` (rendered) or [api/openapi.yaml](api/openapi.yaml).
|
||||
|
||||
## Operations runbook
|
||||
|
||||
**Logs** — JSON to stdout: `docker compose logs -f app` (request ids, sync
|
||||
runs, job retries). `docker compose logs -f atomizer-mock work-performer`
|
||||
for the mocks.
|
||||
|
||||
**Health** — `GET /healthz` (liveness), `GET /readyz` (Mongo required;
|
||||
atomizer/work-performer reported non-fatally), `GET /metricsz` (JSON
|
||||
counters: requests, sync runs, job queue depths). The admin UI →
|
||||
*Service status* shows live health, latency, circuit-breaker state, sync
|
||||
workers, and queue depths.
|
||||
|
||||
**Backups** (§11.23):
|
||||
|
||||
```bash
|
||||
make backup # mongodump → ./backups/bountyboard-<ts>.archive
|
||||
make restore FILE=backups/<archive> # mongorestore --drop
|
||||
```
|
||||
|
||||
Cron example: `0 3 * * * cd /opt/bountyboard && make backup >/dev/null`.
|
||||
|
||||
**Circuit breakers** — after 3 consecutive failures calls to an external
|
||||
service stop for 60 s (one half-open probe per minute afterwards). The
|
||||
consultant board disables Subdivide/Extend while the atomizer is down; AI
|
||||
assignment fails fast while the work performer is down. Each service has an
|
||||
independent breaker.
|
||||
|
||||
**Stuck jobs** — background jobs (`jobs` collection) retry 3× with backoff;
|
||||
jobs stuck `running` after a crash are re-queued automatically within
|
||||
5 minutes.
|
||||
|
||||
## Reverse proxy
|
||||
|
||||
The app expects a TLS-terminating proxy (§12). Set in `.env`:
|
||||
|
||||
```dotenv
|
||||
APP_BASE_URL=https://bounty.example.com
|
||||
TRUSTED_PROXY_CIDRS=172.16.0.0/12 # the proxy's source range as seen by the app
|
||||
COOKIE_SECURE=auto
|
||||
```
|
||||
|
||||
`X-Forwarded-*` headers are honored only from `TRUSTED_PROXY_CIDRS`; the
|
||||
resolved client IP feeds rate limiting and the audit log. WebSocket
|
||||
heartbeats (30 s) keep idle proxy timeouts from killing `/ws`.
|
||||
|
||||
### Caddy
|
||||
|
||||
```caddyfile
|
||||
bounty.example.com {
|
||||
encode gzip
|
||||
reverse_proxy 127.0.0.1:8787
|
||||
# WebSockets are proxied automatically; raise the body limit to match MAX_UPLOAD_MB
|
||||
request_body {
|
||||
max_size 25MB
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### nginx
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name bounty.example.com;
|
||||
ssl_certificate /etc/letsencrypt/live/bounty.example.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/bounty.example.com/privkey.pem;
|
||||
|
||||
client_max_body_size 25m; # match MAX_UPLOAD_MB
|
||||
|
||||
location /ws {
|
||||
proxy_pass http://127.0.0.1:8787;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 120s; # > the 30s WS heartbeat
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8787;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
All configuration comes from `.env` (see `.env.example` for every knob with
|
||||
comments): Mongo, session/credential secrets, bootstrap admin, the two
|
||||
external service URLs + tokens + timeouts, SMTP (enables forgot-password
|
||||
emails when set), OIDC SSO (buttons appear when set), upload limit, rate
|
||||
limits, and the mock services' Anthropic settings.
|
||||
|
||||
Consultants map their ticketing identities in their profile via
|
||||
`users.extra.ticketingIdentities` (e.g. set by an admin):
|
||||
`{"jira": "consultant@corp.com", "azure_devops": "consultant@corp.com", "youtrack": "consultant.login"}`.
|
||||
The `demo` ticketing type needs no identity.
|
||||
|
||||
## Deliberately out of scope (documented stubs, §11.24)
|
||||
|
||||
- **Webhook ingestion** instead of polling — the sync layer is keyed on an
|
||||
idempotent `(system, key, customerId)` upsert, so a webhook receiver can
|
||||
reuse `UpsertImportedTask` unchanged.
|
||||
- **Status write-back** to customer systems — v1 sync is read-only.
|
||||
- **Email digests** — notifications are in-app + WS; the mailer exists and
|
||||
is used for password resets.
|
||||
- **Production AI work performer** — the §5.2 HTTP contract is final; the
|
||||
shipped container is a placeholder.
|
||||
- **Localization** — UI strings are English-only.
|
||||
|
||||
Reference in New Issue
Block a user