Files
jardin/docs/plans/2026-02-22-ameliorations-sprint.md
2026-02-22 22:18:32 +01:00

36 KiB
Raw Permalink Blame History

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 (1220px, défaut 14)
  • ui_menu_font_size : taille texte menu latéral (1118px, défaut 13)
  • ui_menu_icon_size : taille icônes menu (1428px, défaut 18)
  • ui_thumb_size : taille miniatures images/vidéos (60200px, défaut 96)

Step 1: Modifier ReglagesView.vue — ajouter section Interface

Ajouter une nouvelle section avant la section "Général" dans ReglagesView.vue :

<section class="bg-bg-soft border border-bg-hard rounded-xl p-4 mb-4">
  <h2 class="text-text font-semibold mb-2">Interface</h2>
  <p class="text-text-muted text-sm mb-4">Ajustez les tailles d'affichage. Les changements sont appliqués instantanément.</p>

  <div class="grid grid-cols-1 gap-4">
    <div v-for="s in uiSizeSettings" :key="s.key" class="flex items-center gap-3">
      <label class="text-sm text-text w-44 shrink-0">{{ s.label }}</label>
      <input
        type="range"
        :min="s.min" :max="s.max" :step="s.step"
        v-model.number="uiSizes[s.key]"
        class="flex-1 accent-green"
        @input="applyUiSizes"
      />
      <span class="text-text-muted text-xs w-12 text-right">{{ uiSizes[s.key] }}{{ s.unit }}</span>
    </div>
  </div>

  <div class="mt-4 flex items-center gap-2">
    <button
      class="bg-green text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90"
      :disabled="savingUi"
      @click="saveUiSettings"
    >{{ savingUi ? 'Enregistrement...' : 'Enregistrer' }}</button>
    <button
      class="text-text-muted text-xs hover:text-text px-2"
      @click="resetUiSettings"
    >Réinitialiser</button>
    <span v-if="uiSavedMsg" class="text-xs text-aqua">{{ uiSavedMsg }}</span>
  </div>
</section>

Dans <script setup lang="ts">, ajouter :

// --- UI Size settings ---
const UI_DEFAULTS = { ui_font_size: 14, ui_menu_font_size: 13, ui_menu_icon_size: 18, ui_thumb_size: 96 }

const uiSizeSettings = [
  { key: 'ui_font_size',      label: 'Taille texte',           min: 12, max: 20, step: 1, unit: 'px' },
  { key: 'ui_menu_font_size', label: 'Texte menu latéral',     min: 11, max: 18, step: 1, unit: 'px' },
  { key: 'ui_menu_icon_size', label: 'Icônes menu',            min: 14, max: 28, step: 1, unit: 'px' },
  { key: 'ui_thumb_size',     label: 'Miniatures images/vidéo', min: 60, max: 200, step: 4, unit: 'px' },
]

const uiSizes = ref<Record<string, number>>({ ...UI_DEFAULTS })
const savingUi = ref(false)
const uiSavedMsg = ref('')

function applyUiSizes() {
  const root = document.documentElement
  root.style.setProperty('--ui-font-size', `${uiSizes.value.ui_font_size}px`)
  root.style.setProperty('--ui-menu-font-size', `${uiSizes.value.ui_menu_font_size}px`)
  root.style.setProperty('--ui-menu-icon-size', `${uiSizes.value.ui_menu_icon_size}px`)
  root.style.setProperty('--ui-thumb-size', `${uiSizes.value.ui_thumb_size}px`)
  window.dispatchEvent(new CustomEvent('ui-sizes-updated', { detail: { ...uiSizes.value } }))
}

async function saveUiSettings() {
  savingUi.value = true
  uiSavedMsg.value = ''
  try {
    const payload: Record<string, string> = {}
    for (const [k, v] of Object.entries(uiSizes.value)) payload[k] = String(v)
    await settingsApi.update(payload)
    applyUiSizes()
    uiSavedMsg.value = 'Enregistré'
    setTimeout(() => { uiSavedMsg.value = '' }, 1800)
  } finally {
    savingUi.value = false
  }
}

function resetUiSettings() {
  uiSizes.value = { ...UI_DEFAULTS }
  applyUiSizes()
}

Dans loadSettings(), ajouter après debugMode.value = ... :

for (const s of uiSizeSettings) {
  const v = data[s.key]
  if (v != null) uiSizes.value[s.key] = Number(v) || UI_DEFAULTS[s.key as keyof typeof UI_DEFAULTS]
}
applyUiSizes()

Step 2: App.vue — appliquer les CSS vars au chargement

Dans App.vue, modifier loadDebugModeFromApi() pour aussi charger et appliquer les tailles UI. Ajouter une fonction applyUiSizesFromSettings(data: Record<string, string>):

function applyUiSizesFromSettings(data: Record<string, string>) {
  const defaults = { ui_font_size: 14, ui_menu_font_size: 13, ui_menu_icon_size: 18, ui_thumb_size: 96 }
  const root = document.documentElement
  for (const [key, def] of Object.entries(defaults)) {
    const val = Number(data[key]) || def
    const prop = '--' + key.replace(/_/g, '-')
    root.style.setProperty(prop, `${val}px`)
  }
}

Appeler dans loadDebugModeFromApi() après avoir récupéré data.

Step 3: Utiliser les CSS vars dans App.vue sidebar

Modifier les classes du sidebar pour utiliser les vars via style inline :

<!-- Icône nav: -->
<span :style="`font-size: var(--ui-menu-icon-size, 18px)`" class="leading-none">{{ l.icon }}</span>
<!-- Label nav: -->
<span :style="`font-size: var(--ui-menu-font-size, 13px)`">{{ l.label }}</span>

Dans le <main> racine, ajouter style="font-size: var(--ui-font-size, 14px)".

Step 4: Appliquer thumb_size aux images existantes

Dans les vues qui affichent des thumbnails (MediaGallery, BibliothequeView, etc.), utiliser style="width: var(--ui-thumb-size, 96px); height: var(--ui-thumb-size, 96px)" pour les conteneurs d'images.

Step 5: Rebuild frontend et vérifier

cd /home/gilles/Documents/vscode/jardin
docker compose build frontend && docker compose up -d frontend

Aller sur http://10.0.1.109:8061/reglages → vérifier que les 4 sliders sont présents → déplacer un slider → texte change instantanément.

Step 6: Commit

git add frontend/src/views/ReglagesView.vue frontend/src/App.vue
git commit -m "feat(settings): sliders taille texte, menu, icônes, miniatures + CSS vars"

Task 2: Layout — Optimisation largeur desktop + responsive mobile

Priorité: Haute (demandé explicitement)

Problème actuel: Les vues ont max-w-3xl (48rem) codé en dur, ce qui est trop étroit sur laptop (1366px+). Le mobile a des overflow sur certaines vues.

Files:

  • Modify: frontend/src/App.vue (main layout)
  • Modify: toutes les vues: DashboardView.vue, JardinsView.vue, JardinDetailView.vue, PlantesView.vue, PlantationsView.vue, TachesView.vue, PlanningView.vue, ReglagesView.vue, OutilsView.vue, AstucesView.vue

Step 1: App.vue — augmenter le max-w du content

Dans <main>, changer la classe pour permettre plus de largeur sur desktop. Actuellement pas de max-w sur main, mais les vues individuelles ont max-w-3xl.

Modifier <main> dans App.vue :

<main class="pt-14 lg:pt-0 lg:pl-60 min-h-screen w-full bg-bg">
  <div class="mx-auto px-2 sm:px-4 max-w-[1400px]">
    <RouterView />
  </div>
</main>

Step 2: Standardiser les vues — remplacer max-w-3xl par max-w-5xl

Dans chaque vue, remplacer class="p-4 max-w-3xl mx-auto" par class="p-4 max-w-5xl mx-auto".

Vues concernées:

  • DashboardView.vue ligne 2
  • JardinsView.vue
  • JardinDetailView.vue ligne 2
  • PlantesView.vue
  • PlantationsView.vue
  • TachesView.vue
  • PlanningView.vue ligne 2 (max-w-3xlmax-w-5xl)
  • ReglagesView.vue ligne 2 (max-w-3xlmax-w-5xl)
  • OutilsView.vue
  • AstucesView.vue
  • CalendrierView.vue (si max-w-3xl présent)

Step 3: Vérifier mobile — s'assurer que les tables/grilles ont overflow-x-auto

Dans JardinDetailView.vue, la grille a déjà overflow-x-auto. Vérifier les autres vues.

Dans PlanningView.vue, le calendrier grid grid-cols-7 est OK.

Step 4: Dashboard — layout 2 colonnes sur desktop

Dans DashboardView.vue, envelopper les sections Tâches+Jardins dans une colonne et Météo dans l'autre:

<div class="lg:grid lg:grid-cols-3 lg:gap-6">
  <div class="lg:col-span-2">
    <!-- section tâches -->
    <!-- section jardins -->
  </div>
  <div>
    <!-- section météo -->
  </div>
</div>

Step 5: Rebuild et vérifier

docker compose build frontend && docker compose up -d frontend

Sur laptop: vérifier que le contenu utilise plus de largeur. Sur mobile: vérifier que pas d'overflow horizontal.

Step 6: Commit

git add frontend/src/App.vue frontend/src/views/DashboardView.vue frontend/src/views/JardinsView.vue frontend/src/views/JardinDetailView.vue frontend/src/views/PlantesView.vue frontend/src/views/PlantationsView.vue frontend/src/views/TachesView.vue frontend/src/views/PlanningView.vue frontend/src/views/ReglagesView.vue frontend/src/views/OutilsView.vue frontend/src/views/AstucesView.vue
git commit -m "feat(layout): max-w-5xl + 2 colonnes dashboard desktop + overflow mobile"

Task 3: B1 — Nettoyer données orphelines BDD (priorité haute)

Contexte: Garden_id=1 n'existe plus. 24 garden_cells, 1 planting, 1 measurement sont orphelins.

Files:

  • Modify: backend/app/migrate.py (ajouter étape de nettoyage)

Step 1: Vérifier l'état en BDD

sqlite3 /home/gilles/Documents/vscode/jardin/data/jardin.db "
SELECT 'garden_cells orphelins' as t, count(*) FROM garden_cell WHERE garden_id NOT IN (SELECT id FROM garden);
SELECT 'plantings orphelins' as t, count(*) FROM planting WHERE garden_id NOT IN (SELECT id FROM garden);
SELECT 'measurements orphelins' as t, count(*) FROM measurement WHERE garden_id NOT IN (SELECT id FROM garden);
"

Step 2: Nettoyage SQL direct

sqlite3 /home/gilles/Documents/vscode/jardin/data/jardin.db "
DELETE FROM garden_cell WHERE garden_id NOT IN (SELECT id FROM garden);
DELETE FROM planting WHERE garden_id NOT IN (SELECT id FROM garden);
DELETE FROM measurement WHERE garden_id NOT IN (SELECT id FROM garden);
SELECT changes() || ' lignes supprimées';
"

Step 3: Ajouter le nettoyage dans migrate.py

Dans backend/app/migrate.py, ajouter une fonction cleanup_orphans(conn) appelée à la fin de la migration :

def cleanup_orphans(conn):
    """Supprime les enregistrements orphelins (FK rompues après suppression d'entités parentes)."""
    cursor = conn.cursor()
    orphan_queries = [
        ("garden_cell", "garden_id", "garden"),
        ("planting",    "garden_id", "garden"),
        ("measurement", "garden_id", "garden"),
    ]
    for table, fk_col, parent_table in orphan_queries:
        try:
            cursor.execute(f"DELETE FROM {table} WHERE {fk_col} NOT IN (SELECT id FROM {parent_table})")
            if cursor.rowcount:
                print(f"  [cleanup] {cursor.rowcount} orphans supprimés de {table}")
        except Exception as e:
            print(f"  [cleanup] Erreur sur {table}: {e}")
    conn.commit()

Step 4: Commit

git add backend/app/migrate.py
git commit -m "fix(db): nettoyage orphelins garden_cells/plantings/measurements + migrate.py"

Task 4: B4 — Page 404 frontend

Priorité: Haute

Files:

  • Create: frontend/src/views/NotFoundView.vue
  • Modify: frontend/src/router/index.ts

Step 1: Créer NotFoundView.vue

<template>
  <div class="flex flex-col items-center justify-center min-h-[60vh] p-8 text-center">
    <div class="text-8xl mb-6">🌿</div>
    <h1 class="text-4xl font-bold text-green mb-2">404</h1>
    <p class="text-text-muted text-lg mb-6">Cette page n'existe pas dans le jardin.</p>
    <RouterLink
      to="/"
      class="bg-green text-bg px-6 py-3 rounded-lg font-semibold hover:opacity-90 transition-opacity"
    >
      Retour au tableau de bord
    </RouterLink>
  </div>
</template>

<script setup lang="ts">
import { RouterLink } from 'vue-router'
</script>

Step 2: Ajouter route catch-all dans router/index.ts

Ajouter en dernière position dans le tableau routes :

{ path: '/:pathMatch(.*)*', component: () => import('@/views/NotFoundView.vue') },

Step 3: Rebuild et tester

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

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

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

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 :

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

sqlite3 /home/gilles/Documents/vscode/jardin/data/jardin.db "SELECT count(*) FROM lunarcalendarentry;"
# Doit être > 0 après restart

Step 6: Commit

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

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 <template> après la grille :

<!-- Modal ajout plantation sur case -->
<Teleport to="body">
  <div v-if="selectedCell" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50" @click.self="selectedCell = null">
    <div class="bg-bg-hard border border-bg-soft rounded-xl p-6 w-full max-w-sm mx-4">
      <h3 class="text-text font-semibold mb-4">
        Planter en case {{ selectedCell.libelle }}
      </h3>

      <div class="space-y-3">
        <div>
          <label class="text-text-muted text-xs mb-1 block">Plante *</label>
          <select v-model="form.plant_id" class="w-full bg-bg border border-bg-soft rounded px-3 py-2 text-text text-sm">
            <option :value="null" disabled>-- Choisir une plante --</option>
            <option v-for="p in plants" :key="p.id" :value="p.id">{{ p.nom }}</option>
          </select>
        </div>

        <div>
          <label class="text-text-muted text-xs mb-1 block">Date de plantation</label>
          <input type="date" v-model="form.date_plantation"
            class="w-full bg-bg border border-bg-soft rounded px-3 py-2 text-text text-sm" />
        </div>

        <div>
          <label class="text-text-muted text-xs mb-1 block">Notes</label>
          <input type="text" v-model="form.notes" placeholder="Optionnel"
            class="w-full bg-bg border border-bg-soft rounded px-3 py-2 text-text text-sm" />
        </div>
      </div>

      <div class="flex gap-2 mt-5">
        <button
          class="flex-1 bg-green text-bg py-2 rounded-lg text-sm font-semibold hover:opacity-90 disabled:opacity-50"
          :disabled="!form.plant_id || savingPlantation"
          @click="savePlantation"
        >{{ savingPlantation ? 'Enregistrement...' : 'Planter' }}</button>
        <button class="px-4 py-2 text-text-muted text-sm hover:text-text" @click="selectedCell = null">Annuler</button>
      </div>
    </div>
  </div>
</Teleport>

Step 3: Modifier les cases de la grille pour être cliquables

Sur chaque case <div v-for="cell in displayCells", ajouter @click="openCellModal(cell)".

Changer la classe pour mieux indiquer la cliquabilité.

Step 4: Script setup — ajouter la logique

import { plantsApi, type Plant } from '@/api/plants'
import { plantingsApi } from '@/api/plantings'

const plants = ref<Plant[]>([])
const selectedCell = ref<GardenCell | null>(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

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

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

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

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 <template>, ajouter une section après "Tâches à faire" :

<section class="mb-6" v-if="astuceJour">
  <h2 class="text-text-muted text-xs uppercase tracking-widest mb-3">Astuce du jour</h2>
  <div class="bg-bg-soft rounded-xl p-4 border border-bg-hard flex gap-3 items-start">
    <span class="text-2xl">💡</span>
    <div>
      <div class="text-text text-sm font-medium mb-1">{{ astuceJour.titre }}</div>
      <div class="text-text-muted text-xs leading-relaxed">{{ astuceJour.contenu }}</div>
      <div v-if="astuceJour.source" class="text-text-muted text-xs mt-1 opacity-60"> {{ astuceJour.source }}</div>
    </div>
  </div>
</section>

Step 3: Script — logique astuce du jour

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

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

// 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<SaintJour>('/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 :

<div v-if="saintJour?.saints?.length" class="text-text-muted text-xs mt-2">
  <span class="text-yellow mr-1"></span>{{ saintJour.saints[0] }}
  <span v-if="saintJour.saints.length > 1" class="opacity-60"> (+{{ saintJour.saints.length - 1 }})</span>
</div>

Step 3: Charger le saint pour le jour sélectionné

import { saintsApi, type SaintJour } from '@/api/saints'
const saintJour = ref<SaintJour | null>(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

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

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 :

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)

# Lancer le script d'abord en dry-run, puis supprimer
# NE PAS automatiser la suppression sans vérification manuelle

Step 4: Commit (message uniquement)

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 :

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":

<button @click="exportJson" class="bg-yellow text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90">
  Exporter JSON
</button>
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

docker compose build backend && docker compose up -d backend

curl http://localhost:8060/api/export | python3 -m json.tool | head -20

Step 4: Commit

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

cat /home/gilles/Documents/vscode/jardin/backend/app/routers/observations.py

Step 2: Créer frontend/src/api/observations.ts

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<Observation[]>('/api/observations', { params: { planting_id: plantingId } }).then(r => r.data),
  create: (obs: Omit<Observation, 'id'>) =>
    client.post<Observation>('/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

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

<!-- Props: entityType: string, entityId: number -->
<template>
  <div>
    <div v-if="medias.length" class="grid gap-2"
      :style="`grid-template-columns: repeat(auto-fill, minmax(var(--ui-thumb-size, 96px), 1fr))`">
      <div v-for="m in medias" :key="m.id" class="relative group">
        <img
          :src="m.thumbnail_url || m.url"
          :alt="m.titre || 'Photo'"
          class="rounded-lg object-cover border border-bg-hard cursor-pointer hover:border-green transition-colors"
          :style="`width: var(--ui-thumb-size, 96px); height: var(--ui-thumb-size, 96px)`"
          @click="lightbox = m"
        />
        <button
          class="absolute top-1 right-1 bg-bg-hard/80 text-red text-xs rounded px-1 opacity-0 group-hover:opacity-100 transition-opacity"
          @click.stop="deleteMedia(m.id!)"
        ></button>
      </div>
    </div>
    <div v-else class="text-text-muted text-xs py-2">Aucune photo.</div>

    <!-- Upload -->
    <label class="mt-2 inline-flex items-center gap-1 text-xs text-blue hover:text-text cursor-pointer">
      <input type="file" accept="image/*,video/*" class="hidden" @change="uploadFile" multiple />
      + Ajouter des photos
    </label>

    <!-- Lightbox -->
    <Teleport to="body">
      <div v-if="lightbox" class="fixed inset-0 z-50 bg-black/85 flex items-center justify-center" @click.self="lightbox = null">
        <div class="max-w-4xl max-h-screen p-4">
          <img :src="lightbox.url" :alt="lightbox.titre || 'Photo'" class="max-w-full max-h-[80vh] rounded-lg" />
          <div class="text-center text-text-muted text-sm mt-2">{{ lightbox.titre }}</div>
          <button class="absolute top-4 right-4 text-text text-2xl hover:text-red" @click="lightbox = null"></button>
        </div>
      </div>
    </Teleport>
  </div>
</template>

Step 2: Intégrer dans JardinDetailView.vue

<section class="mt-6">
  <h2 class="text-text-muted text-xs uppercase tracking-widest mb-3">Photos</h2>
  <MediaGallery entity-type="jardin" :entity-id="garden.id!" />
</section>

Step 3: Commit

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

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

<section class="mb-6" v-if="dictonJour">
  <h2 class="text-text-muted text-xs uppercase tracking-widest mb-3">Dicton du jour</h2>
  <blockquote class="bg-bg-soft rounded-xl p-4 border border-bg-hard border-l-4 border-l-yellow">
    <p class="text-text text-sm italic">« {{ dictonJour.texte }} »</p>
  </blockquote>
</section>

Step 3: Commit

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

// Changer:
{ to: '/meteo', label: 'Météo', icon: '🌦️' },
// En:
{ to: '/meteo', label: 'Calendrier', icon: '📅' },

Step 2: Dans AppDrawer.vue, renommer

// Changer:
{ to: '/meteo', label: 'Météo' },
// En:
{ to: '/meteo', label: 'Calendrier' },

Step 3: Commit

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 <h1>🌦️ Météo</h1> (ou similaire) en <h1>📅 Calendrier</h1>.

Step 2: Modifier le router

// Remplacer:
{ path: '/meteo', component: () => import('@/views/CalendrierView.vue') },
{ path: '/calendrier', redirect: '/meteo' },
// Par:
{ path: '/calendrier', component: () => import('@/views/CalendrierView.vue') },
{ path: '/meteo', redirect: '/calendrier' },

Mettre à jour les liens dans App.vue et AppDrawer.vue : to="/calendrier".

Step 3: Commit

git commit -m "refactor(nav): /calendrier route principale, /meteo redirect + titre CalendrierView"

Ordre d'exécution recommandé

  1. Task 1 (Settings sliders) ← demandé explicitement, impact immédiat
  2. Task 2 (Layout) ← demandé explicitement
  3. Task 4 (404 page) ← simple, haute priorité
  4. Task 6 (Grille jardin + plantation) ← demandé explicitement
  5. Task 3 (Nettoyage orphelins) ← haute priorité, rapide
  6. Task 9 (Nettoyage uploads) ← haute priorité, disque
  7. Task 5 (Lunar persist) ← haute priorité
  8. Task 7 (Astuce du jour) ← moyen, rapide
  9. Task 13 (Dicton du jour) ← moyen, rapide
  10. Task 8 (Saint du jour) ← moyen, rapide
  11. Task 14 (Renommer nav) ← basse, 5 min
  12. Task 15 (Route calendrier) ← basse
  13. Task 10 (Export JSON) ← moyen
  14. Task 11 (Observations) ← moyen
  15. Task 12 (MediaGallery) ← moyen