Files
kmtnflower/.claude/plans/station-config-plan.md
2026-06-21 02:56:49 +00:00

126 lines
6.4 KiB
Markdown

# 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<StationConfig>` 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/<uuid>.<ext>"}`. 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<StationConfig>(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<StationConfig>)` — 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) { <app-admin-station-form ...> }`.
---
## 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 `<br>`), 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