Eventbrite shut down their public Event Search API (/v3/events/search)
in 2023. The provider now uses the website extractor pipeline
(mana-research + LLM) to scrape Eventbrite's public search pages.
No API key needed — same pipeline as any website source.
Also adds mana-events to generate-env.mjs for automatic .env generation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add EventProvider interface (base.ts) with fetchEvents(url, name, ctx, config)
- Refactor iCal parser and website extractor as provider adapters
- Add Eventbrite provider: API v3 search by location, category mapping,
price info extraction. Requires EVENTBRITE_API_KEY env var.
- Add Meetup provider: GraphQL API search by location, topic→category
mapping, HTML stripping. Requires MEETUP_API_KEY env var.
- Provider registry (getProvider, PROVIDER_TYPES) replaces hardcoded
switch in crawl-scheduler
- Crawl scheduler now joins sources with regions for ProviderContext
(lat/lon/radius/label) — platform providers need this for geo-search
- Source creation accepts 'eventbrite' and 'meetup' types (url optional)
- Both providers gracefully return empty when API keys unconfigured
116 tests (all passing), no regressions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add an "eventItems" mini-collection attached to each social event so
hosts can track what each guest is bringing, and so public visitors
on the share-link page can claim an item without an account.
Local-first side
- New eventItems table (Dexie v11), module config update for sync.
- LocalEventItem type + EventItem domain type, useEventItems query.
- eventItemsStore: addItem / updateItem / toggleDone / assign /
deleteItem. Every mutation pushes the full list to the server
snapshot via eventsStore.syncItems if the event is published.
- BringListEditor component on the host DetailView with assign-to-
guest dropdown, quantity, and done-checkbox.
- eventsStore.syncItems + a syncItems call in publishEvent so the
public page sees pre-existing items as soon as the event ships.
Server side
- New event_items_published table (FK cascade from events_published
so unpublishing wipes the bring list along with the snapshot).
- Host endpoints PUT/GET /events/:eventId/items: full-replace upsert
that preserves any existing claimed_by_name across host edits, max
100 items, ownership check.
- Public POST /rsvp/:token/items/:itemId/claim: name-only claim, 1×
per item (first write wins), shares the per-token hourly rate
bucket with RSVP submissions to keep the abuse surface uniform.
- GET /rsvp/:token now also returns the bring list (sorted) so the
public page renders in a single round-trip.
Public RSVP page
- Renders the bring list with claim buttons; clicking prompts for a
name and POSTs the claim, then optimistically updates the UI.
- New bring-list i18n keys for all five locales (de/en/it/fr/es).
Tests
- 15 new server tests covering host PUT/GET (insert / update / prune /
ownership / claimed-name preservation / cascade), GET /rsvp item
exposure, and POST /claim (success / double-claim / cross-token /
cancelled / validation). 50 server tests total, all green.
- E2E spec scoped to .guest-editor where the new BringListEditor
introduced a duplicate "Hinzufügen" button label.
Add bun:test integration suite that exercises every public and host
endpoint plus the rate-bucket sweeper against a real Postgres. The
Hono app factory was extracted from index.ts into app.ts so tests can
build their own instance with a header-based auth mock instead of
spinning up mana-auth + JWKS.
Coverage:
- health route smoke
- public RSVP: snapshot fetch (incl. 404, cancelled, summary
privacy), submit, validation (name, status, email, plus-ones,
cancelled), upsert dedup (incl. null/missing email parity), summary
aggregation across yes/no/maybe + plus-ones, rate-limit cap (5/h),
absolute per-token cap (20)
- host events: publish (auth, idempotent token reuse, ownership),
snapshot update (partial, ownership, 404), delete (cascade FK to
rsvps + buckets, ownership, idempotent), get rsvps (ownership)
- sweeper: removes >2h-old buckets, keeps fresh ones, no-op on empty
Mock auth lives in a small helper that injects an X-Test-User header
into a fake middleware, so the same createApp() factory powers both
production (real jwtAuth) and tests (header mock).