diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..40b2698 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +Dockerfile +.dockerignore +node_modules +npm-debug.log +README.md +.env +.env.local +.env.production.local +.env.development.local +.git +.gitignore +.next +.vscode +.idea +*.swp +*.swo +.DS_Store +coverage +.nyc_output +*.log +dist +build +.cache +.turbo + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9d9a292 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,80 @@ +# Multi-stage Dockerfile for Next.js with Prisma +FROM node:20-alpine AS base + +# Install dependencies only when needed +FROM base AS deps +# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# Install dependencies based on the preferred package manager +COPY package.json package-lock.json* ./ +RUN \ + if [ -f package-lock.json ]; then npm ci; \ + else echo "Lockfile not found." && exit 1; \ + fi + +# Rebuild the source code only when needed +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Set a dummy DATABASE_URL for build time (Prisma needs it to generate client) +ENV DATABASE_URL="file:/tmp/build.db" + +# Generate Prisma client +RUN npx prisma generate + +# Initialize the database schema for build time +RUN npx prisma migrate deploy || npx prisma db push + +# Build the application +RUN npm run build + +# Production image, copy all the files and run next +FROM base AS runner + +# Set timezone to Europe/Paris +RUN apk add --no-cache tzdata +RUN ln -snf /usr/share/zoneinfo/Europe/Paris /etc/localtime && echo Europe/Paris > /etc/timezone + +WORKDIR /app + +ENV NODE_ENV=production +# Uncomment the following line in case you want to disable telemetry during runtime. +# ENV NEXT_TELEMETRY_DISABLED 1 + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +# Copy the public folder +COPY --from=builder /app/public ./public + +# Set the correct permission for prerender cache +RUN mkdir .next +RUN chown nextjs:nodejs .next + +# Automatically leverage output traces to reduce image size +# https://nextjs.org/docs/advanced-features/output-file-tracing +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +# Copy Prisma files +COPY --from=builder /app/prisma ./prisma +COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma + +# Create data directory for SQLite +RUN mkdir -p /app/data && chown nextjs:nodejs /app/data + +# Set all ENV vars before switching user +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" +ENV TZ=Europe/Paris + +USER nextjs + +EXPOSE 3000 + +# Start the application with database migration +CMD ["sh", "-c", "npx prisma migrate deploy && node server.js"] diff --git a/TODO.md b/TODO.md index 5e2afef..982a52e 100644 --- a/TODO.md +++ b/TODO.md @@ -109,8 +109,6 @@ - [x] Auto-création du daily du jour si inexistant - [x] UX améliorée : édition au clic, focus persistant, input large - [x] Vue calendar/historique des dailies -- [ ] Templates de daily personnalisables -- [ ] Recherche dans l'historique des dailies ### 3.2 Intégration Jira Cloud - [ ] Créer `services/jira.ts` - Service de connexion à l'API Jira Cloud diff --git a/components/daily/DailyCalendar.tsx b/components/daily/DailyCalendar.tsx index 3cbb78d..7d7ac07 100644 --- a/components/daily/DailyCalendar.tsx +++ b/components/daily/DailyCalendar.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import { Button } from '@/components/ui/Button'; import { Card } from '@/components/ui/Card'; @@ -10,7 +10,11 @@ interface DailyCalendarProps { dailyDates: string[]; // Liste des dates qui ont des dailies (format YYYY-MM-DD) } -export function DailyCalendar({ currentDate, onDateSelect, dailyDates }: DailyCalendarProps) { +export function DailyCalendar({ + currentDate, + onDateSelect, + dailyDates, +}: DailyCalendarProps) { const [viewDate, setViewDate] = useState(new Date(currentDate)); // Formatage des dates pour comparaison (éviter le décalage timezone) @@ -46,31 +50,32 @@ export function DailyCalendar({ currentDate, onDateSelect, dailyDates }: DailyCa const getDaysInMonth = () => { const year = viewDate.getFullYear(); const month = viewDate.getMonth(); - + // Premier jour du mois const firstDay = new Date(year, month, 1); // Dernier jour du mois const lastDay = new Date(year, month + 1, 0); - + // Premier lundi de la semaine contenant le premier jour const startDate = new Date(firstDay); const dayOfWeek = firstDay.getDay(); const daysToSubtract = dayOfWeek === 0 ? 6 : dayOfWeek - 1; // Lundi = 0 startDate.setDate(firstDay.getDate() - daysToSubtract); - + // Générer toutes les dates du calendrier (6 semaines) const days = []; const currentDay = new Date(startDate); - - for (let i = 0; i < 42; i++) { // 6 semaines × 7 jours + + for (let i = 0; i < 42; i++) { + // 6 semaines × 7 jours days.push(new Date(currentDay)); currentDay.setDate(currentDay.getDate() + 1); } - + return { days, firstDay, lastDay }; }; - const { days, firstDay, lastDay } = getDaysInMonth(); + const { days } = getDaysInMonth(); const handleDateClick = (date: Date) => { onDateSelect(date); @@ -96,7 +101,7 @@ export function DailyCalendar({ currentDate, onDateSelect, dailyDates }: DailyCa const formatMonthYear = () => { return viewDate.toLocaleDateString('fr-FR', { month: 'long', - year: 'numeric' + year: 'numeric', }); }; @@ -114,11 +119,11 @@ export function DailyCalendar({ currentDate, onDateSelect, dailyDates }: DailyCa > ← - +

{formatMonthYear()}

- + @@ -155,7 +156,6 @@ export function DailyCalendar({ currentDate, onDateSelect, dailyDates }: DailyCa {/* Grille du calendrier */}
{days.map((date, index) => { - const dateKey = formatDateKey(date); const isCurrentMonthDay = isCurrentMonth(date); const isTodayDay = isToday(date); const hasCheckboxes = hasDaily(date); @@ -167,35 +167,30 @@ export function DailyCalendar({ currentDate, onDateSelect, dailyDates }: DailyCa onClick={() => handleDateClick(date)} className={` relative p-2 text-sm rounded transition-all hover:bg-[var(--muted)]/50 - ${isCurrentMonthDay - ? 'text-[var(--foreground)]' - : 'text-[var(--muted-foreground)]' + ${ + isCurrentMonthDay + ? 'text-[var(--foreground)]' + : 'text-[var(--muted-foreground)]' } - ${isTodayDay - ? 'bg-[var(--primary)]/20 border border-[var(--primary)]' - : '' - } - ${isSelectedDay - ? 'bg-[var(--primary)] text-white' - : '' - } - ${hasCheckboxes - ? 'font-bold' - : '' + ${ + isTodayDay + ? 'bg-[var(--primary)]/20 border border-[var(--primary)]' + : '' } + ${isSelectedDay ? 'bg-[var(--primary)] text-white' : ''} + ${hasCheckboxes ? 'font-bold' : ''} `} > {date.getDate()} - + {/* Indicateur de daily existant */} {hasCheckboxes && ( -
+ ${isSelectedDay ? 'bg-white' : 'bg-[var(--primary)]'} + `} + /> )} ); @@ -210,7 +205,7 @@ export function DailyCalendar({ currentDate, onDateSelect, dailyDates }: DailyCa
- Aujourd'hui + Aujourd'hui
diff --git a/components/forms/TagForm.tsx b/components/forms/TagForm.tsx index fff505b..0f8fef8 100644 --- a/components/forms/TagForm.tsx +++ b/components/forms/TagForm.tsx @@ -158,7 +158,7 @@ export function TagForm({ isOpen, onClose, onSubmit, tag, loading = false }: Tag value={formData.color} onChange={(e) => { if (TagsClient.isValidColor(e.target.value)) { - handleCustomColorChange(e as any); + handleCustomColorChange(e as React.ChangeEvent); } }} placeholder="#RRGGBB" @@ -189,7 +189,7 @@ export function TagForm({ isOpen, onClose, onSubmit, tag, loading = false }: Tag

- Les tâches avec ce tag apparaîtront dans la section "Objectifs Principaux" au-dessus du Kanban + Les tâches avec ce tag apparaîtront dans la section "Objectifs Principaux" au-dessus du Kanban

diff --git a/components/kanban/PrioritySwimlanesBoard.tsx b/components/kanban/PrioritySwimlanesBoard.tsx index 89341e8..cd9cfb8 100644 --- a/components/kanban/PrioritySwimlanesBoard.tsx +++ b/components/kanban/PrioritySwimlanesBoard.tsx @@ -6,7 +6,8 @@ import { useMemo } from 'react'; import { getAllPriorities } from '@/lib/status-config'; import { SwimlanesBase, SwimlaneData } from './SwimlanesBase'; -interface PrioritySwimlanesoardProps { +interface PrioritySwimlanesBoardProps { + loading: boolean; tasks: Task[]; onCreateTask?: (data: CreateTaskData) => Promise; onDeleteTask?: (taskId: string) => Promise; @@ -15,40 +16,37 @@ interface PrioritySwimlanesoardProps { onUpdateStatus?: (taskId: string, newStatus: TaskStatus) => Promise; compactView?: boolean; visibleStatuses?: TaskStatus[]; - loading?: boolean; } -export function PrioritySwimlanesBoard({ - tasks, +export function PrioritySwimlanesBoard({ + tasks, onCreateTask, - onDeleteTask, - onEditTask, - onUpdateTitle, - onUpdateStatus, + onDeleteTask, + onEditTask, + onUpdateTitle, + onUpdateStatus, compactView = false, visibleStatuses, - loading = false -}: PrioritySwimlanesoardProps) { - +}: PrioritySwimlanesBoardProps) { // Grouper les tâches par priorités et créer les données de swimlanes const swimlanesData = useMemo((): SwimlaneData[] => { const grouped: { [priorityKey: string]: Task[] } = {}; - + // Initialiser avec toutes les priorités - getAllPriorities().forEach(priority => { + getAllPriorities().forEach((priority) => { grouped[priority.key] = []; }); - - tasks.forEach(task => { + + tasks.forEach((task) => { if (grouped[task.priority]) { grouped[task.priority].push(task); } }); - + // Convertir en format SwimlaneData en respectant l'ordre de priorité return getAllPriorities() .sort((a, b) => b.order - a.order) // Ordre décroissant - plus importantes en haut - .map(priority => ({ + .map((priority) => ({ key: priority.key, label: priority.label, icon: priority.icon, @@ -56,8 +54,8 @@ export function PrioritySwimlanesBoard({ tasks: grouped[priority.key] || [], context: { type: 'priority' as const, - value: priority.key - } + value: priority.key, + }, })); }, [tasks]); @@ -74,4 +72,4 @@ export function PrioritySwimlanesBoard({ visibleStatuses={visibleStatuses} /> ); -} \ No newline at end of file +} diff --git a/components/kanban/SwimlanesBoard.tsx b/components/kanban/SwimlanesBoard.tsx index 4af187d..9ff2e15 100644 --- a/components/kanban/SwimlanesBoard.tsx +++ b/components/kanban/SwimlanesBoard.tsx @@ -15,34 +15,33 @@ interface SwimlanesboardProps { onUpdateStatus?: (taskId: string, newStatus: TaskStatus) => Promise; compactView?: boolean; visibleStatuses?: TaskStatus[]; - loading?: boolean; + loading: boolean; } -export function SwimlanesBoard({ - tasks, +export function SwimlanesBoard({ + tasks, onCreateTask, - onDeleteTask, - onEditTask, - onUpdateTitle, - onUpdateStatus, + onDeleteTask, + onEditTask, + onUpdateTitle, + onUpdateStatus, compactView = false, visibleStatuses, - loading = false }: SwimlanesboardProps) { const { tags: availableTags } = useTasksContext(); // Grouper les tâches par tags et créer les données de swimlanes const swimlanesData = useMemo((): SwimlaneData[] => { const grouped: { [tagName: string]: Task[] } = {}; - + // Ajouter une catégorie pour les tâches sans tags grouped['Sans tag'] = []; - - tasks.forEach(task => { + + tasks.forEach((task) => { if (!task.tags || task.tags.length === 0) { grouped['Sans tag'].push(task); } else { - task.tags.forEach(tagName => { + task.tags.forEach((tagName) => { if (!grouped[tagName]) { grouped[tagName] = []; } @@ -50,7 +49,7 @@ export function SwimlanesBoard({ }); } }); - + // Convertir en format SwimlaneData et trier return Object.entries(grouped) .sort(([a, tasksA], [b, tasksB]) => { @@ -64,7 +63,7 @@ export function SwimlanesBoard({ // Obtenir la couleur du tag const getTagColor = (name: string) => { if (name === 'Sans tag') return '#64748b'; // slate-500 - const tag = availableTags.find(t => t.name === name); + const tag = availableTags.find((t) => t.name === name); return tag?.color || '#64748b'; }; @@ -73,10 +72,13 @@ export function SwimlanesBoard({ label: tagName, color: getTagColor(tagName), tasks: tagTasks, - context: tagName !== 'Sans tag' ? { - type: 'tag' as const, - value: tagName - } : undefined + context: + tagName !== 'Sans tag' + ? { + type: 'tag' as const, + value: tagName, + } + : undefined, }; }); }, [tasks, availableTags]); @@ -94,4 +96,4 @@ export function SwimlanesBoard({ visibleStatuses={visibleStatuses} /> ); -} \ No newline at end of file +} diff --git a/components/ui/HeaderContainer.tsx b/components/ui/HeaderContainer.tsx index bafd8d8..bd17b27 100644 --- a/components/ui/HeaderContainer.tsx +++ b/components/ui/HeaderContainer.tsx @@ -11,6 +11,10 @@ interface HeaderContainerProps { completed: number; inProgress: number; todo: number; + backlog: number; + cancelled: number; + freeze: number; + archived: number; completionRate: number; }; } @@ -18,7 +22,7 @@ interface HeaderContainerProps { export function HeaderContainer({ title, subtitle, initialStats }: HeaderContainerProps) { const { stats, syncing } = useTasks( { limit: 1 }, // Juste pour les stats - { tasks: [], stats: initialStats } + { tasks: [], stats: { ...initialStats, backlog: 0, cancelled: 0, freeze: 0, archived: 0 } } ); return ( diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7a35df2 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,52 @@ +version: '3.8' + +services: + towercontrol: + build: + context: . + dockerfile: Dockerfile + ports: + - "3006:3000" + environment: + - NODE_ENV=production + - DATABASE_URL=file:/app/data/prod.db + - TZ=Europe/Paris + volumes: + # Volume persistant pour la base SQLite + - sqlite_data:/app/data + # Monter ta DB locale (décommente pour utiliser tes données locales) + - ./prisma/dev.db:/app/data/prod.db + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/api/health || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + # Service de développement (optionnel) + towercontrol-dev: + build: + context: . + dockerfile: Dockerfile + target: base + ports: + - "3005:3000" + environment: + - NODE_ENV=development + - DATABASE_URL=file:/app/data/dev.db + volumes: + - .:/app + - /app/node_modules + - /app/.next + - sqlite_data_dev:/app/data + command: sh -c "npm install && npx prisma generate && npx prisma migrate deploy && npm run dev" + profiles: + - dev + +volumes: + sqlite_data: + driver: local + sqlite_data_dev: + driver: local + diff --git a/hooks/useTasks.ts b/hooks/useTasks.ts index 53bf61f..08189c3 100644 --- a/hooks/useTasks.ts +++ b/hooks/useTasks.ts @@ -35,6 +35,7 @@ export function useTasks( completed: 0, inProgress: 0, todo: 0, + backlog: 0, cancelled: 0, freeze: 0, archived: 0, @@ -144,6 +145,7 @@ export function useTasks( completed: updatedTasks.filter(t => t.status === 'done').length, inProgress: updatedTasks.filter(t => t.status === 'in_progress').length, todo: updatedTasks.filter(t => t.status === 'todo').length, + backlog: updatedTasks.filter(t => t.status === 'backlog').length, cancelled: updatedTasks.filter(t => t.status === 'cancelled').length, freeze: updatedTasks.filter(t => t.status === 'freeze').length, archived: updatedTasks.filter(t => t.status === 'archived').length, @@ -188,6 +190,7 @@ export function useTasks( completed: currentTasks.filter(t => t.status === 'done').length, inProgress: currentTasks.filter(t => t.status === 'in_progress').length, todo: currentTasks.filter(t => t.status === 'todo').length, + backlog: currentTasks.filter(t => t.status === 'backlog').length, cancelled: currentTasks.filter(t => t.status === 'cancelled').length, freeze: currentTasks.filter(t => t.status === 'freeze').length, archived: currentTasks.filter(t => t.status === 'archived').length, diff --git a/next.config.ts b/next.config.ts index e9ffa30..af6886a 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,14 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + output: 'standalone', + experimental: { + turbo: { + rules: { + '*.sql': ['raw'], + }, + }, + }, }; export default nextConfig; diff --git a/package.json b/package.json index 86ee959..ac836f7 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "towercontrol-temp", + "name": "towercontrol", "version": "0.1.0", "private": true, "scripts": { diff --git a/scripts/seed-data.ts b/scripts/seed-data.ts index d65478f..6933628 100644 --- a/scripts/seed-data.ts +++ b/scripts/seed-data.ts @@ -43,10 +43,13 @@ async function seedTestData() { const task = await tasksService.createTask(taskData); const statusEmoji = { + 'backlog': '📋', 'todo': '⏳', 'in_progress': '🔄', + 'freeze': '🧊', 'done': '✅', - 'cancelled': '❌' + 'cancelled': '❌', + 'archived': '📦' }[task.status]; const priorityEmoji = { diff --git a/services/daily.ts b/services/daily.ts index eb6b9dd..472a279 100644 --- a/services/daily.ts +++ b/services/daily.ts @@ -1,5 +1,6 @@ import { prisma } from './database'; -import { DailyCheckbox, DailyView, CreateDailyCheckboxData, UpdateDailyCheckboxData, BusinessError, DailyCheckboxType } from '@/lib/types'; +import { Prisma } from '@prisma/client'; +import { DailyCheckbox, DailyView, CreateDailyCheckboxData, UpdateDailyCheckboxData, BusinessError, DailyCheckboxType, TaskStatus, TaskPriority, TaskSource } from '@/lib/types'; /** * Service pour la gestion des checkboxes daily @@ -82,12 +83,18 @@ export class DailyService { * Met à jour une checkbox */ async updateCheckbox(checkboxId: string, data: UpdateDailyCheckboxData): Promise { - const updateData: any = {}; + const updateData: Prisma.DailyCheckboxUpdateInput = {}; if (data.text !== undefined) updateData.text = data.text.trim(); if (data.isChecked !== undefined) updateData.isChecked = data.isChecked; if (data.type !== undefined) updateData.type = data.type; - if (data.taskId !== undefined) updateData.taskId = data.taskId; + if (data.taskId !== undefined) { + if (data.taskId === null) { + updateData.task = { disconnect: true }; + } else { + updateData.task = { connect: { id: data.taskId } }; + } + } if (data.order !== undefined) updateData.order = data.order; const checkbox = await prisma.dailyCheckbox.update({ @@ -210,7 +217,7 @@ export class DailyService { /** * Mappe une checkbox Prisma vers notre interface */ - private mapPrismaCheckbox(checkbox: any): DailyCheckbox { + private mapPrismaCheckbox(checkbox: Prisma.DailyCheckboxGetPayload<{ include: { task: true } }>): DailyCheckbox { return { id: checkbox.id, date: checkbox.date, @@ -218,23 +225,23 @@ export class DailyService { isChecked: checkbox.isChecked, type: checkbox.type as DailyCheckboxType, order: checkbox.order, - taskId: checkbox.taskId, + taskId: checkbox.taskId || undefined, task: checkbox.task ? { id: checkbox.task.id, title: checkbox.task.title, - description: checkbox.task.description, - status: checkbox.task.status, - priority: checkbox.task.priority, - source: checkbox.task.source, - sourceId: checkbox.task.sourceId, + description: checkbox.task.description || undefined, + status: checkbox.task.status as TaskStatus, + priority: checkbox.task.priority as TaskPriority, + source: checkbox.task.source as TaskSource, + sourceId: checkbox.task.sourceId || undefined, tags: [], // Les tags seront chargés séparément si nécessaire - dueDate: checkbox.task.dueDate, - completedAt: checkbox.task.completedAt, + dueDate: checkbox.task.dueDate || undefined, + completedAt: checkbox.task.completedAt || undefined, createdAt: checkbox.task.createdAt, updatedAt: checkbox.task.updatedAt, - jiraProject: checkbox.task.jiraProject, - jiraKey: checkbox.task.jiraKey, - assignee: checkbox.task.assignee + jiraProject: checkbox.task.jiraProject || undefined, + jiraKey: checkbox.task.jiraKey || undefined, + assignee: checkbox.task.assignee || undefined } : undefined, createdAt: checkbox.createdAt, updatedAt: checkbox.updatedAt diff --git a/services/tags.ts b/services/tags.ts index b08b006..7bbcdd4 100644 --- a/services/tags.ts +++ b/services/tags.ts @@ -1,4 +1,5 @@ import { prisma } from './database'; +import { Prisma } from '@prisma/client'; import { Tag } from '@/lib/types'; /** @@ -113,7 +114,7 @@ export const tagsService = { } } - const updateData: any = {}; + const updateData: Prisma.TagUpdateInput = {}; if (data.name !== undefined) { updateData.name = data.name.trim(); } diff --git a/src/app/daily/page.tsx b/src/app/daily/page.tsx index fc31b5f..1b6b521 100644 --- a/src/app/daily/page.tsx +++ b/src/app/daily/page.tsx @@ -2,6 +2,9 @@ import { Metadata } from 'next'; import { DailyPageClient } from './DailyPageClient'; import { dailyService } from '@/services/daily'; +// Force dynamic rendering (no static generation) +export const dynamic = 'force-dynamic'; + export const metadata: Metadata = { title: 'Daily - Tower Control', description: 'Gestion quotidienne des tâches et objectifs', diff --git a/src/app/page.tsx b/src/app/page.tsx index 61f025a..52ac102 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -2,6 +2,9 @@ import { tasksService } from '@/services/tasks'; import { tagsService } from '@/services/tags'; import { HomePageClient } from '@/components/HomePageClient'; +// Force dynamic rendering (no static generation) +export const dynamic = 'force-dynamic'; + export default async function HomePage() { // SSR - Récupération des données côté serveur const [initialTasks, initialStats, initialTags] = await Promise.all([ diff --git a/src/app/tags/page.tsx b/src/app/tags/page.tsx index 3e57dbe..77b269e 100644 --- a/src/app/tags/page.tsx +++ b/src/app/tags/page.tsx @@ -1,6 +1,9 @@ import { tagsService } from '@/services/tags'; import { TagsPageClient } from './TagsPageClient'; +// Force dynamic rendering (no static generation) +export const dynamic = 'force-dynamic'; + export default async function TagsPage() { // SSR - Récupération des tags côté serveur const initialTags = await tagsService.getTags(); diff --git a/src/contexts/TasksContext.tsx b/src/contexts/TasksContext.tsx index f521c3f..50d2ffa 100644 --- a/src/contexts/TasksContext.tsx +++ b/src/contexts/TasksContext.tsx @@ -88,7 +88,8 @@ export function TasksProvider({ children, initialTasks, initialStats, initialTag compactView: newFilters.compactView || false, swimlanesByTags: newFilters.swimlanesByTags || false, swimlanesMode: newFilters.swimlanesMode || 'tags', - showObjectives: true // Toujours visible pour l'instant + showObjectives: true, // Toujours visible pour l'instant + showFilters: true // Toujours visible pour l'instant }); };