docs(03): create phase plan

Phase 03: Organization Layer
- 2 plan(s) in 2 wave(s)
- Wave 1: SwipeService CRUD + API + UI (parallel-ready)
- Wave 2: Swipe-to-calendar integration (depends on 03-01)
- Ready for execution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michele
2026-03-08 23:21:41 +01:00
parent 0b7a6efccd
commit 05988f4be5
3 changed files with 553 additions and 3 deletions

View File

@@ -0,0 +1,338 @@
---
phase: 03-organization-layer
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- backend/services/swipe_service.py
- backend/schemas/swipe.py
- backend/routers/swipe.py
- backend/main.py
- frontend/src/types.ts
- frontend/src/api/hooks.ts
- frontend/src/pages/SwipeFile.tsx
- frontend/src/components/Sidebar.tsx
- frontend/src/App.tsx
autonomous: true
must_haves:
truths:
- "L'utente accede alla pagina Swipe File dalla sidebar e vede la lista delle idee salvate (o un messaggio vuoto)"
- "L'utente aggiunge un'idea con topic e opzionalmente nicchia e note; l'idea appare immediatamente nella lista"
- "L'utente clicca su un'idea e la modifica inline; il salvataggio persiste"
- "L'utente elimina un'idea con conferma; la lista si aggiorna"
- "Le idee persistono al riavvio del container (file JSON su disco)"
- "La lista e' ordinata cronologicamente (piu' recenti prima) e filtrabile per nicchia"
artifacts:
- path: "backend/services/swipe_service.py"
provides: "CRUD per swipe_file.json con load/save/add/update/delete"
contains: "class SwipeService"
- path: "backend/schemas/swipe.py"
provides: "Pydantic models per SwipeItem, SwipeItemCreate, SwipeItemUpdate"
contains: "class SwipeItem"
- path: "backend/routers/swipe.py"
provides: "REST API endpoints GET/POST/PUT/DELETE per swipe file"
contains: "router = APIRouter"
- path: "frontend/src/pages/SwipeFile.tsx"
provides: "Pagina SwipeFile con lista, form aggiunta, modifica inline, eliminazione con conferma"
contains: "export function SwipeFile"
key_links:
- from: "backend/routers/swipe.py"
to: "backend/services/swipe_service.py"
via: "SwipeService instance"
pattern: "SwipeService"
- from: "frontend/src/pages/SwipeFile.tsx"
to: "/api/swipe"
via: "TanStack Query hooks"
pattern: "useSwipeItems|useAddSwipeItem|useUpdateSwipeItem|useDeleteSwipeItem"
- from: "backend/main.py"
to: "backend/routers/swipe.py"
via: "app.include_router"
pattern: "swipe\\.router"
- from: "frontend/src/App.tsx"
to: "frontend/src/pages/SwipeFile.tsx"
via: "React Router Route"
pattern: "swipe-file"
---
<objective>
SwipeService backend (CRUD JSON file-based), endpoint FastAPI REST, e pagina Web UI completa per gestione Swipe File con aggiunta rapida, modifica inline, eliminazione con conferma, filtro per nicchia.
Purpose: Dare all'utente uno strumento di cattura veloce per idee e topic interessanti, consultabile e persistente, prima dell'integrazione con il form Genera Calendario (Piano 02).
Output: Backend CRUD funzionante su data/swipe_file.json + pagina Swipe File nella Web UI con tutte le operazioni CRUD.
</objective>
<execution_context>
@C:\Users\miche\.claude/get-shit-done/workflows/execute-plan.md
@C:\Users\miche\.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/03-organization-layer/03-CONTEXT.md
@.planning/phases/01-core-generation-pipeline/01-04-SUMMARY.md
</context>
<tasks>
<task type="auto">
<name>Task 1: Backend — SwipeService + Pydantic schemas + FastAPI router</name>
<files>
backend/schemas/swipe.py
backend/services/swipe_service.py
backend/routers/swipe.py
backend/main.py
</files>
<action>
**1. Crea `backend/schemas/swipe.py`** — Pydantic models:
```python
class SwipeItem(BaseModel):
id: str # UUID generato dal backend
topic: str # Obbligatorio, min 3 chars
nicchia: Optional[str] = None # Opzionale (es. "dentisti", "ecommerce")
note: Optional[str] = None # Note libere opzionali
created_at: str # ISO datetime
updated_at: str # ISO datetime
used: bool = False # True quando usato come override in calendario
class SwipeItemCreate(BaseModel):
topic: str = Field(..., min_length=3, max_length=200)
nicchia: Optional[str] = None
note: Optional[str] = None
class SwipeItemUpdate(BaseModel):
topic: Optional[str] = Field(None, min_length=3, max_length=200)
nicchia: Optional[str] = None
note: Optional[str] = None
class SwipeListResponse(BaseModel):
items: list[SwipeItem]
total: int
```
**2. Crea `backend/services/swipe_service.py`** — SwipeService class:
- `__init__(self, data_path: Path)`: riceve la directory DATA_PATH, file = `data_path / "swipe_file.json"`
- `_load() -> list[dict]`: legge il file JSON, ritorna lista vuota se non esiste
- `_save(items: list[dict])`: scrive il file JSON con encoding utf-8, indent=2
- `list_items() -> list[SwipeItem]`: carica, ordina per `created_at` descending (piu' recenti prima), ritorna come SwipeItem
- `add_item(data: SwipeItemCreate) -> SwipeItem`: genera UUID, datetime now ISO, salva, ritorna item creato
- `update_item(item_id: str, data: SwipeItemUpdate) -> SwipeItem`: trova per id, aggiorna solo campi non-None, aggiorna `updated_at`, salva. Raise ValueError se non trovato.
- `delete_item(item_id: str) -> bool`: trova per id, rimuove, salva. Raise ValueError se non trovato.
- `mark_used(item_id: str) -> SwipeItem`: setta `used=True`, aggiorna `updated_at`. Raise ValueError se non trovato.
- Usa `uuid.uuid4().hex[:12]` per ID brevi. Usa `datetime.now(timezone.utc).isoformat()` per timestamp.
**3. Crea `backend/routers/swipe.py`** — FastAPI router:
- `router = APIRouter(prefix="/api/swipe", tags=["swipe"])`
- Lazy init pattern identico a prompts.py: `_swipe_service: SwipeService | None = None` + `_get_swipe_service()` che crea l'istanza con `DATA_PATH`
- Endpoint:
- `GET /` -> `SwipeListResponse` — lista tutte le idee
- `POST /` -> `SwipeItem` (status 201) — aggiunge idea
- `PUT /{item_id}` -> `SwipeItem` — modifica idea (ValueError -> 404)
- `DELETE /{item_id}` -> `{"deleted": True}` (status 200) — elimina idea (ValueError -> 404)
- `POST /{item_id}/mark-used` -> `SwipeItem` — segna come usata (ValueError -> 404)
**4. Registra router in `backend/main.py`**:
- Importa `from backend.routers import ... swipe`
- Aggiungi `app.include_router(swipe.router)` PRIMA del SPA catch-all mount, nella sezione con gli altri router. Ordine: calendar, generate, export, settings, prompts, **swipe**.
- Aggiungi `DATA_PATH` al lifespan `mkdir` se non gia' incluso (verificare).
**Pattern da seguire**: Il router prompts.py e' il template di riferimento per lo stile, lazy init, error handling.
</action>
<verify>
- `python -c "from backend.schemas.swipe import SwipeItem, SwipeItemCreate, SwipeItemUpdate, SwipeListResponse; print('Schemas OK')"` passa
- `python -c "from backend.services.swipe_service import SwipeService; print('Service OK')"` passa
- `python -c "from backend.routers.swipe import router; print('Router OK')"` passa
- `python -c "from backend.main import app; print([r.path for r in app.routes])"` include `/api/swipe`
</verify>
<done>
- SwipeService legge/scrive data/swipe_file.json con CRUD completo
- Router espone 5 endpoint REST: list, add, update, delete, mark-used
- Router registrato in main.py prima del SPA catch-all
</done>
</task>
<task type="auto">
<name>Task 2: Frontend — TypeScript types + TanStack Query hooks + pagina SwipeFile + navigazione</name>
<files>
frontend/src/types.ts
frontend/src/api/hooks.ts
frontend/src/pages/SwipeFile.tsx
frontend/src/components/Sidebar.tsx
frontend/src/App.tsx
</files>
<action>
**1. Aggiungi types in `frontend/src/types.ts`** — Alla fine del file, sezione "Swipe File":
```typescript
// ---------------------------------------------------------------------------
// Swipe File
// ---------------------------------------------------------------------------
export interface SwipeItem {
id: string
topic: string
nicchia?: string | null
note?: string | null
created_at: string
updated_at: string
used: boolean
}
export interface SwipeItemCreate {
topic: string
nicchia?: string | null
note?: string | null
}
export interface SwipeItemUpdate {
topic?: string | null
nicchia?: string | null
note?: string | null
}
export interface SwipeListResponse {
items: SwipeItem[]
total: number
}
```
**2. Aggiungi hooks in `frontend/src/api/hooks.ts`** — Alla fine del file, sezione "Swipe File":
```typescript
// ---------------------------------------------------------------------------
// Swipe File
// ---------------------------------------------------------------------------
export function useSwipeItems() {
return useQuery<SwipeListResponse>({
queryKey: ['swipe'],
queryFn: () => apiGet<SwipeListResponse>('/swipe'),
staleTime: 30_000,
})
}
export function useAddSwipeItem() {
const queryClient = useQueryClient()
return useMutation<SwipeItem, Error, SwipeItemCreate>({
mutationFn: (item) => apiPost<SwipeItem>('/swipe', item),
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['swipe'] }) },
})
}
export function useUpdateSwipeItem() {
const queryClient = useQueryClient()
return useMutation<SwipeItem, Error, { id: string; data: SwipeItemUpdate }>({
mutationFn: ({ id, data }) => apiPut<SwipeItem>(`/swipe/${id}`, data),
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['swipe'] }) },
})
}
export function useDeleteSwipeItem() {
const queryClient = useQueryClient()
return useMutation<{ deleted: boolean }, Error, string>({
mutationFn: (id) => apiFetch<{ deleted: boolean }>(`/swipe/${id}`, { method: 'DELETE' }),
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['swipe'] }) },
})
}
```
Importare i nuovi tipi (`SwipeItem`, `SwipeItemCreate`, `SwipeItemUpdate`, `SwipeListResponse`) e `apiFetch` dove necessario.
**3. Crea `frontend/src/pages/SwipeFile.tsx`** — Pagina completa:
Layout: stone/amber palette coerente con il resto dell'app.
**Struttura pagina:**
- Header: titolo "Swipe File", sottotitolo breve, contatore totale idee
- Form aggiunta rapida (inline, sempre visibile sopra la lista):
- Input topic (obbligatorio, placeholder "Scrivi un'idea o topic...")
- Input nicchia (opzionale, placeholder "Es. dentisti")
- Input note (opzionale, placeholder "Note aggiuntive...")
- Bottone "Aggiungi" (amber, con icona Plus)
- Reset form dopo successo
- Filtro nicchia: chip/pill cliccabili sopra la lista. Mostra tutte le nicchie uniche trovate nelle idee + chip "Tutte". Click filtra la lista. Chip attivo evidenziato in amber.
- Lista idee (cronologico inverso): card stone-800 per ogni idea con:
- Topic in bold come titolo
- Badge nicchia (se presente) — pill colorata
- Badge "Usato" se `used === true` — amber badge con icona CheckCircle
- Note se presenti — testo secondario stone-400
- Data relativa ("3 giorni fa", "1 ora fa") — usa logica semplice basata su differenza date, non libreria
- Bottone modifica (icona Pencil) — click attiva editing inline:
- I campi della card diventano input editabili
- Mostra bottoni Salva (Check) e Annulla (X)
- Salva chiama `useUpdateSwipeItem`, Annulla ripristina valori originali
- Bottone elimina (icona Trash2) — click mostra dialog conferma:
- Testo: "Eliminare questa idea? L'azione e' irreversibile."
- Bottoni: "Annulla" e "Elimina" (rosso)
- Conferma chiama `useDeleteSwipeItem`
- Stato vuoto: messaggio "Nessuna idea salvata" con invito a usare il form sopra
**Pattern UI da seguire**: Stile card identico a PostCard (stone-800, border stone-700, rounded-xl). Palette amber per accent. Lucide icons coerenti.
**Icone lucide da usare**: `Plus`, `Pencil`, `Trash2`, `CheckCircle`, `X`, `Check`, `Lightbulb`, `Filter`
**State management**:
- `editingId: string | null` — quale idea e' in modalita' edit
- `editForm: { topic, nicchia, note }` — valori temporanei durante editing
- `deleteConfirmId: string | null` — quale idea mostra il dialog di conferma
- `filterNicchia: string | null` — nicchia selezionata per filtro (null = tutte)
- `newItem: { topic, nicchia, note }` — form di aggiunta
**4. Aggiungi route in `frontend/src/App.tsx`**:
- Importa `SwipeFile` da `./pages/SwipeFile`
- Aggiungi `<Route path="/swipe-file" element={<SwipeFile />} />` dopo la route `/prompt-editor`
**5. Aggiungi voce navigazione in `frontend/src/components/Sidebar.tsx`**:
- Aggiungi nella lista `navItems`, DOPO "Prompt Editor" e PRIMA di "Impostazioni":
```
{ to: '/swipe-file', label: 'Swipe File', icon: <Lightbulb size={18} /> }
```
- Importare `Lightbulb` da lucide-react
</action>
<verify>
- `cd frontend && npx tsc --noEmit` — nessun errore TypeScript
- `cd frontend && npm run build` — build pulita senza errori
- Sidebar contiene 6 link di navigazione (Dashboard, Genera Calendario, Genera Singolo Post, Prompt Editor, Swipe File, Impostazioni)
- Route `/swipe-file` esiste in App.tsx
- hooks.ts contiene 4 nuovi hook: useSwipeItems, useAddSwipeItem, useUpdateSwipeItem, useDeleteSwipeItem
</verify>
<done>
- Pagina SwipeFile completa con form aggiunta, lista con filtro nicchia, modifica inline, eliminazione con conferma
- 4 TanStack Query hooks che cablano tutti gli endpoint swipe
- Navigazione sidebar con link "Swipe File" con icona Lightbulb
- Route `/swipe-file` registrata in App.tsx
- TypeScript compila senza errori
</done>
</task>
</tasks>
<verification>
1. Backend: `python -c "from backend.main import app; print('OK')"` — app importa senza errori
2. Frontend: `cd frontend && npx tsc --noEmit && npm run build` — build pulita
3. Sidebar mostra 6 link di navigazione con Swipe File presente
4. Route `/swipe-file` registrata e componente SwipeFile renderizza
5. types.ts contiene le 4 interfacce SwipeItem, SwipeItemCreate, SwipeItemUpdate, SwipeListResponse
6. hooks.ts contiene 4 nuovi hook per CRUD swipe
7. Router swipe registrato in main.py con 5 endpoint
</verification>
<success_criteria>
- SwipeService legge e scrive data/swipe_file.json con CRUD completo (add, update, delete, mark_used, list con ordinamento cronologico inverso)
- 5 endpoint REST funzionanti: GET /api/swipe, POST /api/swipe, PUT /api/swipe/{id}, DELETE /api/swipe/{id}, POST /api/swipe/{id}/mark-used
- Pagina SwipeFile accessibile dalla sidebar con form aggiunta inline, lista filtrata per nicchia, modifica inline, eliminazione con conferma dialog
- TypeScript e build frontend compilano senza errori
</success_criteria>
<output>
After completion, create `.planning/phases/03-organization-layer/03-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,212 @@
---
phase: 03-organization-layer
plan: 02
type: execute
wave: 2
depends_on: ["03-01"]
files_modified:
- frontend/src/pages/GenerateCalendar.tsx
- frontend/src/api/hooks.ts
autonomous: true
must_haves:
truths:
- "L'utente puo' aprire un picker dallo Swipe File nel form Genera Calendario e selezionare un topic come override per uno slot specifico"
- "Dopo la selezione, l'idea nello Swipe File viene marcata come 'usata' con badge visivo"
- "L'utente vede i topic override selezionati nel form prima di avviare la generazione"
- "I topic override selezionati vengono passati alla generazione bulk e applicati agli slot corrispondenti"
artifacts:
- path: "frontend/src/pages/GenerateCalendar.tsx"
provides: "Sezione override topic con picker da Swipe File integrato nel form"
contains: "swipe"
key_links:
- from: "frontend/src/pages/GenerateCalendar.tsx"
to: "/api/swipe"
via: "useSwipeItems hook + useMarkSwipeUsed"
pattern: "useSwipeItems|markUsed"
- from: "frontend/src/pages/GenerateCalendar.tsx"
to: "/api/generate/bulk"
via: "CalendarRequest with topic_overrides"
pattern: "topic_overrides"
---
<objective>
Integrazione Swipe File nel form Genera Calendario: meccanismo per selezionare topic dallo Swipe File come override per slot specifici prima della generazione batch.
Purpose: Permette all'utente di usare idee catturate nello Swipe File come topic pre-assegnati nel calendario, combinando l'ispirazione spontanea con la generazione strategica.
Output: Sezione "Topic Override" nel form Genera Calendario con picker modale da Swipe File, invio degli override alla generazione bulk.
</objective>
<execution_context>
@C:\Users\miche\.claude/get-shit-done/workflows/execute-plan.md
@C:\Users\miche\.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/03-organization-layer/03-CONTEXT.md
@.planning/phases/03-organization-layer/03-01-SUMMARY.md
</context>
<tasks>
<task type="auto">
<name>Task 1: Backend — topic_overrides in CalendarRequest + pipeline wiring</name>
<files>
backend/schemas/calendar.py
backend/services/generation_pipeline.py
</files>
<action>
**1. Aggiorna `backend/schemas/calendar.py`** — Aggiungi campo `topic_overrides` a `CalendarRequest`:
```python
topic_overrides: Optional[dict[int, str]] = Field(
default=None,
description="Override topic per slot specifici. Chiave: indice slot (0-12), valore: topic. "
"Gli slot con override skipperanno la generazione topic LLM.",
)
```
Questo campo e' un dizionario `{slot_index: topic_string}`. Esempio: `{2: "3 errori che costano clienti", 7: "Case study dentista Milano"}`.
**2. Aggiorna `backend/services/generation_pipeline.py`** — Nel metodo che genera i topic per ogni slot (dentro `generate_bulk_async` o il metodo chiamato da esso), dopo aver creato i CalendarSlot:
- Se `request.topic_overrides` esiste e contiene l'indice dello slot corrente, usa il topic override invece di chiamare l'LLM per generare il topic.
- Applica il topic override al campo `slot.topic` dello slot corrispondente.
- La logica e': `if topic_overrides and slot.indice in topic_overrides: slot.topic = topic_overrides[slot.indice]` — skip la chiamata LLM per generare il topic per quello slot.
Assicurarsi che il campo topic venga applicato PRIMA della generazione del contenuto del carosello, perche' il topic viene passato al prompt LLM.
**Pattern**: Leggere attentamente `generation_pipeline.py` per capire dove i topic vengono generati/assegnati e inserire l'override nel punto giusto del flusso.
</action>
<verify>
- `python -c "from backend.schemas.calendar import CalendarRequest; r = CalendarRequest(obiettivo_campagna='Test obiettivo campagna', topic_overrides={0: 'Test topic'}); print(r.topic_overrides)"` stampa `{0: 'Test topic'}`
- `python -c "from backend.services.generation_pipeline import GenerationPipeline; print('OK')"` importa senza errori
</verify>
<done>
- CalendarRequest accetta `topic_overrides` opzionale (dict[int, str])
- GenerationPipeline applica topic override agli slot corrispondenti prima della generazione LLM
- Slot senza override continuano a funzionare come prima (topic generato dall'LLM)
</done>
</task>
<task type="auto">
<name>Task 2: Frontend — Picker Swipe File nel form Genera Calendario + mark used</name>
<files>
frontend/src/pages/GenerateCalendar.tsx
frontend/src/api/hooks.ts
frontend/src/types.ts
</files>
<action>
**1. Aggiorna `frontend/src/types.ts`** — Aggiungi `topic_overrides` a `CalendarRequest`:
```typescript
export interface CalendarRequest {
obiettivo_campagna: string
settimane?: number
nicchie?: string[] | null
frequenza_post?: number
data_inizio?: string | null
topic_overrides?: Record<number, string> | null // slot_index -> topic
}
```
**2. Aggiungi hook `useMarkSwipeUsed` in `frontend/src/api/hooks.ts`** (nella sezione Swipe File):
```typescript
export function useMarkSwipeUsed() {
const queryClient = useQueryClient()
return useMutation<SwipeItem, Error, string>({
mutationFn: (id) => apiPost<SwipeItem>(`/swipe/${id}/mark-used`),
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['swipe'] }) },
})
}
```
**3. Modifica `frontend/src/pages/GenerateCalendar.tsx`** — Aggiungi sezione "Topic Override dallo Swipe File":
**Nuovo state:**
```typescript
const [topicOverrides, setTopicOverrides] = useState<Record<number, { topic: string; swipeId: string }>>({})
const [showSwipePicker, setShowSwipePicker] = useState<number | null>(null) // slot index attivo
```
**Sezione UI** — Aggiungere DOPO il campo "Tono di voce" e PRIMA del checkbox Nicchie:
- Titolo: "Topic Override (opzionale)" con sottotitolo "Seleziona topic dallo Swipe File per slot specifici"
- Griglia 13 slot (indici 0-12), ognuno con:
- Label: "Slot {N+1}" (1-based per utente)
- Se override assegnato: mostra topic troncato + bottone X per rimuovere override
- Se nessun override: bottone piccolo "Da Swipe File" (icona Lightbulb) per aprire il picker
- Il picker e' un pannello/dropdown che appare inline sotto lo slot cliccato:
- Mostra lista idee dallo Swipe File (via `useSwipeItems`)
- Ogni idea mostra: topic, nicchia badge, badge "usato" se gia' usata
- Click su un'idea: assegna il topic allo slot, chiude picker, chiama `useMarkSwipeUsed(swipeId)`
- Bottone "Chiudi" per chiudere senza selezionare
- Se Swipe File vuoto: messaggio "Nessuna idea nel tuo Swipe File" con link a `/swipe-file`
**Modifica handleSubmit** — Includi topic_overrides nella request:
```typescript
const req: CalendarRequest = {
obiettivo_campagna: obiettivo.trim(),
settimane,
frequenza_post: settings?.frequenza_post ?? 3,
nicchie: customNicchie ? settings?.nicchie_attive : undefined,
topic_overrides: Object.keys(topicOverrides).length > 0
? Object.fromEntries(
Object.entries(topicOverrides).map(([k, v]) => [parseInt(k), v.topic])
)
: undefined,
}
```
**Stile UI:**
- La griglia dei 13 slot usa un layout compatto: 3-4 colonne su desktop, 1 su mobile
- Ogni slot e' una mini-card stone-800 con bordo stone-700
- Slot con override: bordo amber-500/30, background amber-500/5
- Il picker inline usa la stessa palette stone/amber
- Animazione ingresso/uscita non necessaria — keep it simple
</action>
<verify>
- `cd frontend && npx tsc --noEmit` — nessun errore TypeScript
- `cd frontend && npm run build` — build pulita
- CalendarRequest in types.ts include `topic_overrides`
- hooks.ts contiene `useMarkSwipeUsed`
- GenerateCalendar.tsx importa `useSwipeItems` e `useMarkSwipeUsed`
</verify>
<done>
- Il form Genera Calendario mostra 13 slot con possibilita' di assegnare topic override dallo Swipe File
- Click "Da Swipe File" apre picker inline con lista idee
- Selezione di un'idea assegna il topic allo slot e marca l'idea come "usata"
- Override rimovibili con bottone X
- Gli override vengono passati come `topic_overrides` nella CalendarRequest alla generazione bulk
- TypeScript compila senza errori
</done>
</task>
</tasks>
<verification>
1. Frontend: `cd frontend && npx tsc --noEmit && npm run build` — build pulita
2. Backend: `python -c "from backend.schemas.calendar import CalendarRequest; print(CalendarRequest.model_fields.keys())"` include topic_overrides
3. CalendarRequest in types.ts include topic_overrides
4. GenerateCalendar.tsx ha griglia 13 slot con bottoni picker
5. Hook useMarkSwipeUsed presente in hooks.ts
6. La generazione bulk riceve e applica topic_overrides
</verification>
<success_criteria>
- L'utente puo' selezionare topic dallo Swipe File per slot specifici nel form Genera Calendario
- I topic selezionati vengono visualizzati nella griglia slot prima della generazione
- I topic override vengono inviati alla generazione bulk via CalendarRequest.topic_overrides
- L'idea Swipe File selezionata viene marcata come "usata" con badge visivo
- Gli override sono rimovibili (bottone X) prima di avviare la generazione
- Slot senza override continuano a generare topic automaticamente via LLM
</success_criteria>
<output>
After completion, create `.planning/phases/03-organization-layer/03-02-SUMMARY.md`
</output>