fix(01): revise plans based on checker feedback

- Fix CSV-01 column count: 32 -> 33 (8 meta + 24 slide + 1 caption)
- Add TopicResult Pydantic model + topic_generator.txt prompt
- Make bulk generation async with background task + polling endpoint
- Add POST /api/export/{job_id}/csv for inline edit CSV download
- Split Plan 01-04 Task 2 into 2a/2b/2c (badges, slideviewer, pages)
- Update ProgressIndicator to use polling on /status endpoint
- Add --yes flag and frontend/ prerequisite note to Plan 01-01

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michele
2026-03-08 01:40:30 +01:00
parent 3f1dbbf396
commit 696b265e4d
5 changed files with 194 additions and 82 deletions

View File

@@ -39,7 +39,7 @@
### CSV & Export ### CSV & Export
- [ ] **CSV-01**: CSV con header completo compatibile Canva Bulk Create (32 colonne: 8 metadati + 8 slide x 3 campi) - [ ] **CSV-01**: CSV con header completo compatibile Canva Bulk Create (33 colonne: 8 metadati + 24 slide (8 slide x 3 campi) + 1 caption_instagram)
- [ ] **CSV-02**: Encoding utf-8-sig (BOM) per compatibilita' Excel/Windows - [ ] **CSV-02**: Encoding utf-8-sig (BOM) per compatibilita' Excel/Windows
- [ ] **CSV-03**: Campi metadato (campagna, fase, tipo, formato, funzione, livello, nicchia, data) inclusi per analisi - [ ] **CSV-03**: Campi metadato (campagna, fase, tipo, formato, funzione, livello, nicchia, data) inclusi per analisi
- [ ] **CSV-04**: Download CSV dalla Web UI - [ ] **CSV-04**: Download CSV dalla Web UI

View File

@@ -179,7 +179,9 @@ Output: Container Docker buildabile che serve una pagina React vuota su / e risp
</files> </files>
<action> <action>
1. Creare il progetto React + TypeScript con Vite: 1. Creare il progetto React + TypeScript con Vite:
- cd al progetto, eseguire: npm create vite@latest frontend -- --template react-ts - PREREQUISITO: la directory frontend/ NON deve esistere. Se esiste, rimuoverla prima: rm -rf frontend
- cd al progetto, eseguire: npm create vite@latest frontend -- --template react-ts --yes
- Il flag --yes evita prompt interattivi che bloccherebbero l'esecuzione autonoma
- Questo genera la struttura base - Questo genera la struttura base
2. Installare dipendenze frontend: 2. Installare dipendenze frontend:

View File

@@ -14,6 +14,7 @@ files_modified:
- backend/schemas/generate.py - backend/schemas/generate.py
- backend/data/format_mapping.json - backend/data/format_mapping.json
- backend/data/prompts/system_prompt.txt - backend/data/prompts/system_prompt.txt
- backend/data/prompts/topic_generator.txt
- backend/data/prompts/pas_valore.txt - backend/data/prompts/pas_valore.txt
- backend/data/prompts/listicle_valore.txt - backend/data/prompts/listicle_valore.txt
- backend/data/prompts/bab_storytelling.txt - backend/data/prompts/bab_storytelling.txt
@@ -47,7 +48,7 @@ must_haves:
provides: "CalendarSlot, CalendarRequest, CalendarResponse Pydantic models" provides: "CalendarSlot, CalendarRequest, CalendarResponse Pydantic models"
contains: "class CalendarSlot" contains: "class CalendarSlot"
- path: "backend/schemas/generate.py" - path: "backend/schemas/generate.py"
provides: "SlideContent, GeneratedPost Pydantic models per output LLM e CSV" provides: "SlideContent, GeneratedPost, TopicResult Pydantic models per output LLM e CSV"
contains: "class GeneratedPost" contains: "class GeneratedPost"
- path: "backend/data/format_mapping.json" - path: "backend/data/format_mapping.json"
provides: "Tabella mapping tipo_contenuto x livello_schwartz -> formato narrativo" provides: "Tabella mapping tipo_contenuto x livello_schwartz -> formato narrativo"
@@ -158,6 +159,7 @@ Output: Servizi Python testabili indipendentemente, 5 prompt .txt in italiano, s
4. Creare backend/schemas/generate.py con Pydantic models: 4. Creare backend/schemas/generate.py con Pydantic models:
- SlideContent: headline (str), body (str), image_keyword (str) - SlideContent: headline (str), body (str), image_keyword (str)
- GeneratedPost: cover_title (str), cover_subtitle (str), cover_image_keyword (str), slides (list[SlideContent] — 6 slide centrali s2-s7), cta_text (str), cta_subtext (str), cta_image_keyword (str), caption_instagram (str) - GeneratedPost: cover_title (str), cover_subtitle (str), cover_image_keyword (str), slides (list[SlideContent] — 6 slide centrali s2-s7), cta_text (str), cta_subtext (str), cta_image_keyword (str), caption_instagram (str)
- TopicResult: topic (str) — Pydantic model per validare output LLM della generazione topic. Usato da LLMService.generate_topic() con lo stesso loop retry/validation delle altre generazioni.
- GenerateRequest: slot (CalendarSlot), obiettivo_campagna (str), brand_name (Optional[str]), tono (Optional[str]) - GenerateRequest: slot (CalendarSlot), obiettivo_campagna (str), brand_name (Optional[str]), tono (Optional[str])
- PostResult: slot_index (int), status (Literal["success", "failed", "pending"]), post (Optional[GeneratedPost]), error (Optional[str]) - PostResult: slot_index (int), status (Literal["success", "failed", "pending"]), post (Optional[GeneratedPost]), error (Optional[str])
- GenerateResponse: campagna (str), results (list[PostResult]), total (int), success_count (int), failed_count (int) - GenerateResponse: campagna (str), results (list[PostResult]), total (int), success_count (int), failed_count (int)
@@ -178,7 +180,7 @@ Output: Servizi Python testabili indipendentemente, 5 prompt .txt in italiano, s
<verify> <verify>
- backend/constants.py: CANVA_FIELDS ha esattamente 33 elementi, PERSUASION_DISTRIBUTION somma a 13, SCHWARTZ_DISTRIBUTION somma a 13 - backend/constants.py: CANVA_FIELDS ha esattamente 33 elementi, PERSUASION_DISTRIBUTION somma a 13, SCHWARTZ_DISTRIBUTION somma a 13
- backend/schemas/calendar.py: CalendarSlot importabile, CalendarRequest ha campo obiettivo_campagna - backend/schemas/calendar.py: CalendarSlot importabile, CalendarRequest ha campo obiettivo_campagna
- backend/schemas/generate.py: GeneratedPost ha slides come list[SlideContent], PostResult ha campo status - backend/schemas/generate.py: GeneratedPost ha slides come list[SlideContent], PostResult ha campo status, TopicResult ha campo topic (str)
- backend/data/format_mapping.json: contiene tutte le 6 chiavi tipo_contenuto, ciascuna con 5 livelli - backend/data/format_mapping.json: contiene tutte le 6 chiavi tipo_contenuto, ciascuna con 5 livelli
- backend/services/format_selector.py: FormatSelector ha metodo select_format - backend/services/format_selector.py: FormatSelector ha metodo select_format
</verify> </verify>
@@ -193,6 +195,7 @@ Output: Servizi Python testabili indipendentemente, 5 prompt .txt in italiano, s
backend/services/calendar_service.py backend/services/calendar_service.py
backend/services/prompt_service.py backend/services/prompt_service.py
backend/data/prompts/system_prompt.txt backend/data/prompts/system_prompt.txt
backend/data/prompts/topic_generator.txt
backend/data/prompts/pas_valore.txt backend/data/prompts/pas_valore.txt
backend/data/prompts/listicle_valore.txt backend/data/prompts/listicle_valore.txt
backend/data/prompts/bab_storytelling.txt backend/data/prompts/bab_storytelling.txt
@@ -240,6 +243,13 @@ Output: Servizi Python testabili indipendentemente, 5 prompt .txt in italiano, s
- Lingua: italiano naturale, NON tradotto dall'inglese - Lingua: italiano naturale, NON tradotto dall'inglese
- Output: JSON strutturato con i campi specificati nello schema - Output: JSON strutturato con i campi specificati nello schema
backend/data/prompts/topic_generator.txt (prompt per generazione topic):
- Variabili: {{obiettivo_campagna}}, {{tipo_contenuto}}, {{livello_schwartz}}, {{target_nicchia}}, {{fase_campagna}}
- Istruzioni: genera UN topic specifico e concreto per un post Instagram carosello
- Il topic deve essere rilevante per l'obiettivo campagna, il tipo di contenuto e la nicchia
- Output: JSON con campo "topic" (stringa, max 100 caratteri)
- Scritto IN italiano come tutti gli altri prompt
backend/data/prompts/pas_valore.txt (formato PAS per post valore): backend/data/prompts/pas_valore.txt (formato PAS per post valore):
- Sezioni: SYSTEM (ref system_prompt), USER, OUTPUT_SCHEMA - Sezioni: SYSTEM (ref system_prompt), USER, OUTPUT_SCHEMA
- Variabili: {{obiettivo_campagna}}, {{target_nicchia}}, {{livello_schwartz}}, {{topic}}, {{brand_name}} - Variabili: {{obiettivo_campagna}}, {{target_nicchia}}, {{livello_schwartz}}, {{topic}}, {{brand_name}}
@@ -277,13 +287,13 @@ Output: Servizi Python testabili indipendentemente, 5 prompt .txt in italiano, s
- CalendarService.generate_calendar() con CalendarRequest(obiettivo_campagna="test", settimane=2) produce CalendarResponse con esattamente 13 slot - CalendarService.generate_calendar() con CalendarRequest(obiettivo_campagna="test", settimane=2) produce CalendarResponse con esattamente 13 slot
- Distribuzione PN: contare tipi -> 4 valore, 2 storytelling, 2 news, 3 riprova, 1 coinvolgimento, 1 promo - Distribuzione PN: contare tipi -> 4 valore, 2 storytelling, 2 news, 3 riprova, 1 coinvolgimento, 1 promo
- Distribuzione Schwartz: contare livelli -> L5=3, L4=3, L3=4, L2=2, L1=1 - Distribuzione Schwartz: contare livelli -> L5=3, L4=3, L3=4, L2=2, L1=1
- PromptService.list_prompts() ritorna almeno 6 file (system + 5 base) - PromptService.list_prompts() ritorna almeno 7 file (system + topic_generator + 5 base)
- PromptService.compile_prompt("pas_valore", {"obiettivo_campagna": "test", ...}) sostituisce tutte le variabili senza errori - PromptService.compile_prompt("pas_valore", {"obiettivo_campagna": "test", ...}) sostituisce tutte le variabili senza errori
- Tutti i prompt .txt contengono SOLO testo italiano, nessuna istruzione in inglese - Tutti i prompt .txt contengono SOLO testo italiano, nessuna istruzione in inglese
- Nessun prompt contiene numeri hardcoded per slide count — usano {{num_slides}} o la struttura e' definita nell'output schema - Nessun prompt contiene numeri hardcoded per slide count — usano {{num_slides}} o la struttura e' definita nell'output schema
</verify> </verify>
<done> <done>
CalendarService genera 13 slot con distribuzione PN e Schwartz corretta, assegna fasi campagna, calcola date, ruota nicchie. PromptService carica e compila prompt con variabili {{...}}. 5 prompt base + system prompt scritti IN italiano, con output JSON schema esplicito. Nessun valore hardcoded nei template. CalendarService genera 13 slot con distribuzione PN e Schwartz corretta, assegna fasi campagna, calcola date, ruota nicchie. PromptService carica e compila prompt con variabili {{...}}. 5 prompt base + system prompt + topic_generator prompt scritti IN italiano, con output JSON schema esplicito. Nessun valore hardcoded nei template.
</done> </done>
</task> </task>

View File

@@ -23,8 +23,10 @@ must_haves:
- "CSVBuilder produce CSV con encoding utf-8-sig, header CANVA_FIELDS, e caratteri italiani intatti" - "CSVBuilder produce CSV con encoding utf-8-sig, header CANVA_FIELDS, e caratteri italiani intatti"
- "GenerationPipeline genera 13 post con per-item error isolation: un fallimento non blocca il batch" - "GenerationPipeline genera 13 post con per-item error isolation: un fallimento non blocca il batch"
- "API endpoint POST /api/calendar/generate ritorna CalendarResponse con 13 slot" - "API endpoint POST /api/calendar/generate ritorna CalendarResponse con 13 slot"
- "API endpoint POST /api/generate/bulk ritorna GenerateResponse con risultati per-item (success/failed)" - "API endpoint POST /api/generate/bulk avvia generazione come background task e ritorna job_id immediatamente"
- "API endpoint GET /api/export/{job_id}/csv scarica file CSV con Content-Disposition attachment" - "API endpoint GET /api/generate/job/{job_id}/status ritorna progresso in tempo reale (completed/total/current_post) per polling"
- "API endpoint GET /api/export/{job_id}/csv scarica file CSV originale con Content-Disposition attachment"
- "API endpoint POST /api/export/{job_id}/csv accetta dati modificati dall'utente e rigenera CSV con le modifiche inline"
- "API endpoint GET /api/settings ritorna configurazione corrente, PUT /api/settings salva" - "API endpoint GET /api/settings ritorna configurazione corrente, PUT /api/settings salva"
artifacts: artifacts:
- path: "backend/services/llm_service.py" - path: "backend/services/llm_service.py"
@@ -37,13 +39,13 @@ must_haves:
provides: "GenerationPipeline che orchestra calendario -> LLM -> CSV con per-item isolation" provides: "GenerationPipeline che orchestra calendario -> LLM -> CSV con per-item isolation"
contains: "class GenerationPipeline" contains: "class GenerationPipeline"
- path: "backend/routers/generate.py" - path: "backend/routers/generate.py"
provides: "POST /api/generate/bulk e POST /api/generate/single endpoints" provides: "POST /api/generate/bulk (async background task), POST /api/generate/single, GET /api/generate/job/{job_id}/status (polling)"
contains: "router = APIRouter" contains: "router = APIRouter"
- path: "backend/routers/calendar.py" - path: "backend/routers/calendar.py"
provides: "POST /api/calendar/generate endpoint" provides: "POST /api/calendar/generate endpoint"
contains: "router = APIRouter" contains: "router = APIRouter"
- path: "backend/routers/export.py" - path: "backend/routers/export.py"
provides: "GET /api/export/{job_id}/csv endpoint con FileResponse" provides: "GET /api/export/{job_id}/csv (originale), POST /api/export/{job_id}/csv (con modifiche inline)"
contains: "router = APIRouter" contains: "router = APIRouter"
- path: "backend/routers/settings.py" - path: "backend/routers/settings.py"
provides: "GET/PUT /api/settings endpoint per API key e configurazione" provides: "GET/PUT /api/settings endpoint per API key e configurazione"
@@ -53,6 +55,10 @@ must_haves:
to: "Claude API" to: "Claude API"
via: "anthropic.Anthropic client con retry loop" via: "anthropic.Anthropic client con retry loop"
pattern: "client\\.messages\\.create" pattern: "client\\.messages\\.create"
- from: "backend/services/llm_service.py"
to: "backend/schemas/generate.py"
via: "generate_topic() valida output con TopicResult(BaseModel)"
pattern: "TopicResult"
- from: "backend/services/csv_builder.py" - from: "backend/services/csv_builder.py"
to: "backend/constants.py" to: "backend/constants.py"
via: "Importa CANVA_FIELDS per header CSV" via: "Importa CANVA_FIELDS per header CSV"
@@ -120,8 +126,13 @@ Output: API backend completa che accetta una richiesta di generazione calendario
- Pydantic ValidationError: riprova UNA volta con istruzione correttiva appesa al prompt ("Il tuo output precedente non era JSON valido. Rispondi SOLO con JSON valido secondo lo schema.") - Pydantic ValidationError: riprova UNA volta con istruzione correttiva appesa al prompt ("Il tuo output precedente non era JSON valido. Rispondi SOLO con JSON valido secondo lo schema.")
- Qualsiasi altra eccezione: non ritentare, solleva - Qualsiasi altra eccezione: non ritentare, solleva
f. Dopo ogni chiamata riuscita, applica inter_request_delay (time.sleep) per rispettare OTPM Tier 1 f. Dopo ogni chiamata riuscita, applica inter_request_delay (time.sleep) per rispettare OTPM Tier 1
- Metodo generate_topic(system_prompt: str, obiettivo: str, tipo_contenuto: str, nicchia: str) -> str: - Metodo generate_topic(system_prompt: str, obiettivo: str, tipo_contenuto: str, nicchia: str, fase_campagna: str) -> str:
Genera un topic specifico per lo slot dato l'obiettivo campagna. Ritorna una stringa topic. Genera un topic specifico per lo slot dato l'obiettivo campagna.
Usa lo stesso pattern di validazione delle altre generazioni:
a. Carica prompt da topic_generator.txt via PromptService
b. Chiama generate() con response_schema=TopicResult (Pydantic model definito in schemas/generate.py)
c. Ritorna result.topic (stringa estratta dal model validato)
Questo garantisce che anche la generazione topic passi per il loop retry/validation JSON, coerente con LLM-02.
- Log strutturato: ogni chiamata logga model, tokens in/out, tempo risposta, tentativo N/max - Log strutturato: ogni chiamata logga model, tokens in/out, tempo risposta, tentativo N/max
2. Creare backend/services/csv_builder.py: 2. Creare backend/services/csv_builder.py:
@@ -143,20 +154,32 @@ Output: API backend completa che accetta una richiesta di generazione calendario
3. Creare backend/services/generation_pipeline.py: 3. Creare backend/services/generation_pipeline.py:
- class GenerationPipeline(__init__ riceve llm_service: LLMService, prompt_service: PromptService, calendar_service: CalendarService, format_selector: FormatSelector, csv_builder: CSVBuilder) - class GenerationPipeline(__init__ riceve llm_service: LLMService, prompt_service: PromptService, calendar_service: CalendarService, format_selector: FormatSelector, csv_builder: CSVBuilder)
- Metodo generate_bulk(request: CalendarRequest, api_key: str) -> GenerateResponse: - Dict in-memory _jobs: dict[str, JobStatus] per tracciare progresso dei job in corso
a. Genera calendario via calendar_service.generate_calendar(request) - Dataclass JobStatus: job_id (str), status (Literal["running", "completed", "failed"]), total (int), completed (int), current_post (int), results (list[PostResult]), calendar (Optional[CalendarResponse]), error (Optional[str])
b. Per ogni slot del calendario: - Metodo generate_bulk_async(request: CalendarRequest, api_key: str) -> str:
- Genera topic via llm_service.generate_topic() se slot.topic e' None a. Genera job_id (UUID)
b. Genera calendario via calendar_service.generate_calendar(request)
c. Inizializza _jobs[job_id] con status="running", total=len(slots), completed=0
d. Lancia _run_generation(job_id, calendario, request) come asyncio.create_task (background)
e. Ritorna job_id immediatamente
- Metodo _run_generation(job_id, calendar, request) — async background:
a. Per ogni slot del calendario:
- Aggiorna _jobs[job_id].current_post = indice corrente
- Genera topic via llm_service.generate_topic(system_prompt, obiettivo, slot.tipo_contenuto, slot.target_nicchia, slot.fase_campagna) se slot.topic e' None
- Seleziona il prompt template corretto in base a formato_narrativo (es. "pas_valore" per PAS + valore) - Seleziona il prompt template corretto in base a formato_narrativo (es. "pas_valore" per PAS + valore)
- Compila il prompt con variabili (obiettivo, nicchia, livello, topic, brand) - Compila il prompt con variabili (obiettivo, nicchia, livello, topic, brand)
- Chiama llm_service.generate(system_prompt, user_prompt, GeneratedPost) - Chiama llm_service.generate(system_prompt, user_prompt, GeneratedPost)
- Se successo: PostResult(status="success", post=risultato) - Se successo: PostResult(status="success", post=risultato)
- Se fallimento: PostResult(status="failed", error=str(e)) - Se fallimento: PostResult(status="failed", error=str(e))
- CRITICO Pitfall 5: ogni slot in try/except INDIVIDUALE. Un fallimento NON blocca il loop. - CRITICO Pitfall 5: ogni slot in try/except INDIVIDUALE. Un fallimento NON blocca il loop.
c. Genera job_id (UUID) - Aggiorna _jobs[job_id].completed += 1 e appendi risultato
d. Chiama csv_builder.build_csv() con i risultati b. Chiama csv_builder.build_csv() con i risultati
e. Salva job metadata in OUTPUTS_PATH / f"{job_id}.json" (per ricaricamento) c. Salva job metadata in OUTPUTS_PATH / f"{job_id}.json" (per ricaricamento e persistenza)
f. Ritorna GenerateResponse con risultati per-item d. Aggiorna _jobs[job_id].status = "completed"
- Metodo get_job_status(job_id: str) -> JobStatus:
Ritorna lo stato corrente del job (per polling). Se non in memory, carica da disco ({job_id}.json).
- Metodo get_job_results(job_id: str) -> GenerateResponse:
Ritorna risultati completi. Carica da _jobs o da disco.
- Metodo generate_single(slot: CalendarSlot, obiettivo: str, api_key: str) -> PostResult: - Metodo generate_single(slot: CalendarSlot, obiettivo: str, api_key: str) -> PostResult:
Genera un singolo post. Utile per rigenerazione di post falliti. Genera un singolo post. Utile per rigenerazione di post falliti.
- Metodo _select_prompt_template(formato: str, tipo: str) -> str: - Metodo _select_prompt_template(formato: str, tipo: str) -> str:
@@ -166,12 +189,17 @@ Output: API backend completa che accetta una richiesta di generazione calendario
<verify> <verify>
- LLMService ha gestione specifica per RateLimitError con lettura retry-after - LLMService ha gestione specifica per RateLimitError con lettura retry-after
- LLMService ha inter_request_delay dopo ogni chiamata riuscita - LLMService ha inter_request_delay dopo ogni chiamata riuscita
- LLMService.generate_topic() chiama generate() con TopicResult come response_schema
- CSVBuilder importa CANVA_FIELDS e usa encoding='utf-8-sig' - CSVBuilder importa CANVA_FIELDS e usa encoding='utf-8-sig'
- GenerationPipeline.generate_bulk_async() ritorna str (job_id), non GenerateResponse
- GenerationPipeline ha _run_generation come async background task con asyncio.create_task
- GenerationPipeline ha try/except dentro il loop per-slot (non attorno al loop intero) - GenerationPipeline ha try/except dentro il loop per-slot (non attorno al loop intero)
- GenerationPipeline._jobs dict traccia progresso real-time per ogni job
- GenerationPipeline.get_job_status() ritorna JobStatus con completed/total/current_post
- GenerationPipeline salva job metadata JSON per ricaricamento - GenerationPipeline salva job metadata JSON per ricaricamento
</verify> </verify>
<done> <done>
LLMService chiama Claude con retry, backoff specifico per 429, e validation Pydantic. CSVBuilder produce CSV con encoding utf-8-sig e header CANVA_FIELDS locked. GenerationPipeline orchestra il flusso completo con per-item error isolation. LLMService chiama Claude con retry, backoff specifico per 429, e validation Pydantic (incluso generate_topic con TopicResult). CSVBuilder produce CSV con encoding utf-8-sig e header CANVA_FIELDS locked. GenerationPipeline orchestra il flusso completo come background task async con progresso real-time tracciato in _jobs dict e per-item error isolation.
</done> </done>
</task> </task>
@@ -197,18 +225,28 @@ Output: API backend completa che accetta una richiesta di generazione calendario
3. Creare backend/routers/generate.py: 3. Creare backend/routers/generate.py:
- router = APIRouter(prefix="/api/generate", tags=["generate"]) - router = APIRouter(prefix="/api/generate", tags=["generate"])
- POST /bulk: riceve CalendarRequest (+ eventuali topic overrides), usa GenerationPipeline.generate_bulk(), ritorna GenerateResponse - POST /bulk: riceve CalendarRequest (+ eventuali topic overrides), usa GenerationPipeline.generate_bulk_async()
- Prima verifica che API key sia configurata (da settings), ritorna 400 se mancante - Prima verifica che API key sia configurata (da settings), ritorna 400 se mancante
- Ritorna 200 anche con risultati parziali (alcuni failed) — il frontend gestisce lo stato per-item - Ritorna IMMEDIATAMENTE 202 Accepted con {"job_id": "uuid"} — la generazione continua in background
- Il frontend usa polling su /job/{job_id}/status per seguire il progresso
- GET /job/{job_id}/status: ritorna lo stato corrente del job per polling
- Risposta: {"job_id", "status": "running|completed|failed", "total": 13, "completed": 5, "current_post": 6, "results": [...completed results...]}
- Frontend chiama ogni 2 secondi finche' status != "running"
- GET /job/{job_id}: ritorna i risultati completi di un job (carica da GenerationPipeline.get_job_results())
- POST /single: riceve GenerateRequest (singolo slot), usa GenerationPipeline.generate_single(), ritorna PostResult - POST /single: riceve GenerateRequest (singolo slot), usa GenerationPipeline.generate_single(), ritorna PostResult
- GET /job/{job_id}: ritorna i risultati salvati di un job precedente (carica da OUTPUTS_PATH/{job_id}.json)
4. Creare backend/routers/export.py: 4. Creare backend/routers/export.py:
- router = APIRouter(prefix="/api/export", tags=["export"]) - router = APIRouter(prefix="/api/export", tags=["export"])
- GET /{job_id}/csv: trova file CSV in OUTPUTS_PATH/{job_id}.csv - GET /{job_id}/csv: trova file CSV originale in OUTPUTS_PATH/{job_id}.csv
- Ritorna FileResponse con media_type="text/csv; charset=utf-8" - Ritorna FileResponse con media_type="text/csv; charset=utf-8"
- Headers: Content-Disposition: attachment; filename="postgenerator_{job_id}.csv" - Headers: Content-Disposition: attachment; filename="postgenerator_{job_id}.csv"
- Ritorna 404 se file non esiste - Ritorna 404 se file non esiste
- POST /{job_id}/csv: accetta body JSON con i post modificati dall'utente (inline edits)
- Riceve: {"results": list[PostResult]} con i dati aggiornati dal frontend
- Rigenera il CSV usando CSVBuilder.build_csv() con i dati modificati
- Salva come OUTPUTS_PATH/{job_id}_edited.csv
- Ritorna il file CSV rigenerato con Content-Disposition attachment
- Questo risolve il problema delle modifiche inline perse al download: il frontend invia lo stato locale modificato e riceve un CSV aggiornato
5. Creare backend/routers/settings.py: 5. Creare backend/routers/settings.py:
- router = APIRouter(prefix="/api/settings", tags=["settings"]) - router = APIRouter(prefix="/api/settings", tags=["settings"])
@@ -229,13 +267,15 @@ Output: API backend completa che accetta una richiesta di generazione calendario
<verify> <verify>
- backend/main.py include tutti e 4 i router PRIMA del mount SPAStaticFiles - backend/main.py include tutti e 4 i router PRIMA del mount SPAStaticFiles
- POST /api/calendar/generate accetta CalendarRequest body - POST /api/calendar/generate accetta CalendarRequest body
- POST /api/generate/bulk verifica API key prima di procedere - POST /api/generate/bulk verifica API key, ritorna 202 con job_id (non attende completamento)
- GET /api/export/{job_id}/csv ha Content-Disposition header - GET /api/generate/job/{job_id}/status ritorna status, total, completed, current_post per polling
- GET /api/export/{job_id}/csv ha Content-Disposition header (file originale)
- POST /api/export/{job_id}/csv accetta results modificati e rigenera CSV
- GET /api/settings/status ritorna api_key_configured boolean - GET /api/settings/status ritorna api_key_configured boolean
- Nessun router contiene logica di business (solo validazione + chiamata service + return) - Nessun router contiene logica di business (solo validazione + chiamata service + return)
</verify> </verify>
<done> <done>
4 routers API (calendar, generate, export, settings) creati e montati in main.py. Ogni endpoint ha schema request/response Pydantic. Generate verifica API key. Export serve CSV con header corretti. Settings gestisce configurazione persistente. 4 routers API (calendar, generate, export, settings) creati e montati in main.py. Ogni endpoint ha schema request/response Pydantic. Generate e' async (202 + polling via /status). Export serve CSV originale (GET) e CSV con modifiche inline (POST). Settings gestisce configurazione persistente.
</done> </done>
</task> </task>
@@ -244,17 +284,23 @@ Output: API backend completa che accetta una richiesta di generazione calendario
<verification> <verification>
1. `python -c "from backend.services.llm_service import LLMService; print('OK')"` — importa senza errori 1. `python -c "from backend.services.llm_service import LLMService; print('OK')"` — importa senza errori
2. `python -c "from backend.services.csv_builder import CSVBuilder; print('OK')"` — importa senza errori 2. `python -c "from backend.services.csv_builder import CSVBuilder; print('OK')"` — importa senza errori
3. `python -c "from backend.main import app; print(app.routes)"` — mostra tutti i routes registrati 3. `python -c "from backend.main import app; print(app.routes)"` — mostra tutti i routes registrati incluso /api/generate/job/{job_id}/status
4. CSVBuilder usa encoding='utf-8-sig' nel codice (grep) 4. CSVBuilder usa encoding='utf-8-sig' nel codice (grep)
5. GenerationPipeline ha try/except PER SINGOLO slot, non attorno al loop 5. GenerationPipeline ha try/except PER SINGOLO slot, non attorno al loop
6. LLMService gestisce RateLimitError separatamente dalle altre eccezioni 6. GenerationPipeline.generate_bulk_async() ritorna job_id (str), non GenerateResponse
7. Nessun import circolare tra moduli 7. LLMService gestisce RateLimitError separatamente dalle altre eccezioni
8. LLMService.generate_topic() usa TopicResult come response_schema (non ritorna raw string)
9. POST /api/export/{job_id}/csv endpoint esiste e accetta body con results
10. Nessun import circolare tra moduli
</verification> </verification>
<success_criteria> <success_criteria>
- LLMService chiama Claude con retry specifico per 429 e validation Pydantic - LLMService chiama Claude con retry specifico per 429 e validation Pydantic (incluso generate_topic con TopicResult)
- CSVBuilder produce CSV con utf-8-sig encoding e CANVA_FIELDS header - CSVBuilder produce CSV con utf-8-sig encoding e CANVA_FIELDS header
- GenerationPipeline ha per-item error isolation - GenerationPipeline ha per-item error isolation e background task async
- POST /api/generate/bulk ritorna 202 con job_id, generazione continua in background
- GET /api/generate/job/{job_id}/status fornisce progresso real-time per polling
- POST /api/export/{job_id}/csv accetta dati modificati e rigenera CSV
- 4 API routers montati e funzionali - 4 API routers montati e funzionali
- Settings endpoint gestisce API key - Settings endpoint gestisce API key
- Job results salvati su disco per ricaricamento - Job results salvati su disco per ricaricamento

View File

@@ -26,10 +26,10 @@ autonomous: false
must_haves: must_haves:
truths: truths:
- "L'utente vede una Dashboard con link a Genera Calendario, Genera Singolo Post, e Impostazioni" - "L'utente vede una Dashboard con link a Genera Calendario, Genera Singolo Post, e Impostazioni"
- "L'utente compila il form Genera Calendario (obiettivo + settimane) e clicca Genera — vede progress indicator per ogni post" - "L'utente compila il form Genera Calendario (obiettivo + settimane) e clicca Genera — vede progress indicator che si aggiorna in tempo reale tramite polling ogni 2s su /api/generate/job/{job_id}/status"
- "L'utente vede i 13 post generati come griglia di card con badge colorati PN e Schwartz" - "L'utente vede i 13 post generati come griglia di card con badge colorati PN e Schwartz"
- "L'utente clicca su una card e vede le slide con navigazione frecce laterali + caption Instagram" - "L'utente clicca su una card e vede le slide con navigazione frecce laterali + caption Instagram"
- "L'utente puo' modificare il testo di una slide inline (click to edit) e le modifiche si riflettono nel CSV" - "L'utente puo' modificare il testo di una slide inline (click to edit) e le modifiche si riflettono nel CSV scaricato tramite POST /api/export/{job_id}/csv"
- "L'utente scarica il CSV cliccando un pulsante Download CSV" - "L'utente scarica il CSV cliccando un pulsante Download CSV"
- "Post falliti appaiono come card errore con pulsante Riprova" - "Post falliti appaiono come card errore con pulsante Riprova"
- "Il pulsante Genera e' disabilitato se API key non configurata, con messaggio che rimanda a Impostazioni" - "Il pulsante Genera e' disabilitato se API key non configurata, con messaggio che rimanda a Impostazioni"
@@ -63,8 +63,16 @@ must_haves:
pattern: "apiFetch" pattern: "apiFetch"
- from: "frontend/src/pages/GenerateCalendar.tsx" - from: "frontend/src/pages/GenerateCalendar.tsx"
to: "frontend/src/api/hooks.ts" to: "frontend/src/api/hooks.ts"
via: "useMutation per POST /api/generate/bulk" via: "useMutation per POST /api/generate/bulk (async, ritorna job_id)"
pattern: "useMutation" pattern: "useGenerateCalendar"
- from: "frontend/src/components/ProgressIndicator.tsx"
to: "frontend/src/api/hooks.ts"
via: "useJobStatus(jobId) polling ogni 2s su GET /api/generate/job/{job_id}/status"
pattern: "useJobStatus"
- from: "frontend/src/pages/OutputReview.tsx"
to: "frontend/src/api/hooks.ts"
via: "useDownloadEditedCsv per POST /api/export/{job_id}/csv con edits inline"
pattern: "useDownloadEditedCsv"
- from: "frontend/src/pages/OutputReview.tsx" - from: "frontend/src/pages/OutputReview.tsx"
to: "frontend/src/components/PostCard.tsx" to: "frontend/src/components/PostCard.tsx"
via: "Render griglia di PostCard" via: "Render griglia di PostCard"
@@ -116,6 +124,7 @@ Output: SPA React completa con tutte le pagine e componenti per il workflow: con
1. Creare frontend/src/types.ts con i tipi TypeScript che rispecchiano gli schemas Pydantic del backend: 1. Creare frontend/src/types.ts con i tipi TypeScript che rispecchiano gli schemas Pydantic del backend:
- CalendarSlot, CalendarRequest, CalendarResponse - CalendarSlot, CalendarRequest, CalendarResponse
- SlideContent, GeneratedPost, PostResult, GenerateResponse - SlideContent, GeneratedPost, PostResult, GenerateResponse
- JobStatus (job_id: string, status: "running" | "completed" | "failed", total: number, completed: number, current_post: number, results: PostResult[])
- Settings (api_key, llm_model, nicchie_attive, lingua, frequenza_post, brand_name, tono) - Settings (api_key, llm_model, nicchie_attive, lingua, frequenza_post, brand_name, tono)
- SettingsStatus (api_key_configured: boolean, llm_model: string) - SettingsStatus (api_key_configured: boolean, llm_model: string)
@@ -127,10 +136,12 @@ Output: SPA React completa con tutte le pagine e componenti per il workflow: con
- useSettings(): useQuery per GET /api/settings - useSettings(): useQuery per GET /api/settings
- useSettingsStatus(): useQuery per GET /api/settings/status - useSettingsStatus(): useQuery per GET /api/settings/status
- useUpdateSettings(): useMutation per PUT /api/settings - useUpdateSettings(): useMutation per PUT /api/settings
- useGenerateCalendar(): useMutation per POST /api/generate/bulk — ritorna GenerateResponse - useGenerateCalendar(): useMutation per POST /api/generate/bulk — ritorna {job_id} (async, NON GenerateResponse)
- useGenerateSingle(): useMutation per POST /api/generate/single - useGenerateSingle(): useMutation per POST /api/generate/single
- useJobStatus(jobId): useQuery per GET /api/generate/job/{jobId}/status con refetchInterval condizionale (2000ms quando running, disabilitato quando completed/failed)
- useJobResults(jobId): useQuery per GET /api/generate/job/{jobId} - useJobResults(jobId): useQuery per GET /api/generate/job/{jobId}
- useDownloadCsv(): funzione che chiama apiDownload e triggera download browser - useDownloadCsv(): funzione che chiama GET /api/export/{jobId}/csv e triggera download browser (CSV originale)
- useDownloadEditedCsv(): funzione che chiama POST /api/export/{jobId}/csv con results modificati e triggera download browser (CSV con edits)
- useFormats(): useQuery per GET /api/calendar/formats - useFormats(): useQuery per GET /api/calendar/formats
4. Creare frontend/src/components/Layout.tsx: 4. Creare frontend/src/components/Layout.tsx:
@@ -172,8 +183,8 @@ Output: SPA React completa con tutte le pagine e componenti per il workflow: con
</action> </action>
<verify> <verify>
- frontend/src/App.tsx ha BrowserRouter con basename="/postgenerator" - frontend/src/App.tsx ha BrowserRouter con basename="/postgenerator"
- frontend/src/api/hooks.ts ha almeno 7 hooks (settings, settingsStatus, updateSettings, generateCalendar, generateSingle, jobResults, downloadCsv) - frontend/src/api/hooks.ts ha almeno 10 hooks/functions (settings, settingsStatus, updateSettings, generateCalendar, generateSingle, jobStatus, jobResults, downloadCsv, downloadEditedCsv, formats)
- frontend/src/types.ts ha CalendarSlot, GeneratedPost, PostResult, Settings - frontend/src/types.ts ha CalendarSlot, GeneratedPost, PostResult, JobStatus, Settings
- Sidebar ha 4 link di navigazione - Sidebar ha 4 link di navigazione
- Settings ha campo API key con tipo password - Settings ha campo API key con tipo password
- Dashboard mostra banner se API key non configurata - Dashboard mostra banner se API key non configurata
@@ -185,16 +196,11 @@ Output: SPA React completa con tutte le pagine e componenti per il workflow: con
</task> </task>
<task type="auto"> <task type="auto">
<name>Task 2: Genera Calendario, Output Review con card/slide/edit, Genera Singolo Post</name> <name>Task 2a: Badge components e PostCard</name>
<files> <files>
frontend/src/pages/GenerateCalendar.tsx
frontend/src/pages/GenerateSingle.tsx
frontend/src/pages/OutputReview.tsx
frontend/src/components/PostCard.tsx
frontend/src/components/SlideViewer.tsx
frontend/src/components/ProgressIndicator.tsx
frontend/src/components/BadgePN.tsx frontend/src/components/BadgePN.tsx
frontend/src/components/BadgeSchwartz.tsx frontend/src/components/BadgeSchwartz.tsx
frontend/src/components/PostCard.tsx
</files> </files>
<action> <action>
1. Creare frontend/src/components/BadgePN.tsx: 1. Creare frontend/src/components/BadgePN.tsx:
@@ -207,30 +213,78 @@ Output: SPA React completa con tutte le pagine e componenti per il workflow: con
- Colori progressivi (L5 chiaro -> L1 scuro) per indicare vicinanza all'acquisto - Colori progressivi (L5 chiaro -> L1 scuro) per indicare vicinanza all'acquisto
- Tooltip con descrizione livello - Tooltip con descrizione livello
3. Creare frontend/src/components/ProgressIndicator.tsx: 3. Creare frontend/src/components/PostCard.tsx:
- Mostra progresso generazione bulk: "Post 3/13 in generazione..."
- Barra di progresso visuale
- Lista dei post con stato: pending (grigio), processing (spinner), success (verde check), failed (rosso X)
- Animazione per il post attualmente in generazione
4. Creare frontend/src/components/PostCard.tsx:
- Card per singolo post nel risultato - Card per singolo post nel risultato
- Mostra: indice, tipo PN (badge), livello Schwartz (badge), formato narrativo, nicchia, data - Mostra: indice, tipo PN (badge), livello Schwartz (badge), formato narrativo, nicchia, data
- Se status=success: mostra cover_title come titolo card, click per espandere - Se status=success: mostra cover_title come titolo card, click per espandere
- Se status=failed: card con sfondo rosso chiaro, icona errore, messaggio errore, pulsante "Riprova" - Se status=failed: card con sfondo rosso chiaro, icona errore, messaggio errore, pulsante "Riprova"
- Pulsante Riprova chiama useGenerateSingle() per rigenerare quel slot - Pulsante Riprova chiama useGenerateSingle() per rigenerare quel slot
- Click su card success -> espande per mostrare SlideViewer - Click su card success -> espande per mostrare SlideViewer (SlideViewer placeholder prop per ora)
</action>
<verify>
- BadgePN.tsx ha 6 colori distinti per i tipi PN
- BadgeSchwartz.tsx ha 5 livelli con tooltip
- PostCard.tsx ha stati distinti per success e failed, con pulsante Riprova
- npm run build completa senza errori TypeScript
</verify>
<done>
Badge PN e Schwartz con colori distinti. PostCard con stati success/failed, badge, e placeholder per SlideViewer expansion.
</done>
</task>
5. Creare frontend/src/components/SlideViewer.tsx: <task type="auto">
<name>Task 2b: SlideViewer con inline edit e ProgressIndicator con polling</name>
<files>
frontend/src/components/SlideViewer.tsx
frontend/src/components/ProgressIndicator.tsx
frontend/src/api/hooks.ts
</files>
<action>
1. Creare frontend/src/components/SlideViewer.tsx:
- Visualizzazione slide-by-slide con navigazione frecce laterali (stile Instagram stories) - Visualizzazione slide-by-slide con navigazione frecce laterali (stile Instagram stories)
- Mostra: slide corrente N/8, headline, body, image_keyword - Mostra: slide corrente N/8, headline, body, image_keyword
- Freccia sinistra/destra per navigare - Freccia sinistra/destra per navigare
- Ogni campo testo e' EDITABILE inline: click per trasformare in input/textarea - Ogni campo testo e' EDITABILE inline: click per trasformare in input/textarea
- Le modifiche aggiornano lo stato locale (PostResult) che verra' usato per il CSV download - Le modifiche aggiornano lo stato locale (PostResult) tramite callback onEdit prop
- Sotto le slide: caption Instagram in textarea editabile - Sotto le slide: caption Instagram in textarea editabile
- Keyboard navigation: frecce sinistra/destra per cambiare slide - Keyboard navigation: frecce sinistra/destra per cambiare slide
6. Creare frontend/src/pages/GenerateCalendar.tsx: 2. Creare frontend/src/components/ProgressIndicator.tsx:
- Riceve job_id come prop
- USA POLLING: chiama GET /api/generate/job/{job_id}/status ogni 2 secondi via useJobStatus(jobId) hook
- Mostra progresso generazione bulk: "Post {completed}/{total} in generazione..."
- Barra di progresso visuale basata su completed/total dal polling response
- Lista dei post con stato: pending (grigio), processing (spinner — il current_post), success (verde check), failed (rosso X)
- Animazione per il post attualmente in generazione (current_post dal polling)
- Quando status diventa "completed": smette di pollare, chiama callback onComplete(jobId)
3. Aggiornare frontend/src/api/hooks.ts:
- Aggiungere useJobStatus(jobId): useQuery per GET /api/generate/job/{jobId}/status con refetchInterval di 2000ms quando status e' "running", disabilitato quando "completed" o "failed"
- Aggiungere useDownloadEditedCsv(): funzione che chiama POST /api/export/{jobId}/csv con i results modificati e triggera download browser
- Aggiornare useGenerateCalendar(): mutation che chiama POST /api/generate/bulk e ritorna {job_id} (non GenerateResponse, dato che ora e' async)
</action>
<verify>
- SlideViewer.tsx ha navigazione frecce e campi editabili inline con callback onEdit
- ProgressIndicator.tsx usa useJobStatus() hook con polling ogni 2 secondi
- ProgressIndicator.tsx smette di pollare quando status != "running"
- hooks.ts ha useJobStatus con refetchInterval condizionale
- hooks.ts ha useDownloadEditedCsv che chiama POST endpoint
- npm run build completa senza errori TypeScript
</verify>
<done>
SlideViewer con navigazione slide e edit inline via callback. ProgressIndicator usa polling real-time su /status endpoint per mostrare progresso per-item. API hooks aggiornati per async generation pattern (job_id + polling + POST CSV con edits).
</done>
</task>
<task type="auto">
<name>Task 2c: Pagine GenerateCalendar, OutputReview, GenerateSingle</name>
<files>
frontend/src/pages/GenerateCalendar.tsx
frontend/src/pages/GenerateSingle.tsx
frontend/src/pages/OutputReview.tsx
</files>
<action>
1. Creare frontend/src/pages/GenerateCalendar.tsx:
- Form con campi: - Form con campi:
- Obiettivo campagna (textarea, obbligatorio, placeholder "Es: Aumentare awareness sull'AI per PMI italiane") - Obiettivo campagna (textarea, obbligatorio, placeholder "Es: Aumentare awareness sull'AI per PMI italiane")
- Settimane (number, default 2, range 1-4) - Settimane (number, default 2, range 1-4)
@@ -239,23 +293,26 @@ Output: SPA React completa con tutte le pagine e componenti per il workflow: con
- Nicchie (multi-select o checkbox, prende default da Settings) - Nicchie (multi-select o checkbox, prende default da Settings)
- Pulsante "Genera Calendario" con stati: - Pulsante "Genera Calendario" con stati:
- Se API key non configurata: disabilitato, messaggio "Configura API key nelle Impostazioni" - Se API key non configurata: disabilitato, messaggio "Configura API key nelle Impostazioni"
- Se configurata: abilitato, al click mostra ProgressIndicator - Se configurata: abilitato, al click chiama useGenerateCalendar() mutation
- FLUSSO ASYNC: al click, mutation ritorna {job_id}. La pagina mostra ProgressIndicator con job_id.
ProgressIndicator polla /status e quando status="completed" chiama onComplete che fa redirect a OutputReview con jobId.
- Usa useSettingsStatus() per controllare API key - Usa useSettingsStatus() per controllare API key
- Usa useGenerateCalendar() mutation
- Al completamento (successo o parziale): redirect a OutputReview con jobId
7. Creare frontend/src/pages/OutputReview.tsx: 2. Creare frontend/src/pages/OutputReview.tsx:
- Riceve jobId da route params - Riceve jobId da route params
- Carica risultati con useJobResults(jobId) - Carica risultati con useJobResults(jobId)
- Header con: nome campagna, conteggio successi/falliti, pulsante "Download CSV" - Header con: nome campagna, conteggio successi/falliti, pulsante "Download CSV"
- Griglia di PostCard (3 colonne desktop, 2 tablet, 1 mobile) - Griglia di PostCard (3 colonne desktop, 2 tablet, 1 mobile)
- PostCard espandibile con SlideViewer - PostCard espandibile con SlideViewer
- Pulsante "Download CSV": - GESTIONE STATO EDIT INLINE:
- Chiama useDownloadCsv(jobId) - Mantiene stato locale dei post (copia di GenerateResponse)
- Quando utente edita una slide in SlideViewer, aggiorna lo stato locale via callback
- Il pulsante "Download CSV" invia lo stato locale modificato al backend via POST /api/export/{jobId}/csv (useDownloadEditedCsv hook)
- Questo garantisce che il CSV rifletta le modifiche inline dell'utente
- Se ci sono post falliti: mostra nota "Il CSV contiene solo i N post generati con successo" - Se ci sono post falliti: mostra nota "Il CSV contiene solo i N post generati con successo"
- Se tutti i post sono falliti: messaggio "Nessun post generato con successo. Riprova." - Se tutti i post sono falliti: messaggio "Nessun post generato con successo. Riprova."
8. Creare frontend/src/pages/GenerateSingle.tsx: 3. Creare frontend/src/pages/GenerateSingle.tsx:
- Form per generare un singolo post manualmente: - Form per generare un singolo post manualmente:
- Topic (textarea, obbligatorio) - Topic (textarea, obbligatorio)
- Tipo contenuto (select: valore, storytelling, news, riprova_sociale, coinvolgimento, promozione) - Tipo contenuto (select: valore, storytelling, news, riprova_sociale, coinvolgimento, promozione)
@@ -265,30 +322,23 @@ Output: SPA React completa con tutte le pagine e componenti per il workflow: con
- Al submit: chiama useGenerateSingle() - Al submit: chiama useGenerateSingle()
- Mostra risultato con SlideViewer direttamente nella pagina - Mostra risultato con SlideViewer direttamente nella pagina
- Pulsante download CSV per singolo post - Pulsante download CSV per singolo post
GESTIONE STATO EDIT INLINE (importante):
- OutputReview mantiene stato locale dei post (copia di GenerateResponse)
- Quando utente edita una slide in SlideViewer, aggiorna lo stato locale
- Il pulsante Download CSV usa lo stato locale aggiornato (non l'originale dal server)
- Questo significa che il CSV riflette le modifiche dell'utente
</action> </action>
<verify> <verify>
- GenerateCalendar.tsx ha form con obiettivo e settimane, pulsante disabilitato senza API key - GenerateCalendar.tsx ha form con obiettivo e settimane, pulsante disabilitato senza API key
- GenerateCalendar.tsx mostra ProgressIndicator con job_id dopo submit (non attende risposta sincrona)
- OutputReview.tsx mostra griglia di PostCard con badge PN e Schwartz - OutputReview.tsx mostra griglia di PostCard con badge PN e Schwartz
- SlideViewer.tsx ha navigazione frecce e campi editabili inline - OutputReview.tsx usa useDownloadEditedCsv per inviare edits al backend prima del download
- PostCard.tsx ha stati distinti per success e failed, con pulsante Riprova
- ProgressIndicator.tsx mostra progresso per-item
- GenerateSingle.tsx ha form con select per tipo, livello, nicchia, formato - GenerateSingle.tsx ha form con select per tipo, livello, nicchia, formato
- npm run build completa senza errori TypeScript - npm run build completa senza errori TypeScript
</verify> </verify>
<done> <done>
Web UI completa: form Genera Calendario con progress, griglia risultati con card/badge, SlideViewer con navigazione e edit inline, download CSV con modifiche utente, Genera Singolo Post, gestione errori per-item con Riprova. Pagine complete: GenerateCalendar con form + ProgressIndicator async polling. OutputReview con griglia card, SlideViewer expansion, edit inline che si riflettono nel CSV via POST endpoint. GenerateSingle con form e anteprima.
</done> </done>
</task> </task>
<task type="checkpoint:human-verify" gate="blocking"> <task type="checkpoint:human-verify" gate="blocking">
<what-built> <what-built>
Web UI completa con tutte le pagine: Dashboard, Genera Calendario, Output Review, Genera Singolo Post, Impostazioni. Inclusi progress indicator, griglia card con badge, navigazione slide, edit inline. Web UI completa con tutte le pagine: Dashboard, Genera Calendario, Output Review, Genera Singolo Post, Impostazioni. Inclusi progress indicator con polling real-time, griglia card con badge, navigazione slide, edit inline con CSV export tramite POST.
</what-built> </what-built>
<how-to-verify> <how-to-verify>
1. Verificare che `cd frontend && npm run build` completa senza errori 1. Verificare che `cd frontend && npm run build` completa senza errori
@@ -308,16 +358,20 @@ Output: SPA React completa con tutte le pagine e componenti per il workflow: con
3. Tutti i componenti importano tipi da types.ts (non definiscono tipi inline) 3. Tutti i componenti importano tipi da types.ts (non definiscono tipi inline)
4. API hooks usano /postgenerator/api come base URL 4. API hooks usano /postgenerator/api come base URL
5. PostCard ha due varianti visive: success (espandibile) e failed (errore + riprova) 5. PostCard ha due varianti visive: success (espandibile) e failed (errore + riprova)
6. SlideViewer supporta edit inline e navigazione frecce 6. SlideViewer supporta edit inline e navigazione frecce, con callback onEdit
7. GenerateCalendar disabilita pulsante se API key non configurata 7. GenerateCalendar disabilita pulsante se API key non configurata
8. OutputReview fa download CSV con le modifiche inline dell'utente 8. GenerateCalendar mostra ProgressIndicator con job_id (non attende risposta sincrona)
9. ProgressIndicator polla /api/generate/job/{job_id}/status ogni 2s e smette quando completato
10. OutputReview usa useDownloadEditedCsv (POST) per scaricare CSV con modifiche inline
</verification> </verification>
<success_criteria> <success_criteria>
- Dashboard mostra stato API key e quick actions - Dashboard mostra stato API key e quick actions
- Settings permette configurazione API key, modello, nicchie, frequenza - Settings permette configurazione API key, modello, nicchie, frequenza
- Genera Calendario ha form, progress indicator, redirect a risultati - Genera Calendario ha form, async submit con job_id, ProgressIndicator con polling real-time
- Output Review mostra griglia card con badge, slide viewer con edit, download CSV - ProgressIndicator polla /status e mostra progresso per-item (pending/processing/success/failed)
- Output Review mostra griglia card con badge, slide viewer con edit inline
- Download CSV invia edits al backend via POST e riceve CSV aggiornato
- Post falliti mostrano errore e pulsante Riprova - Post falliti mostrano errore e pulsante Riprova
- Genera Singolo Post ha form completo con anteprima risultato - Genera Singolo Post ha form completo con anteprima risultato
- Build frontend completa senza errori - Build frontend completa senza errori