diff --git a/frontend/src/api.js b/frontend/src/api.js
index 67d491e..b0767e6 100644
--- a/frontend/src/api.js
+++ b/frontend/src/api.js
@@ -72,21 +72,6 @@ export const api = {
emailCode: (id) => apiJSON(`/api/tickets/${id}/email-code`, { method: 'POST' }),
emailAllCodes: () => apiJSON('/api/tickets/email-codes', { method: 'POST' }),
},
- attendees: {
- list: (params = {}) => apiJSON('/api/attendees?' + new URLSearchParams(params)),
- get: (id) => apiJSON(`/api/attendees/${id}`),
- create: (data) => apiJSON('/api/attendees', { method: 'POST', body: JSON.stringify(data) }),
- update: (id, data) => apiJSON(`/api/attendees/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
- delete: (id) => apiFetch(`/api/attendees/${id}`, { method: 'DELETE' }),
- checkIn: (id, opts = {}) =>
- apiJSON(`/api/attendees/${id}/checkin`, { method: 'POST', body: JSON.stringify(opts) }),
- generateTokens: () =>
- apiJSON('/api/attendees/generate-tokens', { method: 'POST' }),
- emailToken: (id) =>
- apiJSON(`/api/attendees/${id}/email-token`, { method: 'POST' }),
- emailAllTokens: () =>
- apiJSON('/api/attendees/email-tokens', { method: 'POST' }),
- },
volunteers: {
list: (params = {}) => apiJSON('/api/volunteers?' + new URLSearchParams(params)),
get: (id) => apiJSON(`/api/volunteers/${id}`),
@@ -126,7 +111,6 @@ export const api = {
update: (data) => apiJSON('/api/settings', { method: 'PUT', body: JSON.stringify(data) }),
testEmail: (to) => apiJSON('/api/settings/test-email', { method: 'POST', body: JSON.stringify({ to }) }),
toggleShiftSignups: (open) => apiJSON('/api/settings/shift-signups', { method: 'POST', body: JSON.stringify({ open }) }),
- resetAttendees: () => apiJSON('/api/settings/reset-attendees', { method: 'POST' }),
resetTickets: () => apiJSON('/api/settings/reset-tickets', { method: 'POST' }),
resetVolunteers: () => apiJSON('/api/settings/reset-volunteers', { method: 'POST' }),
resetShifts: () => apiJSON('/api/settings/reset-shifts', { method: 'POST' }),
diff --git a/frontend/src/api.test.js b/frontend/src/api.test.js
index f6527f5..974dd32 100644
--- a/frontend/src/api.test.js
+++ b/frontend/src/api.test.js
@@ -71,16 +71,16 @@ describe('api methods', () => {
expect(JSON.parse(opts.body)).toEqual({ username: 'admin', password: 'pass' })
})
- it('attendees.list calls correct endpoint', async () => {
- const f = mockFetch({ attendees: [] })
- await api.attendees.list({ search: 'test' })
- expect(f.mock.calls[0][0]).toBe('/api/attendees?search=test')
+ it('participants.list calls correct endpoint', async () => {
+ const f = mockFetch({ participants: [] })
+ await api.participants.list({ search: 'test' })
+ expect(f.mock.calls[0][0]).toBe('/api/participants?search=test')
})
- it('attendees.delete uses DELETE method', async () => {
+ it('participants.delete uses DELETE method', async () => {
const f = mockFetch({}, 204)
- await api.attendees.delete(5)
- expect(f.mock.calls[0][0]).toBe('/api/attendees/5')
+ await api.participants.delete(5)
+ expect(f.mock.calls[0][0]).toBe('/api/participants/5')
expect(f.mock.calls[0][1].method).toBe('DELETE')
})
diff --git a/frontend/src/db.js b/frontend/src/db.js
index 6f45ba8..a09f332 100644
--- a/frontend/src/db.js
+++ b/frontend/src/db.js
@@ -40,6 +40,12 @@ db.version(3).stores({
outbox: '++id, table, op, synced_at',
})
+db.version(4).stores({
+ attendees: null,
+ outbox: null,
+ volunteers: 'id, name, department_id, checked_in, participant_id, deleted_at',
+})
+
export async function getLastSync() {
const m = await db.meta.get('last_sync')
return m?.value ?? ''
diff --git a/frontend/src/db.test.js b/frontend/src/db.test.js
index 38fc57d..282b6fc 100644
--- a/frontend/src/db.test.js
+++ b/frontend/src/db.test.js
@@ -9,8 +9,8 @@ describe('db schema', () => {
it('has expected tables', () => {
const names = db.tables.map(t => t.name).sort()
expect(names).toEqual([
- 'attendees', 'departments', 'event', 'meta',
- 'outbox', 'participants', 'session', 'shifts', 'tickets', 'volunteer_shifts', 'volunteers',
+ 'departments', 'event', 'meta',
+ 'participants', 'session', 'shifts', 'tickets', 'volunteer_shifts', 'volunteers',
])
})
})
diff --git a/frontend/src/pages/Attendees.svelte b/frontend/src/pages/Attendees.svelte
deleted file mode 100644
index 72619b8..0000000
--- a/frontend/src/pages/Attendees.svelte
+++ /dev/null
@@ -1,274 +0,0 @@
-
-
-
-
-
- {#if error}
-
{error}
- {/if}
- {#if success}
-
{success}
- {/if}
-
- {#if showAdd && canManage}
-
- {/if}
-
-
-
- {#if ($ticketTypes ?? []).length > 0}
-
- {/if}
-
-
- {filtered.length} shown
-
-
-
- {#if ($allAttendees ?? []).length === 0}
-
-
No attendees yet
-
Import a CSV or add attendees manually.
-
- {:else}
-
-
-
-
- | Name |
- Ticket type |
- Email |
- Status |
- {#if canCheckIn} | {/if}
-
-
-
- {#each filtered as a (a.id)}
-
- |
- {a.name}
- {#if a.ticket_id}
- · {a.ticket_id}
- {/if}
- {#if (a.party_size ?? 1) > 1}
- ×{a.party_size}
- {/if}
- {#if a.note}
- {a.note}
- {/if}
- |
- {a.ticket_type || '—'} |
-
- {a.email || '—'}
- {#if a.volunteer_token && canManage}
-
- {a.volunteer_token}
- {#if a.email}
-
- {/if}
-
- {/if}
- |
-
- {#if (a.party_size ?? 1) > 1}
-
- {a.checked_in_count ?? 0}/{a.party_size} in
-
- {:else}
-
- {a.checked_in ? 'Checked in' : 'Pending'}
-
- {/if}
- {#if a.checked_in_at}
-
- {new Date(a.checked_in_at).toLocaleTimeString()}
-
- {/if}
- |
- {#if canCheckIn}
-
- {#if (a.checked_in_count ?? 0) < (a.party_size ?? 1)}
- checkIn(a)} />
- {/if}
- |
- {/if}
-
- {/each}
-
-
-
- {/if}
-
diff --git a/frontend/src/sync.js b/frontend/src/sync.js
index e8f2d1a..fa3b641 100644
--- a/frontend/src/sync.js
+++ b/frontend/src/sync.js
@@ -12,17 +12,11 @@ export async function syncPull() {
const data = await api.sync.pull(since)
await db.transaction('rw',
- [db.event, db.attendees, db.participants, db.tickets, db.departments, db.volunteers, db.shifts, db.volunteer_shifts],
+ [db.event, db.participants, db.tickets, db.departments, db.volunteers, db.shifts, db.volunteer_shifts],
async () => {
if (data.event) {
await db.event.put(data.event)
}
- if (data.attendees?.length) {
- await db.attendees.bulkPut(data.attendees)
- // Purge hard-deleted records from Dexie
- const deleted = data.attendees.filter(a => a.deleted_at).map(a => a.id)
- if (deleted.length) await db.attendees.bulkDelete(deleted)
- }
if (data.participants?.length) {
await db.participants.bulkPut(data.participants)
const deleted = data.participants.filter(p => p.deleted_at).map(p => p.id)
@@ -82,9 +76,6 @@ export function startSSE(onEvent) {
try {
const payload = JSON.parse(e.data)
if (payload.event === 'checkin') {
- if (payload.data?.type === 'attendee' && payload.data?.attendee) {
- await db.attendees.put(payload.data.attendee)
- }
if (payload.data?.type === 'ticket' && payload.data?.ticket) {
await db.tickets.put(payload.data.ticket)
}
diff --git a/frontend/src/sync.test.js b/frontend/src/sync.test.js
index 2c8915e..21c213e 100644
--- a/frontend/src/sync.test.js
+++ b/frontend/src/sync.test.js
@@ -18,30 +18,31 @@ function mockFetch(body = {}, status = 200) {
}
describe('syncPull', () => {
- it('writes attendees to Dexie', async () => {
+ it('writes participants to Dexie', async () => {
mockFetch({
server_time: '2026-03-01T12:00:00Z',
- attendees: [{ id: 1, name: 'Titania' }],
+ participants: [{ id: 1, preferred_name: 'Titania', email: 'titania@example.com' }],
+ tickets: [],
departments: [],
volunteers: [],
shifts: [],
volunteer_shifts: [],
})
- // Import fresh to reset syncing guard
const { syncPull } = await import('./sync.js')
await syncPull()
- const a = await db.attendees.get(1)
- expect(a.name).toBe('Titania')
+ const p = await db.participants.get(1)
+ expect(p.preferred_name).toBe('Titania')
expect(await getLastSync()).toBe('2026-03-01T12:00:00Z')
})
- it('deletes soft-deleted attendees from Dexie', async () => {
- await db.attendees.put({ id: 1, name: 'Titania' })
+ it('deletes soft-deleted participants from Dexie', async () => {
+ await db.participants.put({ id: 1, preferred_name: 'Titania', email: 'titania@example.com' })
mockFetch({
server_time: '2026-03-01T13:00:00Z',
- attendees: [{ id: 1, name: 'Titania', deleted_at: '2026-03-01T12:30:00Z' }],
+ participants: [{ id: 1, preferred_name: 'Titania', deleted_at: '2026-03-01T12:30:00Z' }],
+ tickets: [],
departments: [],
volunteers: [],
shifts: [],
@@ -50,8 +51,8 @@ describe('syncPull', () => {
const { syncPull } = await import('./sync.js')
await syncPull()
- const a = await db.attendees.get(1)
- expect(a).toBeUndefined()
+ const p = await db.participants.get(1)
+ expect(p).toBeUndefined()
})
it('deletes soft-deleted volunteer_shifts from Dexie', async () => {
@@ -59,7 +60,8 @@ describe('syncPull', () => {
mockFetch({
server_time: '2026-03-01T13:00:00Z',
- attendees: [],
+ participants: [],
+ tickets: [],
departments: [],
volunteers: [],
shifts: [],
@@ -75,7 +77,8 @@ describe('syncPull', () => {
it('sets lastSync timestamp', async () => {
mockFetch({
server_time: '2026-03-02T00:00:00Z',
- attendees: [],
+ participants: [],
+ tickets: [],
departments: [],
volunteers: [],
shifts: [],
diff --git a/handle_attendees.go b/handle_attendees.go
deleted file mode 100644
index 0ee5628..0000000
--- a/handle_attendees.go
+++ /dev/null
@@ -1,177 +0,0 @@
-package main
-
-import (
- "encoding/csv"
- "encoding/json"
- "net/http"
- "strconv"
-)
-
-func (app *App) handleListAttendees(w http.ResponseWriter, r *http.Request) {
- q := r.URL.Query()
- attendees, err := app.listAttendees(q.Get("search"), q.Get("ticket_type"), q.Get("checked_in"))
- if err != nil {
- writeError(w, err.Error(), http.StatusInternalServerError)
- return
- }
- types, _ := app.attendeeTicketTypes()
- total, checkedIn, _ := app.attendeeCounts()
- writeJSON(w, map[string]any{
- "attendees": attendees,
- "ticket_types": types,
- "total": total,
- "checked_in": checkedIn,
- })
-}
-
-func (app *App) handleCreateAttendee(w http.ResponseWriter, r *http.Request) {
- var a Attendee
- if err := json.NewDecoder(r.Body).Decode(&a); err != nil {
- writeError(w, "invalid request", http.StatusBadRequest)
- return
- }
- if a.Name == "" {
- writeError(w, "name is required", http.StatusBadRequest)
- return
- }
- created, err := app.createAttendee(a)
- if err != nil {
- writeError(w, err.Error(), http.StatusInternalServerError)
- return
- }
- w.WriteHeader(http.StatusCreated)
- writeJSON(w, created)
-}
-
-func (app *App) handleGetAttendee(w http.ResponseWriter, r *http.Request) {
- id, err := strconv.Atoi(r.PathValue("id"))
- if err != nil {
- writeError(w, "invalid id", http.StatusBadRequest)
- return
- }
- a, err := app.getAttendee(id)
- if err != nil || a == nil {
- writeError(w, "not found", http.StatusNotFound)
- return
- }
- writeJSON(w, a)
-}
-
-func (app *App) handleUpdateAttendee(w http.ResponseWriter, r *http.Request) {
- id, err := strconv.Atoi(r.PathValue("id"))
- if err != nil {
- writeError(w, "invalid id", http.StatusBadRequest)
- return
- }
- var a Attendee
- if err := json.NewDecoder(r.Body).Decode(&a); err != nil {
- writeError(w, "invalid request", http.StatusBadRequest)
- return
- }
- if a.Name == "" {
- writeError(w, "name is required", http.StatusBadRequest)
- return
- }
- a.ID = id
- if err := app.updateAttendee(a); err != nil {
- writeError(w, err.Error(), http.StatusInternalServerError)
- return
- }
- updated, _ := app.getAttendee(id)
- writeJSON(w, updated)
-}
-
-func (app *App) handleDeleteAttendee(w http.ResponseWriter, r *http.Request) {
- id, err := strconv.Atoi(r.PathValue("id"))
- if err != nil {
- writeError(w, "invalid id", http.StatusBadRequest)
- return
- }
- if err := app.deleteAttendee(id); err != nil {
- writeError(w, err.Error(), http.StatusInternalServerError)
- return
- }
- w.WriteHeader(http.StatusNoContent)
-}
-
-// handleCheckInAttendee handles POST /api/attendees/:id/checkin.
-// Optional body: {"count": N, "also_volunteer": true}
-// Returns {"attendee": ..., "volunteer": ...} — volunteer is included if also_volunteer=true
-// and the attendee has a linked volunteer record.
-func (app *App) handleCheckInAttendee(w http.ResponseWriter, r *http.Request) {
- id, err := strconv.Atoi(r.PathValue("id"))
- if err != nil {
- writeError(w, "invalid id", http.StatusBadRequest)
- return
- }
-
- var body struct {
- Count int `json:"count"`
- AlsoVolunteer bool `json:"also_volunteer"`
- }
- body.Count = 1
- json.NewDecoder(r.Body).Decode(&body)
- if body.Count < 1 {
- body.Count = 1
- }
-
- claims := claimsFromContext(r)
- a, err := app.checkInAttendee(id, claims.UserID, body.Count)
- if err != nil {
- writeError(w, err.Error(), http.StatusInternalServerError)
- return
- }
-
- result := map[string]any{"attendee": a}
-
- if body.AlsoVolunteer {
- // Try to find volunteer via participant_id first (new model), fall back to attendee_id (legacy).
- var v *Volunteer
- if a != nil {
- p, _ := app.getParticipantByEmail(a.Email)
- if p != nil {
- v, _ = app.getVolunteerByParticipantID(p.ID)
- }
- }
- if v == nil {
- v, _ = app.getVolunteerByAttendeeID(id)
- }
- if v != nil {
- if !v.CheckedIn {
- if v2, err := app.checkInVolunteer(v.ID, claims.UserID); err == nil {
- result["volunteer"] = v2
- app.broker.publish("checkin", map[string]any{"type": "volunteer", "volunteer": v2})
- }
- } else {
- result["volunteer"] = v
- }
- }
- }
-
- app.broker.publish("checkin", map[string]any{"type": "attendee", "attendee": a})
- writeJSON(w, result)
-}
-
-func (app *App) handleExportAttendees(w http.ResponseWriter, r *http.Request) {
- attendees, err := app.listAttendees("", "", "")
- if err != nil {
- writeError(w, err.Error(), http.StatusInternalServerError)
- return
- }
- w.Header().Set("Content-Type", "text/csv")
- w.Header().Set("Content-Disposition", `attachment; filename="attendees.csv"`)
- wr := csv.NewWriter(w)
- wr.Write([]string{"name", "email", "phone", "ticket_id", "ticket_type", "party_size", "checked_in_count", "note", "checked_in"})
- for _, a := range attendees {
- ci := "no"
- if a.CheckedIn {
- ci = "yes"
- }
- wr.Write([]string{
- a.Name, a.Email, a.Phone, a.TicketID, a.TicketType,
- strconv.Itoa(a.PartySize), strconv.Itoa(a.CheckedInCount),
- a.Note, ci,
- })
- }
- wr.Flush()
-}
diff --git a/handle_attendees_test.go b/handle_attendees_test.go
index ff2a196..c5e6adb 100644
--- a/handle_attendees_test.go
+++ b/handle_attendees_test.go
@@ -6,14 +6,14 @@ import (
"testing"
)
-func TestAttendeesListCreateDelete(t *testing.T) {
+func TestParticipantsListCreateDelete(t *testing.T) {
app := testApp(t)
admin := testAdminUser(t, app)
token := testToken(t, app, admin)
mux := testMux(app)
// Create
- req := testAuthRequest("POST", "/api/attendees", map[string]string{"name": "Titania"}, token)
+ req := testAuthRequest("POST", "/api/participants", map[string]string{"preferred_name": "Titania", "email": "titania@example.com"}, token)
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusCreated {
@@ -23,20 +23,20 @@ func TestAttendeesListCreateDelete(t *testing.T) {
id := created["id"].(float64)
// List
- req = testAuthRequest("GET", "/api/attendees", nil, token)
+ req = testAuthRequest("GET", "/api/participants", nil, token)
w = httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("list: status = %d", w.Code)
}
list := parseJSON(t, w)
- attendees := list["attendees"].([]any)
- if len(attendees) != 1 {
- t.Errorf("list: got %d, want 1", len(attendees))
+ participants := list["participants"].([]any)
+ if len(participants) != 1 {
+ t.Errorf("list: got %d, want 1", len(participants))
}
// Delete
- req = testAuthRequest("DELETE", "/api/attendees/"+itoa(int(id)), nil, token)
+ req = testAuthRequest("DELETE", "/api/participants/"+itoa(int(id)), nil, token)
w = httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusNoContent {
@@ -44,66 +44,66 @@ func TestAttendeesListCreateDelete(t *testing.T) {
}
// List again — should be empty
- req = testAuthRequest("GET", "/api/attendees", nil, token)
+ req = testAuthRequest("GET", "/api/participants", nil, token)
w = httptest.NewRecorder()
mux.ServeHTTP(w, req)
list = parseJSON(t, w)
- if a2, ok := list["attendees"].([]any); ok && len(a2) != 0 {
- t.Errorf("after delete: got %d, want 0", len(a2))
+ if ps, ok := list["participants"].([]any); ok && len(ps) != 0 {
+ t.Errorf("after delete: got %d, want 0", len(ps))
}
}
-func TestCheckInAttendeeHandler(t *testing.T) {
+func TestCheckInTicketHandler(t *testing.T) {
app := testApp(t)
admin := testAdminUser(t, app)
token := testToken(t, app, admin)
mux := testMux(app)
- app.createAttendee(Attendee{Name: "Oberon"})
- app.db.Exec(`UPDATE attendees SET party_size = 3 WHERE id = 1`)
+ p, _ := app.createParticipant(Participant{PreferredName: "Oberon", Email: "oberon@example.com"})
+ tk, _ := app.createTicket(Ticket{ParticipantID: &p.ID, Name: "Oberon", Source: "manual"})
- // Check in 1
- req := testAuthRequest("POST", "/api/attendees/1/checkin", map[string]int{"count": 1}, token)
+ req := testAuthRequest("POST", "/api/tickets/"+itoa(tk.ID)+"/checkin", nil, token)
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("checkin: status = %d\nbody: %s", w.Code, w.Body.String())
}
result := parseJSON(t, w)
- attendee := result["attendee"].(map[string]any)
- if attendee["checked_in_count"] != float64(1) {
- t.Errorf("checked_in_count = %v, want 1", attendee["checked_in_count"])
+ ticket := result["ticket"].(map[string]any)
+ if ticket["checked_in_at"] == nil {
+ t.Error("checked_in_at should be set after check-in")
}
}
-func TestGateRoleCanCheckIn(t *testing.T) {
+func TestGatekeeperRoleCanCheckIn(t *testing.T) {
app := testApp(t)
gate := testUserWithRole(t, app, "gateuser", "gatekeeper", []int{})
token := testToken(t, app, gate)
mux := testMux(app)
- app.createAttendee(Attendee{Name: "Puck"})
+ p, _ := app.createParticipant(Participant{PreferredName: "Puck", Email: "puck@example.com"})
+ tk, _ := app.createTicket(Ticket{ParticipantID: &p.ID, Name: "Puck", Source: "manual"})
- req := testAuthRequest("POST", "/api/attendees/1/checkin", nil, token)
+ req := testAuthRequest("POST", "/api/tickets/"+itoa(tk.ID)+"/checkin", nil, token)
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusOK {
- t.Errorf("gate checkin: status = %d", w.Code)
+ t.Errorf("gatekeeper checkin: status = %d", w.Code)
}
}
-func TestGateRoleCannotDelete(t *testing.T) {
+func TestGatekeeperRoleCannotDelete(t *testing.T) {
app := testApp(t)
gate := testUserWithRole(t, app, "gateuser", "gatekeeper", []int{})
token := testToken(t, app, gate)
mux := testMux(app)
- app.createAttendee(Attendee{Name: "Puck"})
+ p, _ := app.createParticipant(Participant{PreferredName: "Puck", Email: "puck@example.com"})
- req := testAuthRequest("DELETE", "/api/attendees/1", nil, token)
+ req := testAuthRequest("DELETE", "/api/participants/"+itoa(p.ID), nil, token)
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusForbidden {
- t.Errorf("gate delete: status = %d, want 403", w.Code)
+ t.Errorf("gatekeeper delete: status = %d, want 403", w.Code)
}
}
diff --git a/handle_settings.go b/handle_settings.go
index f812134..d4ed01c 100644
--- a/handle_settings.go
+++ b/handle_settings.go
@@ -79,17 +79,6 @@ func (app *App) handleUpdateSettings(w http.ResponseWriter, r *http.Request) {
app.handleGetSettings(w, r)
}
-func (app *App) handleResetAttendees(w http.ResponseWriter, r *http.Request) {
- ts := now()
- result, err := app.db.Exec(`UPDATE attendees SET deleted_at=?, updated_at=? WHERE deleted_at IS NULL`, ts, ts)
- if err != nil {
- writeError(w, err.Error(), http.StatusInternalServerError)
- return
- }
- n, _ := result.RowsAffected()
- writeJSON(w, map[string]any{"deleted": n})
-}
-
func (app *App) handleResetTickets(w http.ResponseWriter, r *http.Request) {
ts := now()
result, err := app.db.Exec(`UPDATE tickets SET deleted_at=?, updated_at=? WHERE deleted_at IS NULL`, ts, ts)
diff --git a/handle_settings_test.go b/handle_settings_test.go
index c588c88..cbc53fb 100644
--- a/handle_settings_test.go
+++ b/handle_settings_test.go
@@ -55,17 +55,19 @@ func TestUpdateSettings(t *testing.T) {
}
}
-func TestResetAttendees(t *testing.T) {
+func TestResetTickets(t *testing.T) {
app := testApp(t)
admin := testAdminUser(t, app)
token := testToken(t, app, admin)
mux := testMux(app)
- app.createAttendee(Attendee{Name: "Titania", Email: "titania@example.com"})
- app.createAttendee(Attendee{Name: "Oberon", Email: "oberon@example.com"})
+ p1, _ := app.createParticipant(Participant{PreferredName: "Titania", Email: "titania@example.com"})
+ p2, _ := app.createParticipant(Participant{PreferredName: "Oberon", Email: "oberon@example.com"})
+ app.createTicket(Ticket{ParticipantID: &p1.ID, Name: "Titania", Source: "manual"})
+ app.createTicket(Ticket{ParticipantID: &p2.ID, Name: "Oberon", Source: "manual"})
w := httptest.NewRecorder()
- mux.ServeHTTP(w, testAuthRequest("POST", "/api/settings/reset-attendees", nil, token))
+ mux.ServeHTTP(w, testAuthRequest("POST", "/api/settings/reset-tickets", nil, token))
if w.Code != 200 {
t.Fatalf("status = %d: %s", w.Code, w.Body.String())
@@ -75,20 +77,20 @@ func TestResetAttendees(t *testing.T) {
t.Fatalf("deleted = %v, want 2", result["deleted"])
}
- attendees, _ := app.listAttendees("", "", "")
- if len(attendees) != 0 {
- t.Fatalf("attendees remaining = %d, want 0", len(attendees))
+ tickets, _ := app.listTickets(nil, "")
+ if len(tickets) != 0 {
+ t.Fatalf("tickets remaining = %d, want 0", len(tickets))
}
}
-func TestResetAttendeesRequiresAdmin(t *testing.T) {
+func TestResetTicketsRequiresAdmin(t *testing.T) {
app := testApp(t)
gate := testUserWithRole(t, app, "gate1", "gatekeeper", []int{})
token := testToken(t, app, gate)
mux := testMux(app)
w := httptest.NewRecorder()
- mux.ServeHTTP(w, testAuthRequest("POST", "/api/settings/reset-attendees", nil, token))
+ mux.ServeHTTP(w, testAuthRequest("POST", "/api/settings/reset-tickets", nil, token))
if w.Code != 403 {
t.Fatalf("status = %d, want 403", w.Code)
diff --git a/handle_sync.go b/handle_sync.go
index a11d414..f2171ce 100644
--- a/handle_sync.go
+++ b/handle_sync.go
@@ -12,7 +12,6 @@ func (app *App) handleSyncPull(w http.ResponseWriter, r *http.Request) {
since := r.URL.Query().Get("since")
event, _ := app.getEvent()
- attendees, _ := app.attendeesSince(since)
participants, _ := app.listParticipants("", since)
tickets, _ := app.listTickets(nil, since)
departments, _ := app.listDepartments(since)
@@ -20,9 +19,6 @@ func (app *App) handleSyncPull(w http.ResponseWriter, r *http.Request) {
shifts, _ := app.listShifts(nil, "", since)
volunteerShifts, _ := app.listVolunteerShifts(since)
- if attendees == nil {
- attendees = []Attendee{}
- }
if participants == nil {
participants = []Participant{}
}
@@ -45,7 +41,6 @@ func (app *App) handleSyncPull(w http.ResponseWriter, r *http.Request) {
writeJSON(w, map[string]any{
"server_time": time.Now().UTC().Format("2006-01-02T15:04:05Z"),
"event": event,
- "attendees": attendees,
"participants": participants,
"tickets": tickets,
"departments": departments,
diff --git a/handle_sync_test.go b/handle_sync_test.go
index c5d759a..e4aa2af 100644
--- a/handle_sync_test.go
+++ b/handle_sync_test.go
@@ -13,7 +13,7 @@ func TestSyncPullFull(t *testing.T) {
token := testToken(t, app, admin)
mux := testMux(app)
- app.createAttendee(Attendee{Name: "Titania"})
+ app.createParticipant(Participant{PreferredName: "Titania", Email: "titania@example.com"})
dept, _ := app.createDepartment(Department{Name: "Gate"})
deptID := dept.ID
app.createVolunteer(Volunteer{Name: "Titania", DepartmentID: &deptID})
@@ -31,9 +31,9 @@ func TestSyncPullFull(t *testing.T) {
if result["server_time"] == nil {
t.Error("missing server_time")
}
- attendees := result["attendees"].([]any)
- if len(attendees) != 1 {
- t.Errorf("attendees = %d, want 1", len(attendees))
+ participants := result["participants"].([]any)
+ if len(participants) != 1 {
+ t.Errorf("participants = %d, want 1", len(participants))
}
depts := result["departments"].([]any)
if len(depts) != 1 {
@@ -47,29 +47,29 @@ func TestSyncPullIncremental(t *testing.T) {
token := testToken(t, app, admin)
mux := testMux(app)
- app.createAttendee(Attendee{Name: "Titania"})
+ p1, _ := app.createParticipant(Participant{PreferredName: "Titania", Email: "titania@example.com"})
// Backdate Titania so she falls before the "since" cutoff
- app.db.Exec(`UPDATE attendees SET updated_at = '2026-01-01T00:00:00Z' WHERE name = 'Titania'`)
+ app.db.Exec(`UPDATE participants SET updated_at = '2026-01-01T00:00:00Z' WHERE id = ?`, p1.ID)
since := "2026-01-01T12:00:00Z"
// Oberon created with default updated_at (now), which is after our since
- app.createAttendee(Attendee{Name: "Oberon"})
+ app.createParticipant(Participant{PreferredName: "Oberon", Email: "oberon@example.com"})
req := testAuthRequest("GET", "/api/sync/pull?since="+since, nil, token)
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
result := parseJSON(t, w)
- attendees := result["attendees"].([]any)
+ participants := result["participants"].([]any)
// Should only include Oberon (created after `since`)
- if len(attendees) != 1 {
- t.Errorf("incremental: got %d attendees, want 1", len(attendees))
+ if len(participants) != 1 {
+ t.Errorf("incremental: got %d participants, want 1", len(participants))
}
- if len(attendees) == 1 {
- a := attendees[0].(map[string]any)
- if a["name"] != "Oberon" {
- t.Errorf("name = %v, want Oberon", a["name"])
+ if len(participants) == 1 {
+ p := participants[0].(map[string]any)
+ if p["preferred_name"] != "Oberon" {
+ t.Errorf("preferred_name = %v, want Oberon", p["preferred_name"])
}
}
}
@@ -80,31 +80,31 @@ func TestSyncPullIncludesSoftDeleted(t *testing.T) {
token := testToken(t, app, admin)
mux := testMux(app)
- a, _ := app.createAttendee(Attendee{Name: "Titania"})
+ p, _ := app.createParticipant(Participant{PreferredName: "Titania", Email: "titania@example.com"})
// Backdate Titania's creation so the since cutoff is between creation and deletion
- app.db.Exec(`UPDATE attendees SET updated_at = '2026-01-01T00:00:00Z' WHERE id = ?`, a.ID)
+ app.db.Exec(`UPDATE participants SET updated_at = '2026-01-01T00:00:00Z' WHERE id = ?`, p.ID)
since := "2026-01-01T12:00:00Z"
// Delete updates updated_at to now(), which is after our since
- app.deleteAttendee(a.ID)
+ app.deleteParticipant(p.ID)
req := testAuthRequest("GET", "/api/sync/pull?since="+since, nil, token)
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
var result struct {
- Attendees []struct {
+ Participants []struct {
ID int `json:"id"`
DeletedAt *string `json:"deleted_at"`
- } `json:"attendees"`
+ } `json:"participants"`
}
json.Unmarshal(w.Body.Bytes(), &result)
- if len(result.Attendees) != 1 {
- t.Fatalf("got %d attendees, want 1", len(result.Attendees))
+ if len(result.Participants) != 1 {
+ t.Fatalf("got %d participants, want 1", len(result.Participants))
}
- if result.Attendees[0].DeletedAt == nil {
+ if result.Participants[0].DeletedAt == nil {
t.Error("deleted_at should be set for soft-deleted record")
}
}
diff --git a/main.go b/main.go
index ee2db53..2eedcd0 100644
--- a/main.go
+++ b/main.go
@@ -99,18 +99,6 @@ func (app *App) registerRoutes(mux *http.ServeMux) {
mux.HandleFunc("GET /api/event", auth(app.handleGetEvent))
mux.HandleFunc("PUT /api/event", auth(app.handleUpdateEvent, "admin", "ticketing"))
- mux.HandleFunc("GET /api/attendees", auth(app.handleListAttendees, "admin", "ticketing", "gatekeeper"))
- mux.HandleFunc("POST /api/attendees", auth(app.handleCreateAttendee, "admin", "ticketing"))
- mux.HandleFunc("GET /api/attendees/export", auth(app.handleExportAttendees, "admin", "ticketing"))
- mux.HandleFunc("POST /api/attendees/generate-tokens", auth(app.handleGenerateTokens, "admin", "ticketing"))
- mux.HandleFunc("GET /api/attendees/export-tokens", auth(app.handleExportTokenLinks, "admin", "ticketing"))
- mux.HandleFunc("POST /api/attendees/email-tokens", auth(app.handleEmailAllTokens, "admin", "ticketing"))
- mux.HandleFunc("GET /api/attendees/{id}", auth(app.handleGetAttendee, "admin", "ticketing", "gatekeeper"))
- mux.HandleFunc("PUT /api/attendees/{id}", auth(app.handleUpdateAttendee, "admin", "ticketing"))
- mux.HandleFunc("DELETE /api/attendees/{id}", auth(app.handleDeleteAttendee, "admin", "ticketing"))
- mux.HandleFunc("POST /api/attendees/{id}/checkin", auth(app.handleCheckInAttendee, "admin", "ticketing", "gatekeeper"))
- mux.HandleFunc("POST /api/attendees/{id}/email-token", auth(app.handleEmailToken, "admin", "ticketing"))
-
mux.HandleFunc("GET /api/participants", auth(app.handleListParticipants, "admin", "ticketing", "gatekeeper"))
mux.HandleFunc("POST /api/participants", auth(app.handleCreateParticipant, "admin", "ticketing"))
mux.HandleFunc("GET /api/participants/export", auth(app.handleExportParticipants, "admin", "ticketing"))
@@ -157,7 +145,6 @@ func (app *App) registerRoutes(mux *http.ServeMux) {
mux.HandleFunc("GET /api/settings", auth(app.handleGetSettings, "admin", "ticketing"))
mux.HandleFunc("PUT /api/settings", auth(app.handleUpdateSettings, "admin", "ticketing"))
mux.HandleFunc("POST /api/settings/test-email", auth(app.handleTestEmail, "admin", "ticketing"))
- mux.HandleFunc("POST /api/settings/reset-attendees", auth(app.handleResetAttendees, "admin", "ticketing"))
mux.HandleFunc("POST /api/settings/reset-tickets", auth(app.handleResetTickets, "admin", "ticketing"))
mux.HandleFunc("POST /api/settings/reset-volunteers", auth(app.handleResetVolunteers, "admin", "ticketing"))
mux.HandleFunc("POST /api/settings/reset-shifts", auth(app.handleResetShifts, "admin", "ticketing"))