Per la mia collezione di carte Pokemon (poteva essere di monete o altro) uso un App a pagamento sfruttabile via PC, Tablet etc.. per catalogare tutte le carte. Il costo però è piuttosto alto, circa 50 euro ad anno, ho deciso quindi di creare qualcosa di “home made”. Con un catalogo gestito in autonomia, si evita anche di condividere le proprie informazioni, con persone di cui spesso non si sa nulla.
In pochi passi si otterrà un risultato come quello dell’immagine sotto riportata dove le carte sono tutte in bianco e nero perchè il DB è vuoto, sarebbe a colori se posseduta.
Stato progetto (14/09/2026)
✅ VM Debian
✅ Docker
✅ PostgreSQL
✅ FastAPI
✅ Import catalogo Pokémon
✅ 20.479 carte
✅ Ricerca carte
✅ Collezione personale
✅ Dashboard Web
✅ Carte possedute evidenziate
⬜ Login utenti
⬜ Wishlist
⬜ Gestione set multipli
Avendo un PC su cui ho installato il sistema di virtualizzazione Proxmox “open source”, posso creare facilmente virtual machine, di seguito semplicemente “VM”. Avendo anche una linea internet in Fibra da 2.5Gbit, anche la parte di connettività è eccellente.
Partiamo quindi con la creazione di una VM, va bene anche un PC dedicato, un raspberry etc.. l’importante è che abbia un minimo di caratteristiche HW come 2 CPU, Ram 4 gb e un disco da 40 gb o superiore. Nel mio caso ho creato una nuova VM linux su base Debian ultima release, dando 2 CPU, 4G di Ram e 40Gb di disco.
Installazione Debian
Non essendo una guida su come si installa un sistema operativo sorvolerò sulle fasi iniziali, descrivendo solo le parti che ci permettano di avere un server minimale. Durante l’installazione dare come nome macchina “pokemon” o quello che volete, nella sezione dominio non mettere nulla e infine alla schermata “Software selection” lascia selezionate solo le opzioni sotto riportate:
✅ SSH server
✅ Standard system utilities
e deseleziona:
❌ Debian desktop environment
❌ GNOME
❌ KDE
❌ XFCE
❌ Web server
❌ Tutto il resto
Conclusa l’installazione eseguire i canonici aggiornamenti del S.O .
Da root esegui:
apt update
apt full-upgrade -y
al termine:
reboot
Rientra poi nella VM e verifica che sia tutto ok:
uname -a
apt list --upgradable
Se il secondo comando non mostra pacchetti da aggiornare (o ne mostra pochissimi), siamo pronti per il passo successivo.
Da root esegui:
apt install -y docker.io docker-compose-v2 git curl nano
Al termine verifica:
docker --version
e anche:
docker compose version
Dovresti ottenere qualcosa di simile: Docker version 28.x, Docker Compose version v2.x
Benvenuti su Paesi al Tramonto, il mio mondo visto dall’alto! Qui troverete spettacolari riprese aeree in alta definizione, catturate con il mio drone al tramonto o in condizioni di luce uniche. Esploriamo insieme i paesi italiani, castelli, ville, luoghi speciali e paesaggi mozzafiato..
La mia flotta è costituita da due droni un piccolo DJI Mini 2SE e un DJI Air 2S.
Ho conseguito le licenza di volo ENAV A1, A2 e A3 e a breve otterrò la STS01 e 02.
L’attività principale svolta è il girare mini documentati aerei per il mio canale YouTube “Paesi al Tramonto”, di seguito gli articoli su gli ultimi video pubblicati.
Prossimo passo: progettare il database. Prima di creare le tabelle voglio che decidiamo bene come gestire la collezione. Una proposta di base è:
Catalogo Ufficiale (contiene tutte le carte esistenti)
- sets
- cards
Collezione Paolo (Marco.. Giada etc..) contiene solo quelle possedute
- collection
Carta: Charizard ex 125/197
- Quantità: 3
- Lingua: ITA
- Condizione: Near Mint
e una seconda riga (per le carte aggiuntive con lingua diversa)
- Quantità: 1
- Lingua: ENG
- Condizione: Mint
Il DB sarà anche multiutente
Creazione schema database V1
Entriamo in PostgreSQL:
docker exec -it pokemon-postgres psql -U pokemon -d pokemon
Creiamo la Tabella utentidigitando:
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
display_name VARCHAR(100),
is_admin BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Per la Tabella Espansioni digitare:
CREATE TABLE sets (
id VARCHAR(50) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
series VARCHAR(255),
release_date DATE,
total_cards INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Per la Tabella Carte digitare:
CREATE TABLE cards (
id VARCHAR(100) PRIMARY KEY,
set_id VARCHAR(50) NOT NULL,
card_number VARCHAR(20),
name VARCHAR(255) NOT NULL,
rarity VARCHAR(100),
supertype VARCHAR(100),
image_small TEXT,
image_large TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_cards_set
FOREIGN KEY (set_id)
REFERENCES sets(id)
);
Per la Tabella Collezioni digitare:
CREATE TABLE collection (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
card_id VARCHAR(100) NOT NULL,
quantity INTEGER NOT NULL DEFAULT 1,
language VARCHAR(10) NOT NULL,
condition VARCHAR(30) NOT NULL,
variant VARCHAR(30) NOT NULL,
graded BOOLEAN DEFAULT FALSE,
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_collection_user
FOREIGN KEY (user_id)
REFERENCES users(id),
CONSTRAINT fk_collection_card
FOREIGN KEY (card_id)
REFERENCES cards(id)
);
Per la Tabella Wishlist digitare:
CREATE TABLE wishlist (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
card_id VARCHAR(100) NOT NULL,
priority SMALLINT DEFAULT 1,
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_wishlist_user
FOREIGN KEY (user_id)
REFERENCES users(id),
CONSTRAINT fk_wishlist_card
FOREIGN KEY (card_id)
REFERENCES cards(id)
);
Per la Tabella Indici digitare:
CREATE INDEX idx_cards_name
ON cards(name);
CREATE INDEX idx_cards_set
ON cards(set_id);
CREATE INDEX idx_collection_user
ON collection(user_id);
CREATE INDEX idx_collection_card
ON collection(card_id);
Primo utente amministratore:
INSERT INTO users (
username,
email,
password_hash,
display_name,
is_admin
)
VALUES (
'paolo',
'paolo@local',
'TEMP',
'Paolo',
TRUE
);
Verifica finale:
\dt
Dovresti vedere:
cards
collection
sets
users
wishlist
Per verifica l'utente:
SELECT * FROM users;
Prossimo passo: Backend FastAPI
/opt/pokemon
├── backend
├── frontend
├── postgres
├── imports
└── docker-compose.yml
Creiamo il backend:
cd /opt/pokemon/backend
mkdir app
cd app
touch main.py
Verifica: (se non hai tree: apt install tree -y)
tree /opt/pokemon/backend
Dovresti vedere:
backend
└── app
└── main.py
Esegui:
mkdir -p /opt/pokemon/backend/app
touch /opt/pokemon/backend/app/main.py
Adesso creiamo il primo backend minimale e lo facciamo girare in Docker. Passo 1: file Python
nano /opt/pokemon/backend/app/main.py
e inserisci:
from fastapi import FastAPI
app = FastAPI(
title="Pokemon Collection API"
)
@app.get("/")
def root():
return {
"app": "Pokemon Collection",
"status": "running"
}
@app.get("/health")
def health():
return {
"status": "ok"
}
Salva ed esci con CTRL X e poi Y e invio
nano /opt/pokemon/backend/requirements.txt
e inserisci:
fastapi
uvicorn[standard]
Salva ed esci con CTRL X e poi Y e invio
nano /opt/pokemon/backend/Dockerfile
e inserisci:
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app ./app
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Salva ed esci con CTRL X e poi Y e invio
nano /opt/pokemon/docker-compose.yml
e sostituisci il contenuto con:
services:
postgres:
image: postgres:17
container_name: pokemon-postgres
restart: unless-stopped
environment:
POSTGRES_DB: pokemon
POSTGRES_USER: pokemon
POSTGRES_PASSWORD: pokemon123
ports:
- "5432:5432"
volumes:
- ./postgres:/var/lib/postgresql/data
api:
build: ./backend
container_name: pokemon-api
restart: unless-stopped
ports:
- "8000:8000"
depends_on:
- postgres
Salva ed esci con CTRL X e poi Y e invio
Build e avvio
cd /opt/pokemon
docker-compose up -d --build
partirà la compilazione, se è andato tutto bene vedrai:
Container pokemon-postgres Running Container pokemon-api Started
Per ulteriore verifixa comando:
curl http://localhost:8000/health
la risposta dovrebbe essere {"status":"ok"}
Se tutto funziona sarà già visibile l'accesso web al db dall'indirizzo http://ip vm:8000/docs dove ip vm è l'indirizzo ip della tua virtual machine o PC, dovrebbe comparire la pagina "Pokemon Collection API"
Creiamo il file database.py
nano /opt/pokemon/backend/app/database.py
con dentro:
from sqlalchemy import create_engine
DATABASE_URL = "postgresql://pokemon:pokemon123@postgres:5432/pokemon"
engine = create_engine(DATABASE_URL)
Salva ed esci con CTRL X e poi Y e invio
Creiamo il file requirements.txt
nano /opt/pokemon/backend/requirements.txt
Sostituisci il contenuto con:
fastapi
uvicorn[standard]
sqlalchemy
psycopg2-binary
Ricostruisci il container
cd /opt/pokemon
docker-compose down
docker-compose build --no-cache
docker-compose up -d
Verifichiamo aprendo la pagina web http://vm ip:8000/db dovrebbe comparire {"users":1}
Creiamo le API reali
nano /opt/pokemon/backend/app/main.py
Aggiungi in calce:
@app.get("/users")
def get_users():
with engine.connect() as conn:
result = conn.execute(
text("""
SELECT
id,
username,
display_name,
is_admin
FROM users
ORDER BY id
""")
)
rows = result.mappings().all()
return rows
@app.get("/sets")
def get_sets():
with engine.connect() as conn:
result = conn.execute(
text("""
SELECT *
FROM sets
ORDER BY name
""")
)
rows = result.mappings().all()
return rows
@app.get("/cards")
def get_cards():
with engine.connect() as conn:
result = conn.execute(
text("""
SELECT *
FROM cards
ORDER BY name
LIMIT 100
""")
)
rows = result.mappings().all()
return rows
Salva ed esci con CTRL X e poi Y e invio
Ricostruzione solita con:
cd /opt/pokemon
docker-compose up -d --build
finita la ricompilazione dovrebbe dare nuovamente "Runnig" e "Started" Verifichiamo aprendo la pagina web http://vm ip:8000/db dovrebbe comparire:
[
{
"id": 1,
"username": "paolo",
"display_name": "Paolo",
"is_admin": true
}
]
Preparare l’importer
nano /opt/pokemon/backend/requirements.txt
che contiene già 4 righe, aggiungiamo in fondo:
requests
Salva ed esci e ricostruiamo nuovamente l'indice:
cd /opt/pokemon
docker-compose build --no-cache api
docker-compose up -d
Creare lo script di importazione
nano /opt/pokemon/imports/import_sets.py
Inserire:
import requests
import psycopg2
DB_HOST = "127.0.0.1"
DB_NAME = "pokemon"
DB_USER = "pokemon"
DB_PASS = "pokemon123"
url = "https://api.pokemontcg.io/v2/sets"
response = requests.get(url)
data = response.json()["data"]
conn = psycopg2.connect(
host=DB_HOST,
dbname=DB_NAME,
user=DB_USER,
password=DB_PASS
)
cur = conn.cursor()
for s in data:
cur.execute("""
INSERT INTO sets (
id,
name,
series,
release_date,
total_cards
)
VALUES (%s,%s,%s,%s,%s)
ON CONFLICT (id)
DO NOTHING
""", (
s["id"],
s["name"],
s.get("series"),
s.get("releaseDate"),
s.get("total")
))
conn.commit()
print(f"Importati {len(data)} set")
cur.close()
conn.close()
Salva ed esciInizia adesso la fase di importazione dati vera..
apt install -y python3-requests python3-psycopg2
python3 /opt/pokemon/imports/import_sets.py
Mi aspetto una risposta del tipo:
Importati 174 set
Entra in PostgreSQL per una verifica rapida:
docker exec -it pokemon-postgres psql -U pokemon -d pokemon
al prompt pokemon=# digitare:
SELECT COUNT(*) FROM sets;
dovrebbe rispondere nuovamente 174 per uscire \q
nano /opt/pokemon/imports/import_cards.py
e inseriamo:
import requests
import psycopg2
import time
DB_HOST = "127.0.0.1"
DB_NAME = "pokemon"
DB_USER = "pokemon"
DB_PASS = "pokemon123"
conn = psycopg2.connect(
host=DB_HOST,
dbname=DB_NAME,
user=DB_USER,
password=DB_PASS
)
cur = conn.cursor()
page = 1
page_size = 100
total_imported = 0
while True:
print(f"\nPagina {page}")
while True:
try:
response = requests.get(
"https://api.pokemontcg.io/v2/cards",
params={
"page": page,
"pageSize": page_size
},
timeout=60
)
if response.status_code != 200:
print(
f"Errore HTTP {response.status_code} sulla pagina {page}"
)
time.sleep(10)
continue
data = response.json()
break
except Exception as e:
print(f"Errore: {e}")
time.sleep(10)
cards = data["data"]
if not cards:
print("\nImport completato")
break
for card in cards:
cur.execute("""
INSERT INTO cards (
id,
set_id,
card_number,
name,
rarity,
supertype,
image_small,
image_large
)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s)
ON CONFLICT (id)
DO NOTHING
""", (
card["id"],
card["set"]["id"],
card.get("number"),
card.get("name"),
card.get("rarity"),
card.get("supertype"),
card.get("images", {}).get("small"),
card.get("images", {}).get("large")
))
conn.commit()
total_imported += len(cards)
print(f"Importate finora: {total_imported}")
page += 1
time.sleep(1)
print(f"\nTotale carte elaborate: {total_imported}")
cur.close()
conn.close()
Salva ed esci e lancia lo script(ho inserito un controllo di errore che non interrompe la copia, su ogni pagina da 100 carte riproverà finchè non riesce).
python3 /opt/pokemon/imports/import_cards.py
L'importazione durerà diverse ore, quando sarà conclusa verifichiamo entrando nel DB:
docker exec -it pokemon-postgres psql -U pokemon -d pokemon
SELECT COUNT(*) FROM cards;
Mi attendo una risposta del tipo:
count
-------
20479
(1 row)
Usciamo dal DB con \q
Adesso facciamo la prima funzione utile
nano /opt/pokemon/backend/app/main.py
Alla fine del file aggiungere in calce:
@app.get("/cards/search")
def search_cards(name: str):
with engine.connect() as conn:
result = conn.execute(
text("""
SELECT
c.id,
c.name,
c.card_number,
c.rarity,
s.name AS set_name,
c.image_small
FROM cards c
JOIN sets s
ON s.id = c.set_id
WHERE LOWER(c.name) LIKE LOWER(:search)
ORDER BY c.name
LIMIT 50
"""),
{"search": f"%{name}%"}
)
return result.mappings().all()
Ricostruire il DB:
cd /opt/pokemon
docker-compose up -d --build
finita la ricompilazione dovrebbe dare nuovamente "Runnig" e "Started" Verifichiamo aprendo la pagina web http://vm ip:8000/cards/search?name=pikachu dovrebbe mostrare tutte le carte trovate con risultati del tipo:
[{"id":"smp-SM109","name":"Ash's Pikachu","card_number":"SM109","rarity":"Promo","set_name":"SM Black Star Promos","image_small":"https://images.pokemontcg.io/smp/SM109.png"}, .....
Adesso creiamo la prima API che modifica il database
nano /opt/pokemon/backend/app/main.py
cancellate tutto e sostituite con:
from fastapi import FastAPI
from pydantic import BaseModel
from sqlalchemy import text
from app.database import engine
app = FastAPI(
title="Pokemon Collection API"
)
class CollectionAdd(BaseModel):
user_id: int
card_id: str
language: str
condition: str
variant: str
graded: bool = False
quantity: int = 1
@app.get("/")
def root():
return {
"app": "Pokemon Collection",
"status": "running"
}
@app.get("/health")
def health():
return {
"status": "ok"
}
@app.get("/db")
def db_test():
with engine.connect() as conn:
result = conn.execute(
text("SELECT COUNT(*) FROM users")
)
count = result.scalar()
return {
"users": count
}
@app.get("/users")
def get_users():
with engine.connect() as conn:
result = conn.execute(
text("""
SELECT
id,
username,
display_name,
is_admin
FROM users
ORDER BY id
""")
)
rows = result.mappings().all()
return rows
@app.get("/sets")
def get_sets():
with engine.connect() as conn:
result = conn.execute(
text("""
SELECT *
FROM sets
ORDER BY name
""")
)
rows = result.mappings().all()
return rows
@app.get("/cards")
def get_cards():
with engine.connect() as conn:
result = conn.execute(
text("""
SELECT *
FROM cards
ORDER BY name
LIMIT 100
""")
)
rows = result.mappings().all()
return rows
@app.get("/cards/search")
def search_cards(name: str):
with engine.connect() as conn:
result = conn.execute(
text("""
SELECT
c.id,
c.name,
c.card_number,
c.rarity,
s.name AS set_name,
c.image_small
FROM cards c
JOIN sets s
ON s.id = c.set_id
WHERE LOWER(c.name) LIKE LOWER(:search)
ORDER BY c.name
LIMIT 50
"""),
{"search": f"%{name}%"}
)
return result.mappings().all()
@app.post("/collection/add")
def add_to_collection(item: CollectionAdd):
with engine.connect() as conn:
conn.execute(
text("""
INSERT INTO collection (
user_id,
card_id,
quantity,
language,
condition,
variant,
graded
)
VALUES (
:user_id,
:card_id,
:quantity,
:language,
:condition,
:variant,
:graded
)
ON CONFLICT (
user_id,
card_id,
language,
condition,
variant,
graded
)
DO UPDATE
SET quantity = collection.quantity + EXCLUDED.quantity
"""),
{
"user_id": item.user_id,
"card_id": item.card_id,
"quantity": item.quantity,
"language": item.language,
"condition": item.condition,
"variant": item.variant,
"graded": item.graded
}
)
conn.commit()
return {
"success": True,
"card_id": item.card_id
}
Salvare e uscire e ricostruire il DB:
cd /opt/pokemon
docker-compose up -d --build
finita la ricompilazione dovrebbe dare nuovamente "Runnig" e "Started" Verifichiamo aprendo la pagina web http://vm ip:8000/docs dovrebbe mostrare una pagina come la seguente
Creiamo la Collection basata sul multiutente
nano /opt/pokemon/backend/app/main.py
Aggiungere alla fine del file:
@app.get("/collection")
def get_collection(user_id: int):
with engine.connect() as conn:
result = conn.execute(
text("""
SELECT
col.id,
c.name,
c.card_number,
s.name AS set_name,
col.quantity,
col.language,
col.condition,
col.variant,
col.graded
FROM collection col
JOIN cards c
ON c.id = col.card_id
JOIN sets s
ON s.id = c.set_id
WHERE col.user_id = :user_id
ORDER BY c.name
"""),
{"user_id": user_id}
)
return result.mappings().all()
Salvare e uscire e ricostruire il DB:cd /opt/pokemon
docker-compose up -d --build
Aggiungere l’endpoint DELETE
nano /opt/pokemon/backend/app/main.py
Aggiungere alla fine del file:
@app.delete("/collection/{item_id}")
def delete_collection_item(item_id: int):
with engine.connect() as conn:
result = conn.execute(
text("""
DELETE FROM collection
WHERE id = :item_id
"""),
{"item_id": item_id}
)
conn.commit()
return {
"success": True,
"deleted_id": item_id
}
Salvare e uscire e ricostruire il DB:cd /opt/pokemon
docker-compose up -d --build
Aggiungi il modello Pydantic
nano /opt/pokemon/backend/app/main.py
Aggiungere al principio del file:
from fastapi.responses import FileResponse
Sotto class CollectionAdd(BaseModel): aggiungere:
class CollectionUpdate(BaseModel):
quantity: int
language: str
condition: str
variant: str
graded: bool
Aggiungere alla fine del file:
@app.put("/collection/{item_id}")
def update_collection_item(
item_id: int,
item: CollectionUpdate
):
with engine.connect() as conn:
conn.execute(
text("""
UPDATE collection
SET
quantity = :quantity,
language = :language,
condition = :condition,
variant = :variant,
graded = :graded
WHERE id = :item_id
"""),
{
"item_id": item_id,
"quantity": item.quantity,
"language": item.language,
"condition": item.condition,
"variant": item.variant,
"graded": item.graded
}
)
conn.commit()
return {
"success": True,
"updated_id": item_id
}
@app.get("/stats")
def get_stats(user_id: int):
with engine.connect() as conn:
result = conn.execute(
text("""
SELECT
COALESCE(SUM(quantity), 0) AS total_cards,
COUNT(*) AS unique_cards,
COUNT(DISTINCT c.set_id) AS sets_started
FROM collection col
JOIN cards c
ON c.id = col.card_id
WHERE col.user_id = :user_id
"""),
{"user_id": user_id}
)
stats = result.mappings().first()
return stats
@app.get("/set/{set_id}/cards")
def get_set_cards(set_id: str):
with engine.connect() as conn:
result = conn.execute(
text("""
SELECT
id,
name,
card_number,
rarity,
image_small
FROM cards
WHERE set_id = :set_id
ORDER BY
CASE
WHEN card_number ~ '^[0-9]+$'
THEN card_number::integer
END NULLS LAST,
card_number
"""),
{"set_id": set_id}
)
return result.mappings().all()
@app.get("/dashboard")
def dashboard():
return FileResponse(
"app/static/dashboard.html"
)
Salvare e uscire e ricostruire il DB:cd /opt/pokemon
docker-compose up -d --build
Creiamo la Dashboard
mkdir -p /opt/pokemon/backend/app/static
nano /opt/pokemon/backend/app/static/dashboard.html
Inserire:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Pokemon Dashboard</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2"></script>
</head>
<body>
<h1>Pokemon Dashboard</h1>
<canvas id="statsChart" width="800" height="400"></canvas>
<script>
async function loadStats() {
const response = await fetch('/stats?user_id=1');
const stats = await response.json();
Chart.register(ChartDataLabels);
new Chart(document.getElementById('statsChart'), {
type: 'bar',
data: {
labels: [
'Carte Totali',
'Carte Diverse',
'Set Iniziati'
],
datasets: [{
label: 'Statistiche',
data: [
stats.total_cards,
stats.unique_cards,
stats.sets_started
],
backgroundColor: [
'#1976d2',
'#388e3c',
'#f57c00'
]
}]
},
options: {
responsive: true,
plugins: {
datalabels: {
anchor: 'end',
align: 'top',
color: '#000',
font: {
weight: 'bold',
size: 16
}
}
},
scales: {
y: {
beginAtZero: true
}
}
}
});
}
loadStats();
</script>
</body>
</html>
Salva e chiudiRicarica i file nel container:
cd /opt/pokemon
docker-compose down
docker-compose build --no-cache
docker-compose up -d
Verifichiamo aprendo la pagina web http://vm ip:8000/dashboard dovrebbe mostrare una pagina con un grafico
Creiamo finalmente la vera Home Page
nano /opt/pokemon/backend/app/static/index.html
Inseriamo:
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="utf-8">
<title>Pokemon DB</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="header">
<div class="logo">Pokemon DB</div>
<div class="user">Paolo ▼</div>
</div>
<div class="container">
<div class="set-info">
<label>Set:</label>
<select>
<option>Pitch Black</option>
</select>
</div>
<div class="toolbar">
<button>Ricerca</button>
<button>Aggiungi</button>
<button>Collezione</button>
<button>Statistiche</button>
</div>
<div class="set-info">
<h2>Pitch Black</h2>
<p>Tutte le carte del set</p>
</div>
<div id="card-grid" class="card-grid"></div>
</div>
<script>
async function loadCards() {
const response = await fetch('/set/me5/cards');
const cards = await response.json();
const grid = document.getElementById('card-grid');
cards.forEach(card => {
const div = document.createElement('div');
div.className = 'card';
div.innerHTML = `
<img src="${card.image_small}" alt="${card.name}">
<div class="card-body">
<div class="card-number">#${card.card_number}</div>
<div class="card-name">${card.name}</div>
<div class="card-rarity">${card.rarity || ''}</div>
</div>
<button>Aggiungi</button>
`;
grid.appendChild(div);
});
}
loadCards();
</script>
</body>
</html>
Salva ed esci poi
nano /opt/pokemon/backend/app/static/style.css
e inserisci:
body {
margin: 0;
padding: 0;
background: #eef2f5;
font-family: Arial, sans-serif;
}
.header {
background: white;
height: 70px;
padding: 0 30px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.logo {
font-size: 28px;
font-weight: bold;
color: #1976d2;
}
.user {
font-size: 18px;
}
.container {
padding: 20px;
}
.set-info {
background: white;
padding: 20px;
border-radius: 12px;
margin-bottom: 20px;
}
.toolbar {
background: white;
padding: 15px;
border-radius: 12px;
margin-bottom: 20px;
display: flex;
gap: 10px;
}
.toolbar button {
border: none;
border-radius: 8px;
padding: 10px 15px;
cursor: pointer;
}
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, 180px);
gap: 20px;
}
.card {
width: 180px;
}
.card img {
width: 100%;
display: block;
}
.card-body {
padding: 10px;
}
.card-number {
font-weight: bold;
}
.card-name {
margin-top: 5px;
font-size: 18px;
}
.card-rarity {
color: #666;
margin-top: 5px;
}
.card button {
width: 100%;
border: none;
padding: 12px;
background: #1976d2;
color: white;
cursor: pointer;
}
Salva ed esci poi sostituisci il primo blocco di main.py con questo:
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from fastapi import FastAPI
from pydantic import BaseModel
from sqlalchemy import text
from app.database import engine
app = FastAPI(
title="Pokemon Collection API"
)
app.mount(
"/static",
StaticFiles(directory="app/static"),
name="static"
)
Ricarica i file nel container:
cd /opt/pokemon
docker-compose down
docker-compose build --no-cache
docker-compose up -d
Verifichiamo aprendo la pagina web http://vm ip:8000/index dovrebbe mostrare una bozza della pagina simile a questa
Un miglioramento potrebbe essere che le carte possedute non si vedono a colori ma in bianco e nero, per farlo servono tre modifiche
Modificare il file main.py in modo da sotiture il blocco @app.get("/set/{set_id}/cards") con questo:
@app.get("/set/{set_id}/cards")
def get_set_cards(set_id: str):
with engine.connect() as conn:
result = conn.execute(
text("""
SELECT
c.id,
c.name,
c.card_number,
c.rarity,
c.image_small,
EXISTS (
SELECT 1
FROM collection col
WHERE col.card_id = c.id
AND col.user_id = 1
) AS owned
FROM cards c
WHERE c.set_id = :set_id
ORDER BY
CASE
WHEN c.card_number ~ '^[0-9]+$'
THEN c.card_number::integer
END NULLS LAST,
c.card_number
"""),
{"set_id": set_id}
)
return result.mappings().all()
Modificare il file index.html in modo da sostiture la riga:
div.className = 'card';
con questa:
div.className = card.owned
? 'card'
: 'card not-owned';
Modificare il file css e aggiungere:
.not-owned img {
filter: grayscale(100%);
opacity: 0.30;
}
Ricompilare:
docker-compose down
docker-compose build --no-cache
docker-compose up -d
Adesso la pagina dovrebbe essere così perchè non abbiamo ancora inserito alcuna carta
cd /opt/pokemon
nano docker-compose.yml
Aperto il file inserisci:
services:
postgres:
image: postgres:17
container_name: pokemon-postgres
restart: unless-stopped
environment:
POSTGRES_DB: pokemon
POSTGRES_USER: pokemon
POSTGRES_PASSWORD: pokemon123
ports:
- "5432:5432"
volumes:
- ./postgres:/var/lib/postgresql/data
Per salvare CTRL+X e poi Y e invio
Avvio del database
docker-compose up -d
docker ps
Dovresti vedere qualcosa simile a:
pokemon-postgres
postgres:17
Up
Per verificare PostGres sempre da Root esegui:
docker exec -it pokemon-postgres psql -U pokemon -d pokemon
Se tutto è corretto vedrai:
pokemon=#
digita:
SELECT version();
Vedrai una struttura vuota che però indica che il DB è funzionante e pronto ad essere riempito.
per uscire digita il comando:
q
e torni al prompt PostgreSQL:
pokemon=#
Poi esci da PostgreSQL con il comando:
\q
Abbiamo completato tutta la parte infrastrutturale. Prossimo passo: progettare il database.