- Mount static files on /static endpoint - Configure Jinja2Templates with directory structure - Create base template with Pico.css, HTMX, Chart.js - Create all template subdirectories (auth, dashboard, keys, tokens, profile, components) - Create initial CSS and JS files - Add tests for static files and templates configuration Tests: 12 passing Coverage: 100% on new configuration code
14 KiB
Prompt di Ingaggio: Frontend Web (T44-T54)
🎯 MISSIONE
Implementare il Frontend Web per OpenRouter API Key Monitor usando HTML, Jinja2 templates e HTMX per un'interfaccia utente moderna e reattiva.
Task da completare: T44, T45, T46, T47, T48, T49, T50, T51, T52, T53, T54
📋 CONTESTO
AGENTE: @tdd-developer
Repository: /home/google/Sources/LucaSacchiNet/openrouter-watcher
Stato Attuale:
- ✅ MVP Backend completato: 51/74 task (69%)
- ✅ 444+ test passanti, ~98% coverage
- ✅ Tutte le API REST implementate
- ✅ Background Tasks per sincronizzazione automatica
- ✅ Docker support pronto
- 🎯 Manca: Interfaccia web per gli utenti
Perché questa fase è importante: Attualmente l'applicazione espone solo API REST. Gli utenti devono usare strumenti come curl o Postman per interagire. Con il frontend web, gli utenti potranno:
- Registrarsi e fare login via browser
- Visualizzare dashboard con grafici
- Gestire API keys tramite interfaccia grafica
- Generare e revocare token API
- Vedere statistiche in tempo reale
Stack Frontend:
- FastAPI - Serve static files e templates
- Jinja2 - Template engine
- HTMX - AJAX moderno senza JavaScript complesso
- Pico.css - CSS framework minimalista (o Bootstrap/Tailwind)
- Chart.js - Grafici per dashboard
Backend Pronto:
- Tutti i router REST funzionanti
- Autenticazione JWT via cookie
- API documentate su
/docs
🔧 TASK DA IMPLEMENTARE
T44: Configurare FastAPI per Static Files e Templates
File: src/openrouter_monitor/main.py
Requisiti:
- Mount directory
/staticper CSS, JS, immagini - Configurare Jinja2 templates
- Creare struttura directory
templates/estatic/ - Aggiungere context processor per variabili globali
Implementazione:
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from pathlib import Path
# Mount static files
app.mount("/static", StaticFiles(directory="static"), name="static")
# Configure templates
templates = Jinja2Templates(directory="templates")
# Context processor
def get_context(request: Request, **kwargs):
return {
"request": request,
"app_name": "OpenRouter Monitor",
"user": getattr(request.state, 'user', None),
**kwargs
}
File da creare:
static/
├── css/
│ └── style.css
├── js/
│ └── main.js
└── img/
└── favicon.ico
templates/
├── base.html
├── components/
│ ├── navbar.html
│ ├── footer.html
│ └── alert.html
├── auth/
│ ├── login.html
│ └── register.html
├── dashboard/
│ └── index.html
├── keys/
│ └── index.html
├── tokens/
│ └── index.html
└── profile/
└── index.html
Test: Verifica che /static/css/style.css sia accessibile
T45: Creare Base Template HTML
File: templates/base.html, templates/components/navbar.html, templates/components/footer.html
Requisiti:
- Layout base responsive
- Include Pico.css (o altro framework) da CDN
- Meta tags SEO-friendly
- Favicon
- Navbar con menu dinamico (login/logout)
- Footer con info app
- Block content per pagine figlie
Implementazione base.html:
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Monitora l'utilizzo delle tue API key OpenRouter">
<title>{% block title %}{{ app_name }}{% endblock %}</title>
<!-- Pico.css -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css">
<!-- HTMX -->
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<!-- Custom CSS -->
<link rel="stylesheet" href="/static/css/style.css">
{% block extra_css %}{% endblock %}
</head>
<body>
{% include 'components/navbar.html' %}
<main class="container">
{% include 'components/alert.html' %}
{% block content %}{% endblock %}
</main>
{% include 'components/footer.html' %}
{% block extra_js %}{% endblock %}
</body>
</html>
Test: Verifica rendering base template
T46: Configurare HTMX e CSRF
File: templates/base.html (aggiorna), src/openrouter_monitor/middleware/csrf.py
Requisiti:
- Aggiungere CSRF token in meta tag
- Middleware CSRF per protezione form
- HTMX configurato per inviare CSRF header
Implementazione:
# middleware/csrf.py
from fastapi import Request, HTTPException
from starlette.middleware.base import BaseHTTPMiddleware
import secrets
class CSRFMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
# Generate or get CSRF token
if 'csrf_token' not in request.session:
request.session['csrf_token'] = secrets.token_urlsafe(32)
# Validate on POST/PUT/DELETE
if request.method in ['POST', 'PUT', 'DELETE']:
token = request.headers.get('X-CSRF-Token') or request.form().get('_csrf_token')
if token != request.session.get('csrf_token'):
raise HTTPException(status_code=403, detail="Invalid CSRF token")
response = await call_next(request)
return response
Template aggiornamento:
<meta name="csrf-token" content="{{ csrf_token }}">
<script>
// HTMX default headers
document.body.addEventListener('htmx:configRequest', function(evt) {
evt.detail.headers['X-CSRF-Token'] = document.querySelector('meta[name="csrf-token"]').content;
});
</script>
T47: Pagina Login (/login)
File: templates/auth/login.html, src/openrouter_monitor/routers/web_auth.py
Requisiti:
- Form email/password
- Validazione client-side (HTML5)
- HTMX per submit AJAX
- Messaggi errore (flash messages)
- Redirect a dashboard dopo login
- Link a registrazione
Implementazione:
# routers/web_auth.py
from fastapi import APIRouter, Request, Form, HTTPException
from fastapi.responses import HTMLResponse, RedirectResponse
router = APIRouter()
@router.get("/login", response_class=HTMLResponse)
async def login_page(request: Request):
return templates.TemplateResponse(
"auth/login.html",
get_context(request)
)
@router.post("/login")
async def login_submit(
request: Request,
email: str = Form(...),
password: str = Form(...)
):
# Call auth service
try:
token = await authenticate_user(email, password)
response = RedirectResponse(url="/dashboard", status_code=302)
response.set_cookie(key="access_token", value=token, httponly=True)
return response
except AuthenticationError:
return templates.TemplateResponse(
"auth/login.html",
get_context(request, error="Invalid credentials")
)
Template:
{% extends "base.html" %}
{% block title %}Login - {{ app_name }}{% endblock %}
{% block content %}
<article class="grid">
<div>
<h1>Login</h1>
{% if error %}
<div class="alert alert-danger">{{ error }}</div>
{% endif %}
<form method="post" action="/login" hx-post="/login" hx-target="body">
<input type="hidden" name="_csrf_token" value="{{ csrf_token }}">
<label for="email">Email</label>
<input type="email" id="email" name="email" required
placeholder="your@email.com" autocomplete="email">
<label for="password">Password</label>
<input type="password" id="password" name="password" required
placeholder="••••••••" autocomplete="current-password">
<button type="submit">Login</button>
</form>
<p>Don't have an account? <a href="/register">Register</a></p>
</div>
</article>
{% endblock %}
Test: Test login form, validazione, redirect
T48: Pagina Registrazione (/register)
File: templates/auth/register.html
Requisiti:
- Form completo: email, password, password_confirm
- Validazione password strength (client-side)
- Check password match
- Conferma registrazione
- Redirect a login
Template:
{% extends "base.html" %}
{% block content %}
<h1>Register</h1>
<form method="post" action="/register" hx-post="/register" hx-target="body">
<label for="email">Email</label>
<input type="email" id="email" name="email" required>
<label for="password">Password</label>
<input type="password" id="password" name="password" required
minlength="12" pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*])">
<small>Min 12 chars, uppercase, lowercase, number, special char</small>
<label for="password_confirm">Confirm Password</label>
<input type="password" id="password_confirm" name="password_confirm" required>
<button type="submit">Register</button>
</form>
{% endblock %}
T49: Pagina Logout
File: Gestito via endpoint POST con redirect
Requisiti:
- Bottone logout in navbar
- Conferma opzionale
- Redirect a login
- Cancella cookie JWT
T50: Dashboard (/dashboard)
File: templates/dashboard/index.html
Requisiti:
- Card riepilogative (totale richieste, costo, token)
- Grafico andamento temporale (Chart.js)
- Tabella modelli più usati
- Link rapidi a gestione keys e tokens
- Dati caricati via API interna
Implementazione:
{% extends "base.html" %}
{% block content %}
<h1>Dashboard</h1>
<div class="grid">
<article>
<h3>Total Requests</h3>
<p><strong>{{ stats.total_requests }}</strong></p>
</article>
<article>
<h3>Total Cost</h3>
<p><strong>${{ stats.total_cost }}</strong></p>
</article>
<article>
<h3>API Keys</h3>
<p><strong>{{ api_keys_count }}</strong></p>
</article>
</div>
<article>
<h3>Usage Over Time</h3>
<canvas id="usageChart"></canvas>
</article>
<script>
const ctx = document.getElementById('usageChart').getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
labels: {{ chart_labels | tojson }},
datasets: [{
label: 'Requests',
data: {{ chart_data | tojson }}
}]
}
});
</script>
{% endblock %}
T51-T54: Altre Pagine
Seguire lo stesso pattern per:
- T51: Gestione API Keys (
/keys) - Tabella con CRUD via HTMX - T52: Statistiche (
/stats) - Filtri e paginazione - T53: Token API (
/tokens) - Generazione e revoca - T54: Profilo (
/profile) - Cambio password
🔄 WORKFLOW TDD
Per OGNI task:
- RED: Scrivi test che verifica rendering template
- GREEN: Implementa template e route
- REFACTOR: Estrai componenti riutilizzabili
📁 STRUTTURA FILE DA CREARE
templates/
├── base.html
├── components/
│ ├── navbar.html
│ ├── footer.html
│ └── alert.html
├── auth/
│ ├── login.html
│ └── register.html
├── dashboard/
│ └── index.html
├── keys/
│ └── index.html
├── tokens/
│ └── index.html
└── profile/
└── index.html
static/
├── css/
│ └── style.css
└── js/
└── main.js
src/openrouter_monitor/
├── routers/
│ ├── web.py # T44, T47-T54
│ └── web_auth.py # T47-T49
└── middleware/
└── csrf.py # T46
✅ CRITERI DI ACCETTAZIONE
- T44: Static files e templates configurati
- T45: Base template con layout responsive
- T46: CSRF protection e HTMX configurati
- T47: Pagina login funzionante
- T48: Pagina registrazione funzionante
- T49: Logout funzionante
- T50: Dashboard con grafici
- T51: Gestione API keys via web
- T52: Statistiche con filtri
- T53: Gestione token via web
- T54: Profilo utente
- Tutte le pagine responsive (mobile-friendly)
- Test completi per router web
- 11 commit atomici con conventional commits
📝 COMMIT MESSAGES
feat(frontend): T44 setup FastAPI static files and templates
feat(frontend): T45 create base HTML template with layout
feat(frontend): T46 configure HTMX and CSRF protection
feat(frontend): T47 implement login page
feat(frontend): T48 implement registration page
feat(frontend): T49 implement logout functionality
feat(frontend): T50 implement dashboard with charts
feat(frontend): T51 implement API keys management page
feat(frontend): T52 implement detailed stats page
feat(frontend): T53 implement API tokens management page
feat(frontend): T54 implement user profile page
🚀 VERIFICA FINALE
cd /home/google/Sources/LucaSacchiNet/openrouter-watcher
# Avvia app
uvicorn src.openrouter_monitor.main:app --reload
# Test manuali:
# 1. Visita http://localhost:8000/login
# 2. Registra nuovo utente
# 3. Login
# 4. Visualizza dashboard con grafici
# 5. Aggiungi API key
# 6. Genera token API
# 7. Logout
# Test automatici
pytest tests/unit/routers/test_web.py -v
🎨 DESIGN CONSIGLIATO
- Framework CSS: Pico.css (leggero, moderno, semantic HTML)
- Colori: Blu primario, grigio chiaro sfondo
- Layout: Container centrato, max-width 1200px
- Mobile: Responsive con breakpoint 768px
- Grafici: Chart.js con tema coordinato
AGENTE: @tdd-developer
INIZIA CON: T44 - Setup FastAPI static files e templates
QUANDO FINITO: L'applicazione avrà un'interfaccia web completa e user-friendly! 🎨