Created Turnpike, event attendee and volunteer management

Built after prototype, Traverse, an attendee and volunteer list
maintainer.
This commit is contained in:
Pen Anderson 2026-03-03 11:27:07 -06:00
commit 5d56ba8112
59 changed files with 8663 additions and 0 deletions

192
docs/INSTALLATION.md Normal file
View file

@ -0,0 +1,192 @@
# Turnpike Installation Guide
This guide covers building, deploying, and operating Turnpike. For usage and event workflows, see [USAGE.md](USAGE.md).
## Requirements
- **Go 1.24+** (for building)
- **Node.js 18+** (for building the frontend)
- No external database — Turnpike uses embedded SQLite via a pure-Go driver
## Building from Source
```sh
git clone <repo-url>
cd turnpike
make build
```
This runs `npm ci && npm run build` in `frontend/`, then `CGO_ENABLED=0 go build -o turnpike .`. The result is a single static binary with the frontend assets embedded. No runtime dependencies.
Other make targets: `make dev` (prints two-terminal dev instructions), `make clean` (removes binary and build artifacts).
## Configuration Reference
| Flag | Env var | Default | Description |
|------|---------|---------|-------------|
| `--addr` | — | `0.0.0.0:8180` | Listen address |
| `--db` | — | `turnpike.db` | SQLite database path |
| `--secret` | `TURNPIKE_SECRET` | auto-generated | JWT signing secret |
| `--token-expiry` | — | `24` | JWT lifetime in hours |
| `--base-url` | — | — | Public URL for volunteer token links |
| `--smtp-host` | — | — | SMTP server hostname |
| `--smtp-port` | — | `587` | SMTP port (587 = STARTTLS, 465 = implicit TLS) |
| `--smtp-user` | — | — | SMTP username |
| `--smtp-password` | — | — | SMTP password |
| `--smtp-from` | — | — | Sender email address |
| `--smtp-from-name` | — | — | Sender display name |
| — | `TURNPIKE_ADMIN_USER` | — | Bootstrap admin username (first run only) |
| — | `TURNPIKE_ADMIN_PASSWORD` | — | Bootstrap admin password (first run only) |
SMTP settings can also be configured at runtime through the Settings page (admin only). CLI flags override stored database values.
## Running
```sh
# Minimal startup (creates admin user on first run)
TURNPIKE_ADMIN_USER=admin TURNPIKE_ADMIN_PASSWORD=changeme ./turnpike
# With explicit options
./turnpike \
--addr 0.0.0.0:8180 \
--db /var/lib/turnpike/turnpike.db \
--secret your-jwt-secret \
--base-url https://turnpike.example.com
```
The admin account is only created on first startup when no users exist and both `TURNPIKE_ADMIN_USER` and `TURNPIKE_ADMIN_PASSWORD` are set. After the first user is created, these environment variables have no effect.
### JWT Secret
If `--secret` / `TURNPIKE_SECRET` is not provided, Turnpike auto-generates a secret and stores it in the database. This persists across restarts as long as the database file is preserved. To use an explicit secret (recommended for production), pass it via flag or env var.
Changing or losing the JWT secret invalidates all active sessions — users will need to log in again.
## Systemd
```ini
[Unit]
Description=Turnpike event management
After=network.target
[Service]
ExecStart=/usr/local/bin/turnpike --db /var/lib/turnpike/turnpike.db --base-url https://turnpike.example.com
Environment=TURNPIKE_SECRET=your-secret-here
EnvironmentFile=/etc/turnpike/admin.env
StateDirectory=turnpike
DynamicUser=true
Restart=on-failure
[Install]
WantedBy=multi-user.target
```
The `admin.env` file should contain:
```
TURNPIKE_ADMIN_USER=admin
TURNPIKE_ADMIN_PASSWORD=your-password
```
Note: systemd uses `EnvironmentFile` (singular). The plural `EnvironmentFiles` is silently ignored.
## Docker
A multi-stage `Dockerfile` is included in the repo (node build, Go build, scratch final image):
```sh
docker build -t turnpike .
docker run -p 8180:8180 \
-v turnpike-data:/data \
-e TURNPIKE_ADMIN_USER=admin \
-e TURNPIKE_ADMIN_PASSWORD=changeme \
-e TURNPIKE_SECRET=your-secret \
turnpike --db /data/turnpike.db
```
## NixOS
Turnpike builds with `buildGoModule` using the pure-Go SQLite driver (no CGO):
```nix
turnpike = pkgs.buildGoModule {
pname = "turnpike";
version = "0.1.0";
src = ./path/to/turnpike; # must include vendor/ and frontend/dist/
vendorHash = null;
env.CGO_ENABLED = 0;
};
```
The source directory must contain:
- Go source files and `vendor/` (run `go mod vendor`)
- Pre-built frontend at `frontend/dist/` (run `cd frontend && npm run build`)
A complete NixOS module example with `DynamicUser`, `StateDirectory`, and agenix secrets is in the project's `homelab/turnpike.nix`.
## Reverse Proxy
Turnpike serves both the API and frontend on a single port. Behind a reverse proxy, ensure SSE connections for real-time sync are not buffered.
### nginx
```nginx
server {
listen 443 ssl;
server_name turnpike.example.com;
location / {
proxy_pass http://127.0.0.1:8180;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /api/sync/stream {
proxy_pass http://127.0.0.1:8180;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 86400s;
}
}
```
The `/api/sync/stream` endpoint uses Server-Sent Events (SSE). The key settings are `proxy_buffering off` and a long `proxy_read_timeout` to keep the connection alive.
## TLS / HTTPS
Use a TLS termination proxy (nginx, Caddy, Traefik) with Let's Encrypt or your own certificates. Turnpike itself serves plain HTTP.
Set `--base-url https://turnpike.example.com` so that volunteer token links in exported CSVs and emails use the correct URL.
## Backup
Turnpike stores all data in a single SQLite file (the path passed to `--db`).
**Simple backup:** copy the `.db` file while the server is running — SQLite WAL mode ensures a consistent snapshot.
**Continuous replication:** [Litestream](https://litestream.io) streams WAL changes to S3, GCS, or Azure Blob Storage with near-zero overhead.
## Upgrading
1. Pull the latest source
2. Rebuild: `make build`
3. Replace the binary and restart the service
Database migrations run automatically on startup. All migrations are additive (`ALTER TABLE ... ADD COLUMN`) — no data is lost.
## Troubleshooting
**Login fails after restart:** If the JWT secret wasn't persisted (`--secret` or `TURNPIKE_SECRET`), a new secret is generated on restart, invalidating all existing tokens. Users need to log in again. To prevent this, always set an explicit secret in production.
**Admin account not created:** The bootstrap admin is only created on first startup when no users exist. If the service started without `TURNPIKE_ADMIN_USER` / `TURNPIKE_ADMIN_PASSWORD`, users were never created. Delete the database file and restart with the env vars set.
**Soft deletes:** Records are marked with `deleted_at`, not removed from the database. This is required for sync — clients need to see deletions to purge their local IndexedDB. Don't hard-delete records directly from the SQLite database.
**`EnvironmentFile` vs `EnvironmentFiles`:** systemd's directive is `EnvironmentFile` (singular). Using the plural form silently ignores the file — the service starts but environment variables are never set.
**SSE disconnections:** If real-time updates stop working behind a proxy, check that `proxy_buffering off` is set for `/api/sync/stream`. The client reconnects automatically, but buffering proxies can prevent events from reaching the browser.

183
docs/USAGE.md Normal file
View file

@ -0,0 +1,183 @@
# Turnpike Usage Guide
This guide is for event organizers and ops teams running a Turnpike instance. For installation and deployment, see [INSTALLATION.md](INSTALLATION.md).
## First Login
On first startup with `TURNPIKE_ADMIN_USER` and `TURNPIKE_ADMIN_PASSWORD` set, Turnpike creates a bootstrap admin account. Log in at `https://your-instance/` with those credentials.
After logging in, create accounts for your team under **Users**. Each user gets a username, password, and role. The admin bootstrap credentials are only used on initial setup — they have no effect on subsequent restarts.
## User Roles
| Role | What they see | What they can do |
|------|--------------|------------------|
| **admin** | All pages + Settings | Everything: attendee import, user management, SMTP config, departments, shifts, volunteers |
| **coordinator** | Dashboard, Schedule Board, Volunteers, Departments, Shifts | Manage volunteers, departments, and shifts across all departments. Cannot manage users or settings |
| **volunteer_lead** | Schedule Board, Volunteers, Departments | Manage volunteers and shifts within their assigned department only |
| **gate** | Full-screen Gate UI | Check in attendees (search + QR scan). No access to other pages |
Ticketing and ops staff should use the **admin** role. The `ticketing` role exists in the codebase but is effectively unused — admin covers all ticketing functions.
Volunteer leads are scoped to a single department. When creating a volunteer_lead user, assign their department.
## Event Setup
1. **Configure your event** — go to the Dashboard and set the event name and dates.
2. **Create departments** — under Departments, add each department your event needs (e.g., Gate, Greeters, Rangers, Build, LNT).
3. **Import attendees** — see next section.
4. **Create shifts** — under Shifts, create shifts for each department with day, start/end time, and capacity.
## Importing Attendees
Go to **Import** and upload a CSV file. Turnpike auto-detects two formats:
### CrowdWork / Zeffy format
| Column | Maps to |
|--------|---------|
| `Patron Name` | Name |
| `Patron Email` | Email |
| `Order Number` | Ticket ID |
| `Tier Name` | Ticket type |
### Generic format
| Column | Maps to |
|--------|---------|
| `name` (required) | Name |
| `email` | Email |
| `ticket_id` | Ticket ID |
| `ticket_type` | Ticket type |
| `note` | Note |
Column matching is case-insensitive. Extra columns are ignored. BOM-encoded files (Windows Excel exports) are handled automatically.
### Party-size dedup
CrowdWork exports one row per ticket, even when the same person bought multiple tickets in one order. Turnpike handles this automatically:
- First row for "Alice Smith" (order 1234) creates a record with `party_size=1`
- Subsequent rows with the same name + order number increment `party_size` (no duplicate record)
- Result: one attendee record, `party_size=3` if three tickets were purchased
The import result shows `inserted` (new records), `grouped` (merged into existing party), and `skipped` (exact duplicates).
Re-importing the same CSV is safe — existing records are skipped, not duplicated.
## Managing Volunteers
Under **Volunteers**, you can:
- Create volunteers manually (name, email, department)
- Link a volunteer to an existing attendee record (for dual check-in at the gate)
- Assign volunteers to departments
- Check in volunteers
Volunteers are separate from attendees. A person can be both an attendee (ticket holder) and a volunteer (shift worker). Linking them enables the gate team to check in both records simultaneously.
## Shift Scheduling
Under **Shifts**, create shifts for each department:
- **Day** — the date of the shift
- **Start/end time** — HH:MM format
- **Capacity** — maximum number of volunteers
### Assigning volunteers
From the Shifts page or the Schedule Board, assign volunteers to shifts. Turnpike checks for conflicts — if a volunteer already has a shift on the same day with overlapping times, you'll see a warning and can choose to force the assignment.
### Reordering
Shifts can be reordered within a department to reflect priority or sequence. The Schedule Board supports drag-and-drop reordering.
## Volunteer Kiosk
The kiosk lets volunteers self-select shifts without logging in.
### Setup
1. **Generate tokens** — on the Attendees page, click "Generate Tokens." This creates a unique 8-character code for every attendee that doesn't have one.
2. **Distribute tokens** — two options:
- **Export CSV** — downloads a file with columns `Email Address`, `First Name`, `Token`, `Signup Link`. Import this into MailChimp, Zeffy, or any email platform.
- **Email directly** — if SMTP is configured (see below), use "Email All" to send token links, or email individually per attendee.
3. **Set base URL** — in Settings, set the public base URL (e.g., `https://turnpike.example.com`). Token links use this URL.
### Volunteer experience
Each volunteer receives a link like `https://turnpike.example.com/#/v/ABC12345`. This opens a mobile-friendly page showing:
- Their name and department
- Currently assigned shifts
- Available shifts with remaining capacity
Claiming a shift checks for time conflicts. If a conflict exists, the volunteer sees which shifts overlap and can confirm to proceed anyway.
No login is required. The 8-character token authenticates the request.
### Token format
Tokens use the character set `A-Z, 2-9` (excluding 0/O, 1/I/L to avoid ambiguity when reading aloud or on printed badges).
## Gate Check-In
Users with the **gate** role see a dedicated full-screen UI:
- **QR scanner** — uses the device camera via the BarcodeDetector API. Scanned codes populate the search field.
- **Search** — type a name to filter attendees in real-time (searches local IndexedDB, works offline).
- **Party check-in** — for attendees with `party_size > 1`, the gate UI shows progress ("2/3 checked in") and offers "Check in 1" or "Check in all remaining."
- **Volunteer dual check-in** — if an attendee is linked to a volunteer record, the gate UI shows their volunteer status and offers to check in both simultaneously.
- **Recent check-ins** — the last 10 check-ins are shown for quick reference.
Gate devices should install Turnpike as a PWA (Add to Home Screen) for the best experience. Check-ins are stored locally and sync when connectivity is available.
## Schedule Board
The Schedule Board is the primary UI for coordinators and volunteer leads. It shows:
- Shifts grouped by department and day
- Each shift card shows: name, time, capacity (used/total), assigned volunteers
- Conflict badges when a volunteer has overlapping shifts on the same day
**Coordinators and admins** see all departments. **Volunteer leads** see only their assigned department.
Actions available:
- Assign volunteers to shifts from a dropdown
- Remove volunteer assignments
- Reorder shifts within a department
- Edit shift details inline
## SMTP Configuration
SMTP enables token email distribution and test emails. Configure in **Settings** (admin only):
| Field | Description |
|-------|-------------|
| SMTP Host | Mail server hostname (e.g., `smtp.fastmail.com`) |
| SMTP Port | `587` for STARTTLS (default), `465` for implicit TLS |
| SMTP User | Login username |
| SMTP Password | Login password |
| From Address | Sender email address |
| From Name | Sender display name |
After saving, use "Send Test Email" to verify the configuration.
SMTP can also be set via CLI flags (`--smtp-host`, etc.) which override database values.
## Offline Mode
Turnpike is a Progressive Web App (PWA). After the first load, it works offline:
- **Gate check-ins** are stored in the browser's IndexedDB and sync when connectivity returns.
- **Real-time updates** use Server-Sent Events (SSE). When the connection drops, the client reconnects automatically.
- **Sync** pulls all changes from the server on startup and periodically thereafter. Local changes are queued in an outbox and flushed in order.
Install Turnpike as a PWA (Add to Home Screen on mobile, or Install App in desktop Chrome) for the best offline experience.
## CSV Exports
Two CSV exports are available from the Attendees page:
- **Attendee export** — all attendee records with check-in status
- **Token link export** — columns: `Email Address`, `First Name`, `Token`, `Signup Link`. Only includes attendees with tokens. Compatible with MailChimp and Zeffy for bulk email campaigns.