1117 lines
36 KiB
Markdown
1117 lines
36 KiB
Markdown
# 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
|
||
<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 :
|
||
|
||
```typescript
|
||
// --- 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 = ...` :
|
||
```typescript
|
||
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>)`:
|
||
|
||
```typescript
|
||
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 :
|
||
|
||
```vue
|
||
<!-- 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**
|
||
|
||
```bash
|
||
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**
|
||
|
||
```bash
|
||
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 :
|
||
```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-3xl` → `max-w-5xl`)
|
||
- `ReglagesView.vue` ligne 2 (`max-w-3xl` → `max-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:
|
||
|
||
```vue
|
||
<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**
|
||
|
||
```bash
|
||
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**
|
||
|
||
```bash
|
||
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**
|
||
|
||
```bash
|
||
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**
|
||
|
||
```bash
|
||
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 :
|
||
|
||
```python
|
||
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**
|
||
|
||
```bash
|
||
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**
|
||
|
||
```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` :
|
||
```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 `<template>` après la grille :
|
||
|
||
```vue
|
||
<!-- 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**
|
||
|
||
```typescript
|
||
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**
|
||
|
||
```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 `<template>`, ajouter une section après "Tâches à faire" :
|
||
|
||
```vue
|
||
<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**
|
||
|
||
```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<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 :
|
||
|
||
```vue
|
||
<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é**
|
||
|
||
```typescript
|
||
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**
|
||
|
||
```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
|
||
<button @click="exportJson" class="bg-yellow text-bg px-4 py-2 rounded-lg text-sm font-semibold hover:opacity-90">
|
||
Exporter JSON
|
||
</button>
|
||
```
|
||
|
||
```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<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**
|
||
|
||
```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
|
||
<!-- 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**
|
||
|
||
```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**
|
||
|
||
```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
|
||
<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**
|
||
|
||
```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 `<h1>🌦️ Météo</h1>` (ou similaire) en `<h1>📅 Calendrier</h1>`.
|
||
|
||
**Step 2: Modifier le router**
|
||
|
||
```typescript
|
||
// 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**
|
||
|
||
```bash
|
||
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
|