Imported from atzsta13/studio (
AGENTS.md). Install upstream withnpx skills add atzsta13/studio. Copyright stays with the author.
AGENTS.md
Agent instructions for the Open Festival Hub. Applies to Claude Code, Cursor, Gemini, and any other AI tool working in this repo.
Start Here
Read in this order, nothing more:
- This file — rules, commands, architecture summary.
TASKS.md— the backlog. If you were given a bug or feature, it is probably already specified there with exact file paths, the fix, and a verify step. Follow that spec.- Deeper docs only if the task router below points you there.
Task router — read only what your task needs
| Your task touches… | Read / edit |
|---|---|
| Web page or feature | src/app/[festivalId]/<page>/page.tsx; shared components in src/components/; data + favorites via useInsider() from src/components/layout/insider-provider.tsx |
| Timetable (web) | docs/TIMETABLE.md first, then src/components/timetable/ |
| Android screen or feature | android/README.md first, then android/app/src/main/java/org/openfestivalhub/ui/<area>/ |
| Festival data or lineups | Edit festivals/<id>/data/*.json and festivals/<id>/config.json, then run npm run lineup:sync |
| Adding a festival | scripts/add-festival.mjs + festivals/festival-config.schema.json |
| Styling / design | docs/guides/UI_GUIDE.md |
| PWA / service worker / offline | public/sw.js + src/components/layout/pwa-loader.tsx. Every path must go through BASE_PATH; cache names in sw.js must match basePath or the live site serves stale assets |
| Something is broken | docs/guides/TROUBLESHOOTING.md |
Generated directories — NEVER edit directly
| Directory | Regenerated by |
|---|---|
public/data/ |
npm run lineup:sync (source: festivals/<id>/) |
android/app/src/main/assets/<festival-id>/ |
same sync |
public/manifest.json |
scripts/generate-manifest.mjs (runs on prebuild) |
Edit the source in festivals/<id>/ and re-run the sync — direct edits to these directories get silently overwritten.
Definition of done
- Web change:
npm run typecheck+npm run lint+npm test -- --runall green. - Android change:
./gradlew testgreen. - Data change:
npm run lineup:syncran, and the synced outputs are committed together with the source. - You touched none of the "What Was Removed" list below and violated no Hard Constraint.
Project Overview
The Open Festival Hub is a multi-festival, offline-first companion app with two parallel codebases:
- Web (Next.js 16 / React 19) — root directory. Exported as a fully static site (
output: 'export'), deployed to GitHub Pages athttps://atzsta13.github.io/studio/. - Android (Jetpack Compose / Kotlin) —
android/directory.
No backend. No API routes. No server required. Everything runs client-side from static JSON.
White-Label Mandate (CRITICAL)
This is a Config-First platform. NEVER hardcode brand names, dates, colors, or coordinates in components or logic.
- Web: Always import
FESTIVALfrom@/config/festival-engine. - Android: Always use
FestivalConfigconstants. - Data: All festival-specific data lives in
festivals/<festival-id>/data/. The build process syncs this topublic/data/and the Android assets.
Festivals
| ID | Name | Artists | Schedule | Status (as of 2026-07-26) |
|---|---|---|---|---|
sziget-2026 |
Sziget | 451 | Full timetable 431/451 | Upcoming — Aug 9–16. Flagship priority. |
novarock-2026 |
Nova Rock | 84 | Full timetable 84/84 | Past (Jun 11–14) |
frequency-2026 |
Frequency | 82 | Full timetable 82/82 | Imminent — Aug 20–22. |
area53-2026 |
Area 53 | 32 | Full timetable 32/32 | Past (music Jul 16–18, Wed 15 warm-up). 32 slots / 29 distinct acts |
ernte-punk-2026 |
Ernte Punk | 17 | TBA (null) | Upcoming, schedule not published |
rock-am-ring-2026 |
Rock am Ring | 73 | Full timetable 73/73 | Past (Jun 5–7) |
All startTime/endTime values are ISO 8601 with offset (e.g. "2026-07-16T22:30:00+02:00") — never plain "HH:mm". Festivals marked TBA have null times. features.timetable is false for festivals without schedule data.
Known bugs and the prioritized backlog live in TASKS.md — check it before starting work; your task may already be specified there.
Common Commands
Web (Next.js)
# Development
npm run dev
# Quality Control (must pass before any commit)
npm run typecheck
npm run lint
# Tests (Vitest + React Testing Library) — 190 passing
npm test -- --run
# Static export build (outputs to out/)
npm run build
Data Pipeline
# Full lineup update: scrape → clean → vibes → sync → android assets
npm run lineup:update:sziget
npm run lineup:update:novarock
npm run lineup:update:area53
npm run lineup:update:frequency
npm run lineup:update:ernte-punk
npm run lineup:update:rock-am-ring # sync only — timetable data is hand-authored from PDF
# Sync existing data to public/data/ and android assets (no scrape)
npm run lineup:sync
Android
./gradlew assembleDebug # single "Open Festival Hub" APK
./gradlew test # unit tests (no device needed)
Deployment
The URL still says /studio. The project was renamed to Open Festival Hub on 2026-07-25, but basePath tracks the GitHub Pages repo name, not the product name. It stays /studio until the repo is renamed to openfestivalhub or a custom domain is wired up. When that happens, three things move together: basePath + NEXT_PUBLIC_BASE_PATH in next.config.ts, productionUrl in every festivals/<id>/config.json, and the cache names in public/sw.js. Changing one without the others breaks the live site.
Auto-deploy is PAUSED (site under maintenance, 2026-07-10). The push trigger in .github/workflows/deploy-pages.yml is commented out, so merges to main no longer deploy. Deploy manually via Actions → "Deploy to GitHub Pages" → Run workflow (workflow_dispatch). Re-enable auto-deploy by uncommenting the push trigger once the live site is fixed.
Architecture
Web — Key Patterns
Static Export: next.config.ts has output: 'export', basePath: '/studio', trailingSlash: true.
basePath is critical: All client-side fetch() calls for JSON data MUST use the BASE_PATH helper:
import { BASE_PATH } from '@/lib/base-path';
fetch(`${BASE_PATH}/data/${festivalId}/lineup.json`);
Without this, fetches will 404 on GitHub Pages. BASE_PATH is '/studio' in production, '' locally.
Dynamic routing: Pages live in src/app/[festivalId]/. The [festivalId]/layout.tsx exports generateStaticParams() for all festivals. The [festivalId]/artist/[id]/layout.tsx pre-renders all artist pages.
Data loading: useInsider() hook (from InsiderProvider) loads lineup + config client-side from public/data/<festivalId>/lineup.json. All user state (favorites, progress) is localStorage-only, prefixed with ${FESTIVAL.id}.
Config: FestivalConfig interface in src/config/festival-engine.ts. Each festival has a config.json in festivals/<id>/. No spotifyIntegration field — Spotify OAuth was removed.
Weather: WeatherWidget fetches Open-Meteo directly (free public API, no auth). No proxy needed.
Images: All artist images are hotlinked to their original CDN — never downloaded or hosted. ArtistImage component (src/components/ui/artist-image.tsx) wraps every <img> with a © source.com attribution watermark.
UI Stack: ShadCN (Radix) for atomic components, MUI 6 for complex layouts. MUI theme synced to Tailwind via MuiRegistry. Never set MUI colors directly.
Hydration: Use isMounted pattern or suppressHydrationWarning for browser-only values.
Android — Key Patterns
- Single APK:
applicationId = "org.openfestivalhub", all festival assets bundled undersrc/main/assets/<festival-id>/ - Config:
FestivalConfig.kt— readsassets/<festival-id>/config.jsonfor the selected festival - DB: Room v2,
fallbackToDestructiveMigration(), increment@Database(version=…)for every entity change - Nav: Manual
ViewModelProvider.Factory— no Hilt - Artist images: Hotlinked to CDN with
SpotifyIsland-style attribution (social links only, no OAuth) - Haptics: Required on all interactive elements via
rememberHapticManager()
Data Flow
festivals/<id>/data/*.json
→ scripts/sync-data.mjs
→ public/data/<id>/ (served statically, fetched at runtime)
→ android/app/src/main/assets/<id>/ (bundled into APK)
What Was Removed (Do Not Re-Add)
- Spotify OAuth — Spotify revoked API access for new apps. The
spotifyIntegrationfeature flag is gone from all configs. Do not add OAuth flows,/api/auth/spotify/, match endpoints, or playlist builders. - Firebase — Project migrated away from Firebase Studio. No Firebase dependency, no
firebase.ts. - API routes — There are zero Next.js API routes. The app is fully static. Do not add
route.tsfiles. - Rate limiting / middleware — No server, so no middleware.
src/proxy.tsis deleted. - Server-side AI — No Genkit, no cloud Gemini API calls. Android uses on-device Gemini Nano (ML Kit Prompt API) only.
Hard Constraints
- NO ACCOUNTS — no login, no email, no phone numbers. 100% anonymous.
- NO SOCIAL — no feeds, no photo walls, no moderation liability.
- NO CAMERA, NO MICROPHONE — no AR, no QR scanning, no audio capture. Ever, and not even for local-only processing. Generating a QR code to display is fine; reading one is not. On-device AI is allowed (Gemini Nano via ML Kit) but its input is text + local lineup data, never a sensor feed.
- NO DATA COLLECTION — all user data stays 100% local.
- NO API ROUTES — static export only. Any
route.tsbreaks the build. - OFFLINE FIRST — Map, Guide, Lineup, and all core features must work with zero signal.
- CONFIG FIRST — no hardcoded festival names, colors, coordinates, or dates in any component.
- IMAGES — never download or host artist images. Always hotlink to source CDN. Always use
ArtistImagecomponent for attribution. - NO CRITICAL INFRA — never build ticketing, payments, cashless wristband top-up, entry scanning, or anything where failure strands an attendee at a gate. Those belong to the official festival app. This is a companion, not a replacement.
- NON-COMMERCIAL — no ads, no sponsored placement, no paid tiers, no monetisation of any kind.
- UNOFFICIAL — never imply affiliation with or endorsement by any festival. No festival logos, wordmarks, or official artwork anywhere in the repo or app.
Coding Standards
- TypeScript: Strict mode. No
any. Interfaces insrc/types/index.ts. - Icons: Lucide (Web), import individually. Android uses Vector Drawables.
- No comments unless the WHY is non-obvious.
- Tests: 190 passing — keep green. Run
npm test -- --runbefore committing.
Docs Map (keep it this lean)
| File | Purpose |
|---|---|
AGENTS.md |
This file — rules, commands, architecture summary. Canonical agent entry point. |
README.md |
Human-facing project overview |
TASKS.md |
Open/unfinished work — the only backlog file |
docs/STATUS.md |
Live state snapshot (data coverage, open issues, recently shipped) |
docs/architecture/ARCHITECTURE.md |
Deep technical reference |
docs/GOALS.md |
The why behind every feature |
docs/LANDSCAPE.md |
Market survey — who builds festival apps, why this one exists. Public-facing. |
docs/DATA_SOURCES.md |
Per-festival data provenance + verification dates |
docs/features/FEATURES.md |
Feature matrix per platform |
docs/TIMETABLE.md |
Full brief of the timetable feature |
docs/guides/MANDATES.md |
Hard constraints |
docs/guides/UI_GUIDE.md |
Design system |
docs/guides/TROUBLESHOOTING.md |
Dev troubleshooting |
CONTRIBUTING.md |
How to contribute — the "add your festival in one PR" flow |
CODE_OF_CONDUCT.md / SECURITY.md |
Community health files — rarely need edits |
android/README.md |
Android architecture + routes |
Do not create new status/snapshot docs. CURRENT.md, UPDATED.md, LLM_BRIEF.md, VERIFICATION.md, ISSUES.md, and android/HANDOFF.md were all deleted for rotting — every one of them drifted from the code within weeks. Update docs/STATUS.md and TASKS.md instead. Handoff notes between AI sessions belong in TASKS.md, not a new file.
Vendor entry files (do not fork content into them)
AGENTS.md is the single source of truth for every AI tool. The per-vendor files are thin pointers only:
| File | Tool | Mechanism |
|---|---|---|
CLAUDE.md |
Claude Code | @AGENTS.md import |
GEMINI.md |
Gemini CLI | @AGENTS.md import |
.github/copilot-instructions.md |
GitHub Copilot | Plain-text pointer (Copilot does not expand @ imports) |
| — | Cursor, Codex, and other AGENTS.md-native tools | Read AGENTS.md directly |
Never add rules to a vendor file — add them here.