Created Turnpike, event attendee and volunteer management
Built after prototype, Traverse, an attendee and volunteer list maintainer.
This commit is contained in:
commit
d05b8dc7e0
59 changed files with 8663 additions and 0 deletions
192
docs/INSTALLATION.md
Normal file
192
docs/INSTALLATION.md
Normal 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
183
docs/USAGE.md
Normal 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue