working basic site admin
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user