Admin: serie-detail, billedhåndtering, dark mode fix, source_url kolonne + verify

- Serie-detail side: sæt under serie + enkelt/bulk opret
- Billedhentning: URL→lokal download med source_url til re-sync
- Advarselsbadge (⚠) på kort med eksterne URLs (ingen streaming)
- Sync/Verify endpoints + knapper i admin UI
- Brugerstyring: opret/slet (self-delete beskyttet)
- Dark mode flash fix: render-blokerende script i <head>
- Persistent volume til billeder (pokedexter_images)
This commit is contained in:
2026-07-01 23:11:55 +02:00
parent 8361066f2f
commit 54465f751c
10 changed files with 678 additions and 113 deletions
+151 -94
View File
@@ -1,47 +1,51 @@
# Pokedexter ⚡
Pokémon TCG-samlerwebapp — bygget med **FastAPI**, **PostgreSQL**, **Nginx** og **Docker**.
Pokémon TCG-samlerwebapp — bygget med **FastAPI**, **PostgreSQL 16**, **Nginx** og **Podman/Docker**.
Administratorer opretter serier, kort og brugere. Brugere kan logge ind, browse og søge. Ingen åben registrering.
Administratorer opretter serier, t og kort. Brugere kan logge ind, browse og søge. Ingen åben registrering.
---
## Arkitektur
```
Browser ──► Nginx (:8080) ──► FastAPI (:8000) ──► PostgreSQL (:5432)
Browser ──► Nginx (:8085) ──► FastAPI (:8000) ──► PostgreSQL (:5432)
▲ │
└─── static/ ───────┘
```
- **Nginx** — reverse proxy, serverer statiske filer
- **Nginx** — reverse proxy, serverer statiske filer på port 8085
- **FastAPI** — async Python backend, Jinja2 templates
- **PostgreSQL 16** — database med SQLAlchemy (async)
- **PostgreSQL 16** — database med SQLAlchemy (async/asyncpg)
---
## Kom i gang
### Docker
Billeder bygges lokalt først, deploy sker via Portainer.
### Byg lokalt (podman)
```bash
docker compose up -d --build
podman build -t pokedexter-backend:latest -f backend/Dockerfile backend/
podman build -t pokedexter-nginx:latest -f nginx/Dockerfile nginx/
```
### Podman
### Start lokalt
```bash
./start.sh
podman-compose up -d # podman
docker compose up -d # docker
```
### Stop
```bash
./stop.sh # podman
docker compose down # docker
podman-compose down
docker compose down
```
Data i `pgdata`-volumen bevares mellem genstarter.
Data i volumes (`pokedexter_pgdata`, `pokedexter_images`) bevares mellem genstarter.
---
@@ -57,54 +61,77 @@ Brugeren oprettes automatisk ved første opstart. Skift adgangskoden efter førs
## Sådan bruges det
### Opret en serie (set)
### Opret en serie
1. Log ind på `/login` med `admin` / `admin123`
2. Gå til **Admin → Serier**
3. Indtast **kode** (f.eks. `CRI`) og **navn** (f.eks. `Chaos Rising`)
3. Indtast **navn** (f.eks. `Sword & Shield`) og evt. beskrivelse
4. Tryk **Opret**
### Tilføj kort til en serie
### Opret sæt under en serie
1. Gå til **Admin → Serier**
2. Klik på serienavnet (link)
3. Indtast **kode** (f.eks. `EVS`), **navn** (`Evolving Skies`) og evt. **release_date**
4. Tryk **Opret sæt**
Eller **bulk import**: indsæt CSV direkte i tekstfeltet:
```
Stellar Crown,SCR,Stellar Crown,2024-09-13,
Surging Sparks,SSP,Surging Sparks,2024-11-08,
```
### Tilføj kort
1. Gå til **Admin → Sæt**
2. Klik **Vis kort** på det ønskede sæt
3. Udfyld kortets detaljer (nummer, navn, type, sjældenhed, HP, stadie)
4. Tryk **Tilføj kort**
3. Udfyld **nummer**, **navn** og evt. **image URL**
4. Tryk **Opret kort**
Eksempel:
Eller **bulk import** via CSV:
| Felt | Værdi |
|------------|-----------------|
| Nummer | `068` |
| Navn | `Chaos Dragon` |
| Type | `Dragon` |
| Rarity | `Rare` |
| HP | `120` |
| Stadie | `Basic` |
```
SCR,001,Caterpie,https://images.pokemontcg.io/scarlet_ivy/1.png
SCR,002,Metapod,
```
Kortet får automatisk koden `CRI-068`.
Kortet får automatisk koden `SCR-001`.
### Billedhåndtering
Når en URL angives ved kortoprettelse:
- Billedet **downloades automatisk** og gemmes lokalt i volume'et
- Hvis download fejler, bevares URL'en som `image_path` og et ⚠-badge vises
- `Card.image_url` returnerer **aldrig eksterne URLs** — ingen streaming
#### Synkronisér billeder
**Admin → Sæt → Vis kort** findes en **Synkronisér billeder**-knap, der downloader alle eksterne billeder for sættet til lokal lagring.
#### Verificér & genopret
**Admin**-dashboardet findes:
- **Verificér & genopret** — tjekker om alle lokale billedfiler findes på disken. Manglende filer re-downloades fra `source_url`
- **Force-genhent alle** — genhenter ALLE billeder fra deres originale kilder (uanset om de findes)
Dette er nyttigt hvis volume'et afmonteres ved en fejl, eller billeder tabes ved genstart.
### Opret brugere (admin)
API — ingen HTML-side endnu:
```bash
curl -X POST http://localhost:8080/api/admin/users \
-H "Content-Type: application/json" \
-b "token=DIN_JWT_TOKEN" \
-d '{"username":"glenn","password":"hemmeligt","role":"user"}'
```
**Admin**-dashboardet under "Brugere" kan du oprette og slette brugere. Du kan ikke slette dig selv.
### Browse kort
- `/sets` — liste alle serier
- `/sets/CRI` — se alle kort i serie `CRI`
- `/card/CRI-068` — detaljer om et specifikt kort
- `/sets` — liste alle serier med sæt
- `/sets/{code}` — se alle kort i sæt `EVS`
- `/card/{full_code}` — detaljer om et specifikt kort
### Søg
- `/search?q=CRI-068` — søg på fuld kortkode
- `/search?q=dragon` — søg på kortnavn
- `/search?q=SCR-001` — søg på fuld kortkode
- `/search?q=pikachu` — søg på kortnavn
---
@@ -112,13 +139,13 @@ curl -X POST http://localhost:8080/api/admin/users \
### Offentlige endpoints
| Metode | Sti | Beskrivelse |
|--------|----------------|------------------------|
| GET | `/` | Forside |
| GET | `/sets` | Liste alle serier |
| GET | `/sets/{code}` | Kort i en serie |
| GET | `/search` | Søgeside |
| GET | `/api/search` | Søg (JSON) |
| Metode | Sti | Beskrivelse |
|--------|----------------------|------------------------|
| GET | `/` | Forside |
| GET | `/sets` | Liste alle serier |
| GET | `/sets/{code}` | Kort i et sæt |
| GET | `/search` | Søgeside |
| GET | `/api/search` | Søg (JSON) |
| GET | `/api/cards/{full_code}` | Enkelt kort (JSON) |
### Auth endpoints
@@ -131,19 +158,28 @@ curl -X POST http://localhost:8080/api/admin/users \
### Admin endpoints (kræver admin)
| Metode | Sti | Beskrivelse |
|--------|-------------------------------------|--------------------------------|
| GET | `/admin` | Admin dashboard |
| GET | `/admin/sets` | Administrér serier |
| GET | `/admin/cards/{code}` | Administrér kort i serie |
| POST | `/api/admin/users` | Opret bruger |
| GET | `/api/admin/users` | List brugere |
| POST | `/api/admin/sets` | Opret serie |
| GET | `/api/admin/sets` | List serier |
| DELETE | `/api/admin/sets/{id}` | Slet serie |
| POST | `/api/admin/sets/{code}/cards` | Opret kort |
| GET | `/api/admin/sets/{code}/cards` | List kort i serie |
| DELETE | `/api/admin/cards/{id}` | Slet kort |
| Metode | Sti | Beskrivelse |
|--------|--------------------------------------------|-------------------------------------|
| GET | `/admin` | Admin dashboard |
| GET | `/admin/series` | Administrér serier |
| GET | `/admin/series/{name}` | Serie-detail med sæt |
| GET | `/admin/sets` | Administrér sæt |
| GET | `/admin/sets/{code}` | Administrér kort i sæt |
| POST | `/api/admin/users` | Opret bruger |
| DELETE | `/api/admin/users/{id}` | Slet bruger (ikke dig selv) |
| POST | `/api/admin/series` | Opret serie |
| POST | `/api/admin/series/csv` | Bulk import af serier (CSV) |
| DELETE | `/api/admin/series/{id}` | Slet serie |
| POST | `/api/admin/series/{name}/sets` | Opret sæt under serie |
| POST | `/api/admin/series/{name}/sets/csv` | Bulk import af sæt (CSV) |
| POST | `/api/admin/sets/{code}/cards` | Opret kort |
| POST | `/api/admin/sets/{code}/cards/csv` | Bulk import af kort (CSV) |
| DELETE | `/api/admin/cards/{id}` | Slet kort |
| POST | `/api/admin/cards/{id}/upload` | Upload billede (fil) |
| POST | `/api/admin/sets/{code}/sync-images` | Synkronisér billeder for sæt |
| POST | `/api/admin/sync-all-images` | Synkronisér alle billeder |
| POST | `/api/admin/verify-images` | Verificér og genopret billeder |
| POST | `/api/admin/verify-images?force=true` | Force-genhent alle billeder |
---
@@ -157,38 +193,44 @@ User
role String ("admin" | "user")
created_at DateTime
Serie
id Integer (PK)
name String (unique) — "Sword & Shield"
description Text (optional)
created_at DateTime
sets ► Set[]
Set
id Integer (PK)
code String (unique) — "CRI"
name String — "Chaos Rising"
serie_id Integer (FK → series.id)
code String (unique) — "EVS"
name String — "Evolving Skies"
release_date String (optional)
description Text (optional)
created_at DateTime
serie ► Serie
cards ► Card[]
Card
id Integer (PK)
set_id Integer (FK → sets.id)
number String — "068"
name String — "Chaos Dragon"
type String — "Dragon", "Fire", ...
rarity String — "Common", "Uncommon", "Rare", ...
hp Integer
stage String — "Basic", "Stage 1", "Stage 2"
description Text
image_url String (optional)
number String — "050"
name String — "Raichu"
image_path String (optional) — lokal sti eller URL
source_url String (optional) — original ekstern URL til re-sync
set ► Set
full_code [property] — "CRI-068"
full_code [property] — "EVS-050"
image_url [property] — "/static/..." eller None (ingen eksterne URLs)
```
---
## Miljøvariable
| Variabel | Standard | Beskrivelse |
|---------------|----------------------------------|----------------------|
| Variabel | Standard | Beskrivelse |
|----------------|----------------------------------|----------------------|
| `DATABASE_URL` | `postgresql+asyncpg://...` | PostgreSQL forbindelse |
| `SECRET_KEY` | `skift-mig-til-en-sikker-noegle` | JWT signeringsnøgle |
| `SECRET_KEY` | `skift-mig-til-en-sikker-noegle` | JWT signeringsnøgle |
---
@@ -197,22 +239,26 @@ Card
```
pokedexter/
├── docker-compose.yml
├── start.sh # podman start
├── stop.sh # podman stop
├── start.sh # podman start
├── stop.sh # podman stop
├── README.md
├── backend/
│ ├── Dockerfile
│ ├── requirements.txt
│ └── app/
│ ├── main.py # FastAPI app, middleware
│ ├── database.py # async engine + session
│ ├── models.py # SQLAlchemy modeller
│ ├── auth.py # bcrypt + JWT
│ ├── routers/
│ │ ── auth.py # login/logout + admin user mgmt
│ ├── admin.py # admin CRUD for sets/cards
│ │ ── cards.py # browse + search
├── templates/ # Jinja2 HTML templates
│ └── static/ # CSS, JS, billeder
│ ├── main.py # FastAPI app, middleware
│ ├── database.py # async engine + session
│ ├── models.py # SQLAlchemy modeller
│ ├── auth.py # bcrypt + JWT
│ ├── static/
│ │ ── images/ # uploadet/downloadede billeder (volume)
├── templates/ # Jinja2 HTML templates
│ │ ── base.html # layout m. dark mode toggle
│ └── admin/ # admin UI templates
│ └── routers/
│ ├── auth.py # login/logout + admin user mgmt
│ ├── admin.py # admin CRUD for series/sets/cards
│ └── cards.py # browse + search
├── nginx/
│ ├── Dockerfile
│ └── nginx.conf
@@ -224,22 +270,33 @@ pokedexter/
## Udvikling
Genstart backend efter kodeændringer:
### Genstart backend efter kodeændringer
```bash
docker compose up -d --build backend
# eller med podman:
# Byg nyt image
podman build -t pokedexter-backend:latest -f backend/Dockerfile backend/
# Genstart via Portainer, eller:
podman stop pokedexter-backend && podman rm pokedexter-backend
podman build -t pokedexter-backend ./backend
# ... og kør igen med podman run ...
podman-compose up -d backend
```
### Dark mode
Dark mode gemmes i `localStorage` og sættes via render-blokerende script i `<head>` — ingen flash ved side-skift.
### Billeder og volumes
Billeder gemmes i volume `pokedexter_images` mountet på `/app/static/images`.
Volume'et skal være mountet før første hentning — ellers går billeder tabt ved container-genstart.
Hvis billeder mangler: brug **Verificér & genopret** på admin-dashboardet.
---
## Planlagte features
- [ ] Personlig mappe — brugere kan markere kort de ejer/samler
- [ ] Kortbilleder (upload eller URL)
- [ ] Filtrering på type, sjældenhed, HP
- [ ] Dark mode toggle gemt i localStorage *(done)*
- [ ] Mobilvenlig navigation *(done)*
- [ ] Kort-detalje side med artwork
- [ ] Eksport af samling til CSV