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

6.4 KiB

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