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>