Skip to content

Commit 4cdbfb1

Browse files
pfillion42claude
andcommitted
feat: add light/dark theme toggle and stale memories detection
Sprint 9: theme toggle with CSS custom properties and localStorage persistence, plus stale memories page with age/quality filters and bulk delete. 248 tests passing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 38d5d3d commit 4cdbfb1

12 files changed

Lines changed: 961 additions & 4 deletions

File tree

DEVLOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
11
# Journal de Developpement
22

3+
## 2026-02-14 - Sprint 9 : Theme clair et memoires obsoletes
4+
5+
### Sprint 9.1 - Toggle theme clair/sombre
6+
- **CSS** : bloc `[data-theme="light"]` dans theme.css, toutes les variables redefinies (backgrounds blancs, textes sombres, borders gris, shadows subtils)
7+
- **Hook** : `useTheme()` - localStorage (cle `memviz-theme`), support `prefers-color-scheme`, applique `data-theme` sur `documentElement`
8+
- **UI** : bouton soleil/lune dans le header App.tsx, toggle au clic
9+
- 9 tests ThemeToggle (localStorage, toggle, persistence, defaut dark, preference systeme)
10+
11+
### Sprint 9.2 - Detection memoires obsoletes
12+
- **Backend** : `GET /api/memories/stale` - filtre OR (anciennes created_at < now-days*86400, basse qualite <= quality_max), COALESCE pour metadata null, tri ASC, limit max 200
13+
- **Frontend** : Page `/stale` avec sliders (age 30-365j, qualite 0-50%), liste avec liens, badges type/tags, score qualite, boutons Supprimer/Tout supprimer (avec confirm)
14+
- **Hook** : `useStaleMemories(days, qualityMax)` via React Query
15+
- **Navigation** : lien "Obsoletes" entre Tags et Graphe
16+
- 11 nouveaux tests backend + 7 tests frontend
17+
18+
### Resultats
19+
- 150 tests serveur + 98 tests client = **248 tests total**, tous verts
20+
- TypeScript compile sans erreur, lint propre
21+
- Equipe 2 agents paralleles (theme-dev + stale-dev)
22+
23+
---
24+
325
## 2026-02-14 - Sprint 8.1 : Correctifs de securite
426

527
### Contexte

PLAN.md

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,19 +72,34 @@ avec une interface web moderne et une API backend. Interfacable avec Claude.
7272
- [ ] Validation d'entrees avec `zod` (schemas stricts)
7373
- [ ] Token API optionnel (authentification legere)
7474

75+
## Sprint 9 - Theme clair et memoires obsoletes
76+
77+
### 9.1 Toggle theme clair/sombre - COMPLETE
78+
- CSS : bloc `[data-theme="light"]` dans theme.css avec toutes les variables redefinies
79+
- Hook : `useTheme()` avec localStorage + prefers-color-scheme
80+
- UI : bouton soleil/lune dans le header App.tsx
81+
- 9 tests ThemeToggle
82+
83+
### 9.2 Detection memoires obsoletes - COMPLETE
84+
- Backend : GET /api/memories/stale (filtres days, quality_max, limit, tri ASC)
85+
- Frontend : Page /stale avec sliders age/qualite, liste, boutons Supprimer/Tout supprimer
86+
- Hook : useStaleMemories(days, qualityMax)
87+
- Navigation : lien "Obsoletes" entre Tags et Graphe
88+
- 11 tests backend + 7 tests frontend
89+
7590
## Backlog - Fonctionnalites futures
7691

7792
### Exploration et comprehension
7893
- [ ] Projection 2D des embeddings (t-SNE/UMAP) - vue espace vectoriel complet
7994
- [ ] Clustering automatique - grouper par proximite semantique
8095

8196
### Navigation et UX
82-
- [ ] Mode clair / toggle theme
97+
- [x] Mode clair / toggle theme (Sprint 9)
8398
- [ ] Pagination recherche vectorielle
8499

85100
### Gestion et maintenance
86-
- [ ] Gestion globale des tags - renommer, fusionner, supprimer un tag partout
87-
- [ ] Memoires obsoletes - identifier et suggerer nettoyage (jamais accedees, anciennes)
101+
- [x] Gestion globale des tags - renommer, fusionner, supprimer un tag partout (Sprint 7)
102+
- [x] Memoires obsoletes - identifier et suggerer nettoyage (Sprint 9)
88103

89104
### Synchronisation et integration
90105
- [ ] Live reload - surveiller la DB SQLite, mise a jour temps reel
@@ -124,3 +139,4 @@ avec une interface web moderne et une API backend. Interfacable avec Claude.
124139
| 2026-02-14 | Sprint 6 timeline + qualite | Timeline chronologique, QualityVoter etoiles 1-5, 175 tests verts |
125140
| 2026-02-14 | Sprint 7 tags + raccourcis | Gestion tags (rename/delete/merge), raccourcis clavier, 215 tests verts |
126141
| 2026-02-14 | Sprint 8.1 securite | 6 correctifs : bind localhost, CORS, body limit, env DB, FTS5 sanitize, LIKE escape, 221 tests verts |
142+
| 2026-02-14 | Sprint 9 theme + obsoletes | Toggle dark/light, page memoires obsoletes, 248 tests verts |

client/src/App.tsx

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ import { GraphView } from './pages/GraphView';
88
import { Duplicates } from './pages/Duplicates';
99
import { Timeline } from './pages/Timeline';
1010
import { Tags } from './pages/Tags';
11+
import { Stale } from './pages/Stale';
1112
import { Logo } from './components/Logo';
1213
import { KeyboardHelp } from './components/KeyboardHelp';
1314
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts';
15+
import { useTheme } from './hooks/useTheme';
1416

1517
const queryClient = new QueryClient({
1618
defaultOptions: {
@@ -40,6 +42,7 @@ const activeStyle: React.CSSProperties = {
4042

4143
function AppContent() {
4244
const [showHelp, setShowHelp] = useState(false);
45+
const { theme, toggleTheme } = useTheme();
4346
useKeyboardShortcuts({ onToggleHelp: () => setShowHelp(v => !v) });
4447

4548
return (
@@ -83,12 +86,44 @@ function AppContent() {
8386
<NavLink to="/tags" style={({ isActive }) => isActive ? activeStyle : navStyle}>
8487
Tags
8588
</NavLink>
89+
<NavLink to="/stale" style={({ isActive }) => isActive ? activeStyle : navStyle}>
90+
Obsoletes
91+
</NavLink>
8692
<NavLink to="/graph" style={({ isActive }) => isActive ? activeStyle : navStyle}>
8793
Graphe
8894
</NavLink>
8995
</nav>
9096

91-
<div style={{ marginLeft: 'auto', fontSize: '12px', color: 'var(--text-muted)' }}>
97+
<button
98+
onClick={toggleTheme}
99+
aria-label="Toggle theme"
100+
style={{
101+
marginLeft: 'auto',
102+
background: 'transparent',
103+
border: '1px solid var(--border-default)',
104+
borderRadius: 'var(--radius-sm)',
105+
color: 'var(--text-secondary)',
106+
cursor: 'pointer',
107+
fontSize: '18px',
108+
padding: '6px 10px',
109+
transition: 'all var(--transition-fast)',
110+
display: 'flex',
111+
alignItems: 'center',
112+
justifyContent: 'center',
113+
}}
114+
onMouseEnter={(e) => {
115+
e.currentTarget.style.backgroundColor = 'var(--bg-hover)';
116+
e.currentTarget.style.borderColor = 'var(--border-accent)';
117+
}}
118+
onMouseLeave={(e) => {
119+
e.currentTarget.style.backgroundColor = 'transparent';
120+
e.currentTarget.style.borderColor = 'var(--border-default)';
121+
}}
122+
>
123+
{theme === 'dark' ? '☀' : '☾'}
124+
</button>
125+
126+
<div style={{ fontSize: '12px', color: 'var(--text-muted)' }}>
92127
Memory Explorer
93128
</div>
94129
</header>
@@ -101,6 +136,7 @@ function AppContent() {
101136
<Route path="/memories/:hash" element={<MemoryDetail />} />
102137
<Route path="/duplicates" element={<Duplicates />} />
103138
<Route path="/tags" element={<Tags />} />
139+
<Route path="/stale" element={<Stale />} />
104140
<Route path="/graph" element={<GraphView />} />
105141
</Routes>
106142
</main>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { useQuery } from '@tanstack/react-query';
2+
import type { Memory } from '../types';
3+
4+
export interface StaleResponse {
5+
data: Memory[];
6+
total: number;
7+
criteria: {
8+
days: number;
9+
quality_max: number;
10+
};
11+
}
12+
13+
async function fetchStaleMemories(days: number, qualityMax: number): Promise<StaleResponse> {
14+
const params = new URLSearchParams({
15+
days: days.toString(),
16+
quality_max: qualityMax.toString(),
17+
});
18+
19+
const res = await fetch(`/api/memories/stale?${params}`);
20+
if (!res.ok) throw new Error('Erreur lors du chargement des memoires obsoletes');
21+
return res.json();
22+
}
23+
24+
export function useStaleMemories(days: number, qualityMax: number) {
25+
return useQuery({
26+
queryKey: ['stale-memories', days, qualityMax],
27+
queryFn: () => fetchStaleMemories(days, qualityMax),
28+
});
29+
}

client/src/hooks/useTheme.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { useState, useEffect } from 'react';
2+
3+
type Theme = 'dark' | 'light';
4+
5+
interface UseThemeReturn {
6+
theme: Theme;
7+
toggleTheme: () => void;
8+
setTheme: (theme: Theme) => void;
9+
}
10+
11+
const STORAGE_KEY = 'memviz-theme';
12+
13+
function getSystemPreference(): Theme {
14+
if (typeof window === 'undefined') return 'dark';
15+
return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
16+
}
17+
18+
function getInitialTheme(): Theme {
19+
if (typeof window === 'undefined') return 'dark';
20+
21+
const stored = localStorage.getItem(STORAGE_KEY);
22+
if (stored === 'dark' || stored === 'light') {
23+
return stored;
24+
}
25+
26+
return getSystemPreference();
27+
}
28+
29+
function applyTheme(theme: Theme): void {
30+
document.documentElement.dataset.theme = theme;
31+
}
32+
33+
export function useTheme(): UseThemeReturn {
34+
const [theme, setThemeState] = useState<Theme>(getInitialTheme);
35+
36+
useEffect(() => {
37+
applyTheme(theme);
38+
}, [theme]);
39+
40+
const setTheme = (newTheme: Theme) => {
41+
setThemeState(newTheme);
42+
localStorage.setItem(STORAGE_KEY, newTheme);
43+
};
44+
45+
const toggleTheme = () => {
46+
const newTheme = theme === 'dark' ? 'light' : 'dark';
47+
setTheme(newTheme);
48+
};
49+
50+
return {
51+
theme,
52+
toggleTheme,
53+
setTheme,
54+
};
55+
}

0 commit comments

Comments
 (0)