--- 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" --- 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. @C:\Users\miche\.claude/get-shit-done/workflows/execute-plan.md @C:\Users\miche\.claude/get-shit-done/templates/summary.md @.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 Task 1: Backend — SwipeService + Pydantic schemas + FastAPI router backend/schemas/swipe.py backend/services/swipe_service.py backend/routers/swipe.py backend/main.py **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. - `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` - 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 Task 2: Frontend — TypeScript types + TanStack Query hooks + pagina SwipeFile + navigazione frontend/src/types.ts frontend/src/api/hooks.ts frontend/src/pages/SwipeFile.tsx frontend/src/components/Sidebar.tsx frontend/src/App.tsx **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({ queryKey: ['swipe'], queryFn: () => apiGet('/swipe'), staleTime: 30_000, }) } export function useAddSwipeItem() { const queryClient = useQueryClient() return useMutation({ mutationFn: (item) => apiPost('/swipe', item), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['swipe'] }) }, }) } export function useUpdateSwipeItem() { const queryClient = useQueryClient() return useMutation({ mutationFn: ({ id, data }) => apiPut(`/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 `} />` 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: } ``` - Importare `Lightbulb` da lucide-react - `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 - 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 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 - 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 After completion, create `.planning/phases/03-organization-layer/03-01-SUMMARY.md`