# Plan: Data-Driven Station Configuration ## Context The station's branding — callsign, name, founding year, nonprofit status, logo, hero banner images, contact info, and frequency — is hardcoded across 10+ component templates. This makes the app a single-use template rather than a customizable platform. We need an admin-editable `StationConfig` singleton stored in PostgreSQL, a reactive frontend service, and an admin form so any station operator can rebrand the site without touching code. ## Architecture - **Backend**: New `StationConfig` SQLAlchemy model (singleton via `key='default'`), a `PUT /station-config` endpoint (admin-only), a `GET /station-config` public endpoint, and a `POST /station-config/upload` file-upload endpoint. - **Frontend**: New `StationConfigService` with a `signal` loaded via `APP_INITIALIZER`. All components that currently hardcode station data read from this service. - **Admin UI**: New "Station" tab in the existing admin dashboard with a modal form for editing all fields, including file upload inputs for images. --- ## Step 1: Backend — Model + Schemas ### `backend/app/models.py` Add `StationConfig` class using the existing `Base`. Fields: - `id`, `key` (unique, always `'default'`) - **Identity**: `callsign`, `name_primary`, `name_secondary`, `tagline`, `frequency`, `founded_year` - **Nonprofit**: `nonprofit_type`, `ein` - **Images** (URL strings): `logo_url`, `hero_background_url`, `hero_icon_url`, `hero_divider_url` - **Contact**: `address` (Text), `phone`, `email`, `website` All columns have defaults matching current hardcoded values. ### `backend/app/schemas.py` Add `StationConfigResponse` (all fields, `from_attributes: True`) and `StationConfigUpdate` (all fields `Optional`, default `None` for partial updates). --- ## Step 2: Backend — API Router ### `backend/app/api/station_config.py` (NEW) Three endpoints: 1. `GET /station-config` — Returns the singleton row. Public. 2. `PUT /station-config` — Partial update via `model_dump(exclude_unset=True)`. Requires `get_current_admin_user`. 3. `POST /station-config/upload` — Accepts `UploadFile`, saves to `uploads/` directory with UUID filename, validates `image/*` MIME type, max 5MB. Returns `{"url": "uploads/."}`. Requires `get_current_admin_user`. ### `backend/app/main.py` - Import `StationConfig` from models. - Register router: `app.include_router(station_config.router, prefix="/api/station-config", tags=["station-config"])`. - Mount `StaticFiles` for `uploads/` directory at `/uploads` (dev only; Nginx handles prod). - Create `uploads/` directory on startup. ### `backend/seed.py` - Add `STATION_CONFIG` dict with current defaults. - Add `_upsert_station_config()` helper (get-or-create by `key='default'`). - Call it in `seed()`. - Add `StationConfig.__table__.delete()` to `truncate_all()`. ### `backend/app/api/admin.py` - Add `StationConfig` to the truncation list in `reset_database()`. --- ## Step 3: Frontend — Interface + Service ### `src/app/interfaces/station-config.ts` (NEW) TypeScript interface mirroring the schema. ### `src/app/services/station-config.service.ts` (NEW) - `config = signal(DEFAULT_CONFIG)` — signal initialized with hardcoded defaults so components never see `null`. - `load()` — fetches from API, updates signal. Errors silently caught (same pattern as `appConfigFactory`). - `updateConfig(payload: Partial)` — PUT request. - `uploadImage(file: File)` — POST FormData to upload endpoint. ### `src/app/app.config.ts` Add `APP_INITIALIZER` provider that calls `stationConfigService.load()` (parallel with existing `appConfigInitProvider`). --- ## Step 4: Frontend — Admin Station Form ### `src/app/admin/admin-station-form.component.*` (3 NEW files) Modal form following the existing `admin-program-form` pattern: - **Identity section**: callsign, name_primary, name_secondary, tagline, frequency, founded_year - **Nonprofit section**: nonprofit_type, ein - **Images section**: 4 file inputs (logo, hero background, hero icon, hero divider) with thumbnail previews of current values - **Contact section**: address (textarea), phone, email, website - On submit: calls `stationConfigService.updateConfig()` with all fields as a partial update ### `src/app/admin/admin.component.ts` - Add `'station'` to `TabKey` union. - Import `StationConfigService` and `AdminStationFormComponent`. - Add `editingStation`, `showStationForm` properties. - Add `openStationForm()`, `onStationSaved()`, `onStationClosed()` methods. - In `loadAll()`, load station config. ### `src/app/admin/admin.component.html` - Add "Station" tab button. - Add station tab content: a read-only form display of current values with an "Edit" button. - Add form modal: `@if (showStationForm) { }`. --- ## Step 5: Frontend — Update All Components Each component injects `StationConfigService` and reads `configService.config()` (a signal). Since config loads via `APP_INITIALIZER` with defaults, it's never null. | Component | Changes | |-----------|---------| | **Navbar** | Logo src, brand name (callsign + name_primary + name_secondary), tagline with founded_year | | **Hero** | Background/icon/divider image srcs, title, subtitle (tagline + founded_year) | | **Footer** | Logo src, brand name, tagline, nonprofit_type, EIN, copyright year range | | **About** | Hero icon src, station name, frequency, nonprofit_type | | **Contact** | Logo src, address (split on `\n` for `
`), phone, email, frequency, website | | **Donate** | Hero icon src, station name, EIN, frequency | | **Schedule** | Logo src, frequency | | **Login** | Logo src, station name | | **App** | Set `document.title` from config in `ngOnInit` | --- ## Step 6: Verification 1. Start backend: `cd backend && KMTN_DATABASE_URL=sqlite+aiosqlite:///./kmountain.db uvicorn app.main:app --reload` 2. Verify Swagger: `GET /api/station-config` returns seeded defaults 3. Verify upload: `POST /api/station-config/upload` with an image file returns a URL 4. Start frontend: `ng serve` 5. Navigate to site — verify all pages render with seeded data 6. Log in as admin → navigate to `/admin` → click "Station" tab 7. Edit a field (e.g., change name_primary to "Summit") → save → verify navbar/hero/footer update 8. Upload a new logo image → verify it appears in navbar and footer 9. Run `curl -X POST http://localhost:8000/api/admin/reset` → verify config resets to seed defaults