# Améliorations Sprint — Jardin App
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Améliorer l'UI (settings taille/responsive), nettoyer la BDD, ajouter 404, sélection cases grille pour plantation, astuce du jour, médias galerie, export JSON, observations frontend.
**Architecture:** Frontend Vue 3 + Tailwind (CSS vars pour tailles), backend FastAPI + SQLModel + SQLite. Les settings de taille sont stockés dans `user_settings` (table clé-valeur) et appliqués via des variables CSS sur `:root` dans App.vue au chargement.
**Tech Stack:** Vue 3 TypeScript, Tailwind CSS, FastAPI, SQLModel, SQLite, Docker Compose
---
## Contexte du projet
- Backend: `/home/gilles/Documents/vscode/jardin/backend/` sur port 8060 (Docker)
- Frontend: `/home/gilles/Documents/vscode/jardin/frontend/` sur port 8061 (Docker)
- DB: `/home/gilles/Documents/vscode/jardin/data/jardin.db`
- Rebuild backend: `docker compose build backend && docker compose up -d backend` (depuis la racine du projet)
- Rebuild frontend: `docker compose build frontend && docker compose up -d frontend`
- Les `.vue` et `.ts` sont dans `frontend/src/`
- Table `user_settings` : colonnes `cle` (str, PK), `valeur` (str)
- API settings: `GET /api/settings` → `{cle: valeur, ...}` / `PUT /api/settings` → body `{cle: valeur, ...}`
- Thème Gruvbox Dark: bg=#282828, bg-soft=#3c3836, bg-hard=#1d2021, text=#ebdbb2, green=#b8bb26, etc.
---
## Task 1: Settings UI — Paramètres de taille d'interface
**Priorité:** Haute (demandé explicitement)
**Files:**
- Modify: `frontend/src/views/ReglagesView.vue`
- Modify: `frontend/src/App.vue`
**Paramètres à ajouter (clés dans user_settings):**
- `ui_font_size` : taille texte général (12–20px, défaut 14)
- `ui_menu_font_size` : taille texte menu latéral (11–18px, défaut 13)
- `ui_menu_icon_size` : taille icônes menu (14–28px, défaut 18)
- `ui_thumb_size` : taille miniatures images/vidéos (60–200px, défaut 96)
**Step 1: Modifier ReglagesView.vue — ajouter section Interface**
Ajouter une nouvelle section avant la section "Général" dans `ReglagesView.vue` :
```vue
Interface
Ajustez les tailles d'affichage. Les changements sont appliqués instantanément.
{{ uiSizes[s.key] }}{{ s.unit }}
{{ uiSavedMsg }}
```
Dans `
```
**Step 2: Ajouter route catch-all dans router/index.ts**
Ajouter en dernière position dans le tableau `routes` :
```typescript
{ path: '/:pathMatch(.*)*', component: () => import('@/views/NotFoundView.vue') },
```
**Step 3: Rebuild et tester**
```bash
docker compose build frontend && docker compose up -d frontend
```
Aller sur http://10.0.1.109:8061/page-inexistante → vérifier 404 page.
**Step 4: Commit**
```bash
git add frontend/src/views/NotFoundView.vue frontend/src/router/index.ts
git commit -m "feat(frontend): page 404 avec redirect vers dashboard"
```
---
## Task 5: B3 — Populer la table lunarcalendarentry
**Priorité:** Haute
**Contexte:** La table `lunarcalendarentry` existe (0 lignes). Le service lunar est dans `backend/app/services/lunar.py` et retourne les données par mois via `/api/lunar?month=YYYY-MM`.
**Files:**
- Read first: `backend/app/routers/lunar.py`
- Read first: `backend/app/models/settings.py` (pour voir LunarCalendarEntry)
- Modify: `backend/app/routers/lunar.py` (auto-persist after compute)
**Step 1: Lire les fichiers concernés**
```bash
cat /home/gilles/Documents/vscode/jardin/backend/app/routers/lunar.py
cat /home/gilles/Documents/vscode/jardin/backend/app/services/lunar.py | head -80
```
**Step 2: Vérifier le modèle LunarCalendarEntry**
```bash
cat /home/gilles/Documents/vscode/jardin/backend/app/models/settings.py
```
**Step 3: Modifier le router lunar pour persister les données**
Dans `backend/app/routers/lunar.py`, après avoir calculé les données du mois, sauvegarder chaque jour dans `lunarcalendarentry`. Pattern upsert :
```python
from app.models.settings import LunarCalendarEntry
from sqlmodel import Session, select
from app.database import get_session
# Dans l'endpoint GET /api/lunar?month=YYYY-MM :
# Après avoir calculé days_data, persister :
for day_data in days_data:
existing = session.exec(
select(LunarCalendarEntry).where(LunarCalendarEntry.date == day_data['date'])
).first()
if not existing:
entry = LunarCalendarEntry(
date=day_data['date'],
phase=day_data.get('phase'),
illumination=day_data.get('illumination'),
type_jour=day_data.get('type_jour'),
# ... autres champs selon le modèle
)
session.add(entry)
session.commit()
```
**Step 4: Pré-populer les 3 prochains mois au démarrage**
Dans `backend/app/main.py`, dans le lifespan, après la migration, appeler l'endpoint lunar pour les 3 mois autour de la date courante pour pré-remplir la table.
**Step 5: Vérifier**
```bash
sqlite3 /home/gilles/Documents/vscode/jardin/data/jardin.db "SELECT count(*) FROM lunarcalendarentry;"
# Doit être > 0 après restart
```
**Step 6: Commit**
```bash
git commit -m "feat(lunar): persister les données calculées dans lunarcalendarentry"
```
---
## Task 6: Grille jardin — Sélection de case pour ajouter une plantation
**Priorité:** Haute (demandé explicitement : "je peux sélectionner des cases de la grille jardin pour ajouter une plantation")
**Files:**
- Modify: `frontend/src/views/JardinDetailView.vue`
- Read first: `frontend/src/api/plants.ts` et `frontend/src/api/gardens.ts` pour les types
**Ce que ça fait:**
- Clic sur une case libre → modal s'ouvre avec formulaire de création de plantation
- Le formulaire propose: plante (liste déroulante), date plantation, notes
- POST vers /api/plantings avec `garden_id`, `plant_id`, `cell_row`, `cell_col`
- Après succès: la case passe en "occupé" (orange)
**Step 1: Lire les fichiers API nécessaires**
```bash
cat /home/gilles/Documents/vscode/jardin/frontend/src/api/plants.ts | head -40
cat /home/gilles/Documents/vscode/jardin/frontend/src/api/gardens.ts | head -40
```
**Step 2: Ajouter le modal de plantation dans JardinDetailView.vue**
Ajouter dans le `` après la grille :
```vue
Planter en case {{ selectedCell.libelle }}
```
**Step 3: Modifier les cases de la grille pour être cliquables**
Sur chaque case `
([])
const selectedCell = ref(null)
const savingPlantation = ref(false)
const form = ref({ plant_id: null as number | null, date_plantation: '', notes: '' })
function openCellModal(cell: GardenCell) {
selectedCell.value = cell
form.value = { plant_id: null, date_plantation: new Date().toISOString().slice(0, 10), notes: '' }
}
async function savePlantation() {
if (!selectedCell.value || !form.value.plant_id || !garden.value) return
savingPlantation.value = true
try {
await plantingsApi.create({
garden_id: garden.value.id!,
plant_id: form.value.plant_id,
cell_row: selectedCell.value.row,
cell_col: selectedCell.value.col,
date_plantation: form.value.date_plantation || undefined,
notes: form.value.notes || undefined,
})
// Rafraîchir les cellules
cells.value = await gardensApi.cells(garden.value.id!)
selectedCell.value = null
} catch (e) {
alert('Erreur lors de la création de la plantation.')
} finally {
savingPlantation.value = false
}
}
// Dans onMounted(), ajouter:
// plants.value = await plantsApi.getAll()
```
**Step 5: Vérifier l'API plantings**
```bash
cat /home/gilles/Documents/vscode/jardin/frontend/src/api/plantings.ts 2>/dev/null | head -40
```
Si `plantingsApi.create` n'existe pas, l'adapter selon l'API existante.
**Step 6: Rebuild et tester**
```bash
docker compose build frontend && docker compose up -d frontend
```
Sur http://10.0.1.109:8061/jardins/2, cliquer une case → vérifier que le modal s'ouvre.
**Step 7: Commit**
```bash
git add frontend/src/views/JardinDetailView.vue
git commit -m "feat(grille): sélection de case pour créer une plantation (modal inline)"
```
---
## Task 7: F3 — Astuce du jour sur le Dashboard
**Priorité:** Moyenne
**Files:**
- Modify: `frontend/src/views/DashboardView.vue`
- Read first: `frontend/src/api/astuces.ts` ou `frontend/src/stores/astuces.ts`
**Ce que ça fait:**
- Afficher une "Astuce du jour" dans le Dashboard
- Sélection par `jour_annee = dayOfYear(today) % nb_astuces` (déterministe, change chaque jour)
**Step 1: Lire l'API astuces**
```bash
cat /home/gilles/Documents/vscode/jardin/frontend/src/api/astuces.ts 2>/dev/null || ls frontend/src/api/
```
**Step 2: Ajouter la section "Astuce du jour" dans DashboardView.vue**
Dans le ``, ajouter une section après "Tâches à faire" :
```vue
Astuce du jour
💡
{{ astuceJour.titre }}
{{ astuceJour.contenu }}
— {{ astuceJour.source }}
```
**Step 3: Script — logique astuce du jour**
```typescript
import { astucesApi } from '@/api/astuces' // adapter selon le nom exact
const astuceJour = ref<{titre: string; contenu: string; source?: string} | null>(null)
function dayOfYear(): number {
const now = new Date()
const start = new Date(now.getFullYear(), 0, 0)
return Math.floor((now.getTime() - start.getTime()) / 86400000)
}
// Dans onMounted():
try {
const all = await astucesApi.getAll()
if (all.length) astuceJour.value = all[dayOfYear() % all.length]
} catch { astuceJour.value = null }
```
**Step 4: Commit**
```bash
git add frontend/src/views/DashboardView.vue
git commit -m "feat(dashboard): astuce du jour (rotation quotidienne déterministe)"
```
---
## Task 8: F2 — Améliorer l'affichage des saints du jour dans CalendrierView
**Priorité:** Moyenne
**Contexte:** La table `saint_du_jour` est maintenant remplie (366 jours). L'afficher dans CalendrierView.
**Files:**
- Modify: `frontend/src/views/CalendrierView.vue`
- Create: `frontend/src/api/saints.ts`
**Step 1: Créer l'API saints**
```typescript
// frontend/src/api/saints.ts
import client from './client'
export interface SaintJour {
mois: number
jour: number
saints: string[]
source_url?: string
}
export const saintsApi = {
getJour: (mois: number, jour: number) =>
client.get('/api/saints/jour', { params: { mois, jour } }).then(r => r.data),
}
```
**Step 2: Dans CalendrierView.vue — afficher le saint du jour**
Repérer où le jour courant est affiché (dans le panneau de détail). Ajouter :
```vue
```
**Step 3: Charger le saint pour le jour sélectionné**
```typescript
import { saintsApi, type SaintJour } from '@/api/saints'
const saintJour = ref(null)
// Quand le jour change (watch selectedDate ou onMounted):
watch(selectedDate, async (d) => {
if (!d) return
const date = new Date(d)
try {
saintJour.value = await saintsApi.getJour(date.getMonth() + 1, date.getDate())
} catch { saintJour.value = null }
})
```
**Step 4: Commit**
```bash
git add frontend/src/api/saints.ts frontend/src/views/CalendrierView.vue
git commit -m "feat(calendrier): afficher le saint du jour via /api/saints/jour"
```
---
## Task 9: B2 — Nettoyage fichiers upload test
**Priorité:** Haute (disque à 80%)
**Ce que ça fait:** Identifier les fichiers dans `/data/uploads/` qui ne sont pas référencés dans la table `media` et les supprimer.
**Step 1: Lister les fichiers non référencés**
```bash
sqlite3 /home/gilles/Documents/vscode/jardin/data/jardin.db "SELECT url FROM media;" > /tmp/media_urls.txt
ls /home/gilles/Documents/vscode/jardin/data/uploads/ 2>/dev/null | head -20
```
**Step 2: Comparer et lister les orphelins**
Script Python pour identifier les fichiers non référencés :
```python
import sqlite3, os
from pathlib import Path
db = Path('/home/gilles/Documents/vscode/jardin/data/jardin.db')
uploads = Path('/home/gilles/Documents/vscode/jardin/data/uploads')
conn = sqlite3.connect(str(db))
referenced = set()
for (url,) in conn.execute("SELECT url FROM media WHERE url IS NOT NULL"):
fname = url.split('/')[-1] if url else ''
referenced.add(fname)
for (url,) in conn.execute("SELECT thumbnail_url FROM media WHERE thumbnail_url IS NOT NULL"):
fname = url.split('/')[-1] if url else ''
referenced.add(fname)
conn.close()
orphans = []
total_size = 0
for f in uploads.rglob('*'):
if f.is_file() and f.name not in referenced:
orphans.append(f)
total_size += f.stat().st_size
print(f"{len(orphans)} fichiers orphelins, {total_size/1024/1024:.1f} MB")
for f in orphans[:20]:
print(f" {f}")
```
**Step 3: Supprimer les orphelins (après confirmation manuelle)**
```bash
# Lancer le script d'abord en dry-run, puis supprimer
# NE PAS automatiser la suppression sans vérification manuelle
```
**Step 4: Commit (message uniquement)**
```bash
git commit --allow-empty -m "chore: nettoyage manuel fichiers uploads non référencés"
```
---
## Task 10: B5 — Export JSON
**Priorité:** Moyenne (plan 10.2)
**Files:**
- Modify: `backend/app/routers/settings.py` (ajouter endpoint export)
**Step 1: Ajouter endpoint GET /api/export**
Dans `backend/app/routers/settings.py`, ajouter :
```python
from fastapi.responses import JSONResponse
from app.models import Garden, Plant, Planting, Task, Tool, Astuce, Dicton
from sqlmodel import select
@router.get("/export")
def export_all_data(session: Session = Depends(get_session)):
"""Export complet de toutes les données en JSON."""
def rows(model):
return [r.model_dump() for r in session.exec(select(model)).all()]
return JSONResponse(content={
"version": "1.0",
"exported_at": datetime.now(timezone.utc).isoformat(),
"gardens": rows(Garden),
"plants": rows(Plant),
"plantings": rows(Planting),
"tasks": rows(Task),
"tools": rows(Tool),
"astuces": rows(Astuce),
"dictons": rows(Dicton),
})
```
**Step 2: Ajouter bouton dans ReglagesView.vue**
Dans la section "Sauvegarde des données", ajouter un bouton "Exporter JSON":
```vue
```
```typescript
async function exportJson() {
const r = await fetch('/api/export')
const blob = await r.blob()
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `jardin_export_${new Date().toISOString().slice(0, 10)}.json`
a.click()
URL.revokeObjectURL(url)
}
```
**Step 3: Rebuild et tester**
```bash
docker compose build backend && docker compose up -d backend
```
`curl http://localhost:8060/api/export | python3 -m json.tool | head -20`
**Step 4: Commit**
```bash
git commit -m "feat(backend): endpoint GET /api/export JSON complet + bouton frontend"
```
---
## Task 11: B6 — Section Observations dans la fiche plantation
**Priorité:** Moyenne (plan 5.2 frontend)
**Contexte:** Le backend `Observation` existe avec router `/api/observations`. À afficher dans PlantationsView.
**Files:**
- Read first: `backend/app/routers/observations.py` pour comprendre l'API
- Modify: `frontend/src/views/PlantationsView.vue`
- Create: `frontend/src/api/observations.ts`
**Step 1: Lire le router observations**
```bash
cat /home/gilles/Documents/vscode/jardin/backend/app/routers/observations.py
```
**Step 2: Créer frontend/src/api/observations.ts**
```typescript
import client from './client'
export interface Observation {
id?: number
planting_id?: number
garden_id?: number
type: 'maladie' | 'ravageur' | 'traitement' | 'note'
titre: string
description?: string
date: string
photo_url?: string
}
export const observationsApi = {
getByPlanting: (plantingId: number) =>
client.get('/api/observations', { params: { planting_id: plantingId } }).then(r => r.data),
create: (obs: Omit) =>
client.post('/api/observations', obs).then(r => r.data),
delete: (id: number) => client.delete(`/api/observations/${id}`).then(r => r.data),
}
```
**Step 3: Ajouter section Observations dans PlantationsView.vue**
Dans la fiche dépliable de chaque plantation, ajouter un sous-section observations. S'inspirer du pattern récoltes déjà implémenté.
**Step 4: Commit**
```bash
git commit -m "feat(plantations): section observations (maladies/ravageurs/traitements)"
```
---
## Task 12: B7 — Composant MediaGallery.vue réutilisable
**Priorité:** Moyenne (plan 2.2 frontend)
**Files:**
- Create: `frontend/src/components/MediaGallery.vue`
- Modify: `frontend/src/views/JardinDetailView.vue` (utiliser le composant)
- Read first: `frontend/src/api/media.ts` ou similaire
**Step 1: Créer MediaGallery.vue**
```vue
Aucune photo.
{{ lightbox.titre }}
```
**Step 2: Intégrer dans JardinDetailView.vue**
```vue
Photos
```
**Step 3: Commit**
```bash
git commit -m "feat(frontend): composant MediaGallery.vue réutilisable avec lightbox + upload"
```
---
## Task 13: Dashboard — Dicton du jour + amélioration
**Priorité:** Moyenne (liée aux items dictons)
**Files:**
- Modify: `frontend/src/views/DashboardView.vue`
**Ce que ça fait:** Afficher le dicton du jour correspondant à la date courante.
**Step 1: Ajouter la logique dicton dans DashboardView.vue**
```typescript
import { dictonsApi } from '@/api/dictons' // vérifier le nom exact
const dictonJour = ref<{texte: string} | null>(null)
// Dans onMounted():
try {
const today = new Date()
const mois = today.getMonth() + 1
const jour = today.getDate()
const dictons = await dictonsApi.getByMois(mois)
// Chercher d'abord le dicton du jour exact, sinon aléatoire du mois
const exact = dictons.find((d: any) => d.jour === jour)
dictonJour.value = exact || (dictons.length ? dictons[Math.floor(Math.random() * dictons.length)] : null)
} catch { dictonJour.value = null }
```
**Step 2: Afficher dans le template**
```vue
Dicton du jour
« {{ dictonJour.texte }} »
```
**Step 3: Commit**
```bash
git add frontend/src/views/DashboardView.vue
git commit -m "feat(dashboard): dicton du jour depuis la BDD (jour exact ou du mois)"
```
---
## Task 14: Renommer /meteo en /calendrier dans le header
**Priorité:** Basse (item 17 — route calendrier)
**Contexte:** La route `/calendrier` redirige vers `/meteo`. Mais dans le header/drawer, le lien est labelled "Météo". L'utilisateur veut "Calendrier" dans la nav.
**Files:**
- Modify: `frontend/src/App.vue` (sidebar link)
- Modify: `frontend/src/components/AppDrawer.vue`
**Step 1: Dans App.vue, renommer le lien**
```typescript
// Changer:
{ to: '/meteo', label: 'Météo', icon: '🌦️' },
// En:
{ to: '/meteo', label: 'Calendrier', icon: '📅' },
```
**Step 2: Dans AppDrawer.vue, renommer**
```typescript
// Changer:
{ to: '/meteo', label: 'Météo' },
// En:
{ to: '/meteo', label: 'Calendrier' },
```
**Step 3: Commit**
```bash
git commit -m "chore(nav): renommer 'Météo' → 'Calendrier' dans la navigation"
```
---
## Task 15: Amélioration CalendrierView — Titre + route
**Priorité:** Basse (item 19-20)
**Contexte:** Le titre de CalendrierView.vue est "🌦️ Météo". Renommer en "📅 Calendrier". Ajouter `/calendrier` comme route principale (et garder `/meteo` en redirect).
**Files:**
- Modify: `frontend/src/views/CalendrierView.vue` (h1)
- Modify: `frontend/src/router/index.ts` (route principale /calendrier)
**Step 1: Modifier le titre dans CalendrierView.vue**
Changer `