Skip to content

Commit 742fc1a

Browse files
pfillion42claude
andcommitted
fix(server): use separate writable DB for access logs
The memory_access_log table was created in the main MCP DB which is opened readonly, so the table never existed and usage-stats always returned empty accesses. Move access logs to a dedicated writable SQLite DB injected via RouterOptions.accessLogDb. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 417f918 commit 742fc1a

7 files changed

Lines changed: 186 additions & 75 deletions

File tree

PLAN.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,26 @@ avec une interface web moderne et une API backend. Interfacable avec Claude.
194194
- Build client OK
195195
- Audit securite : pas de vulnerabilite specifique au sprint, points preexistants (rate-limit, helmet, zod) dans backlog Sprint 8.2
196196

197+
## Sprint 14 - DB separee pour les logs d'acces
198+
199+
### 14.1 Refactoring access log DB - COMPLETE
200+
- Nouveau fichier `server/src/access-log-db.ts` : singleton writable (pattern identique a db.ts)
201+
- Chemin : `ACCESS_LOG_DB_PATH` env var, ou derive de `MEMORY_DB_PATH` (remplace .db par _access.db)
202+
- Table `memory_access_log` + index crees au premier appel, pas de sqlite-vec requis
203+
- `RouterOptions.accessLogDb` optionnel dans createMemoriesRouter
204+
- POST /:hash/access : UPDATE metadata dans main DB (try/catch readonly), INSERT dans accessLogDb
205+
- GET /usage-stats : lecture des acces depuis accessLogDb au lieu de la DB principale
206+
- Comportement graceful si accessLogDb absent (tableau vide, pas d'erreur)
207+
- `memory_access_log` retiree du schema test principal, nouvelle fonction `createTestAccessLogDb()`
208+
- 2 nouveaux tests : POST /access avec main DB readonly + GET /usage-stats lit depuis DB separee
209+
- Correction lint pre-existante (hasRating non utilise)
210+
211+
### Bilan Sprint 14
212+
- 2 nouveaux tests backend
213+
- Total : 339 tests (213 serveur + 126 client), tous verts
214+
- Lint serveur OK
215+
- Build client inchange
216+
197217
## Backlog - Fonctionnalites futures
198218

199219
### Exploration et comprehension
@@ -228,7 +248,8 @@ avec une interface web moderne et une API backend. Interfacable avec Claude.
228248
- Chemin DB : via variable d'environnement `MEMORY_DB_PATH` (requis, defini dans server/.env)
229249
- Schema : 5 tables (memories, memory_content_fts fts5, memory_embeddings vec0, memory_graph, metadata)
230250
- Embedding : all-MiniLM-L6-v2 (384 dims, cosine distance)
231-
- Injection de dependance : `createMemoriesRouter(db)` pour faciliter les tests
251+
- Injection de dependance : `createMemoriesRouter(db, { accessLogDb, embedFn })` pour faciliter les tests
252+
- Access log : DB separee writable (`access-log-db.ts`), derive de MEMORY_DB_PATH si ACCESS_LOG_DB_PATH absent
232253
- Frontend : React Query + React Router, hooks custom, theme sombre via CSS custom properties
233254
- Navigation : / (Dashboard), /timeline (Timeline), /memories (MemoryList), /memories/:hash (MemoryDetail), /duplicates (Duplicates), /tags (Tags), /stale (Stale), /embeddings (EmbeddingView), /clusters (ClusterView), /graph (GraphView)
234255
- Embedder : @huggingface/transformers (all-MiniLM-L6-v2), injection de dependance pour tests
@@ -258,3 +279,4 @@ avec une interface web moderne et une API backend. Interfacable avec Claude.
258279
| 2026-02-15 | Sprint 13 clustering semantique | UnionFind classe, GET clusters, ClusterView, ScatterPlot colorMap, 314 tests verts |
259280
| 2026-02-15 | Sprint 8.2 securite secondaire | helmet, rate-limit, token API, validation zod (8 schemas), 337 tests verts |
260281
| 2026-02-15 | Lanceur desktop | start/stop-memviz.bat/.ps1, raccourci bureau avec icone SVG→ICO, detection services existants |
282+
| 2026-02-15 | Sprint 14 DB separee access log | access-log-db.ts singleton, refactoring POST /access + GET /usage-stats, 339 tests verts |

server/.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,8 @@ MEMORY_DB_PATH=/chemin/vers/sqlite_vec.db
77
# Origines CORS autorisees (separer par des virgules)
88
# CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173
99

10+
# Chemin vers la base access log (optionnel, derive de MEMORY_DB_PATH si absent)
11+
# ACCESS_LOG_DB_PATH=/chemin/vers/sqlite_vec_access.db
12+
1013
# Interface d'ecoute (127.0.0.1 par defaut = localhost seulement)
1114
# HOST=127.0.0.1

server/src/access-log-db.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import Database, { Database as DatabaseType } from 'better-sqlite3';
2+
3+
const ACCESS_LOG_SCHEMA = `
4+
CREATE TABLE IF NOT EXISTS memory_access_log (
5+
id INTEGER PRIMARY KEY AUTOINCREMENT,
6+
content_hash TEXT NOT NULL,
7+
accessed_at REAL NOT NULL
8+
);
9+
CREATE INDEX IF NOT EXISTS idx_access_log_at ON memory_access_log(accessed_at);
10+
`;
11+
12+
let accessLogDb: DatabaseType | null = null;
13+
14+
function deriveAccessLogPath(): string {
15+
const mainPath = process.env.MEMORY_DB_PATH;
16+
if (!mainPath) {
17+
throw new Error('ACCESS_LOG_DB_PATH ou MEMORY_DB_PATH requis pour la base access log.');
18+
}
19+
return mainPath.replace(/\.db$/, '_access.db');
20+
}
21+
22+
export function getAccessLogDb(): DatabaseType {
23+
if (!accessLogDb) {
24+
const dbPath = process.env.ACCESS_LOG_DB_PATH || deriveAccessLogPath();
25+
accessLogDb = new Database(dbPath);
26+
accessLogDb.exec(ACCESS_LOG_SCHEMA);
27+
}
28+
return accessLogDb;
29+
}
30+
31+
export function closeAccessLogDb(): void {
32+
if (accessLogDb) {
33+
accessLogDb.close();
34+
accessLogDb = null;
35+
}
36+
}

server/src/index.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import cors from 'cors';
44
import helmet from 'helmet';
55
import rateLimit from 'express-rate-limit';
66
import { getDb, closeDb } from './db';
7+
import { getAccessLogDb, closeAccessLogDb } from './access-log-db';
78
import { createMemoriesRouter } from './routes/memories';
89
import { initEmbedder, getEmbedder } from './embedder';
910

@@ -71,8 +72,8 @@ async function start() {
7172
console.warn('Embedder non disponible - recherche vectorielle desactivee.', err);
7273
}
7374

74-
// Routes memories (avec embedder si disponible)
75-
app.use('/api', createMemoriesRouter(getDb(), { embedFn }));
75+
// Routes memories (avec embedder et access log DB si disponibles)
76+
app.use('/api', createMemoriesRouter(getDb(), { embedFn, accessLogDb: getAccessLogDb() }));
7677

7778
if (process.env.NODE_ENV !== 'test') {
7879
// Securite : ecouter uniquement sur localhost (pas expose au reseau)
@@ -85,9 +86,10 @@ async function start() {
8586

8687
start();
8788

88-
// Fermer la DB proprement a l'arret
89+
// Fermer les DB proprement a l'arret
8990
process.on('SIGINT', () => {
9091
closeDb();
92+
closeAccessLogDb();
9193
process.exit(0);
9294
});
9395

server/src/routes/memories.ts

Lines changed: 25 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ function parseMemory(row: MemoryRow) {
108108

109109
interface RouterOptions {
110110
embedFn?: EmbedFn;
111+
accessLogDb?: DatabaseType;
111112
}
112113

113114
// Securite : assainir les entrees FTS5 MATCH (retirer les operateurs speciaux)
@@ -175,20 +176,6 @@ export class UnionFind {
175176
export function createMemoriesRouter(db: DatabaseType, options: RouterOptions = {}): Router {
176177
const router = Router();
177178

178-
// Creer la table memory_access_log si elle n'existe pas (try/catch pour DB readonly)
179-
try {
180-
db.exec(`
181-
CREATE TABLE IF NOT EXISTS memory_access_log (
182-
id INTEGER PRIMARY KEY AUTOINCREMENT,
183-
content_hash TEXT NOT NULL,
184-
accessed_at REAL NOT NULL
185-
);
186-
CREATE INDEX IF NOT EXISTS idx_access_log_at ON memory_access_log(accessed_at);
187-
`);
188-
} catch {
189-
// DB en lecture seule, la table n'existe peut-etre pas
190-
}
191-
192179
// GET /api/memories/vector-search - recherche par similarite vectorielle
193180
router.get('/memories/vector-search', async (req: Request, res: Response) => {
194181
const q = req.query.q as string | undefined;
@@ -748,17 +735,19 @@ export function createMemoriesRouter(db: DatabaseType, options: RouterOptions =
748735
ORDER BY date ASC
749736
`).all() as { date: string; count: number }[];
750737

751-
// Acces par periode (depuis memory_access_log)
738+
// Acces par periode (depuis la DB de log separee)
752739
let accesses: { date: string; count: number }[] = [];
753-
try {
754-
accesses = db.prepare(`
755-
SELECT ${accessDateExpr} as date, COUNT(*) as count
756-
FROM memory_access_log
757-
GROUP BY date
758-
ORDER BY date ASC
759-
`).all() as { date: string; count: number }[];
760-
} catch {
761-
// Table absente
740+
if (options.accessLogDb) {
741+
try {
742+
accesses = options.accessLogDb.prepare(`
743+
SELECT ${accessDateExpr} as date, COUNT(*) as count
744+
FROM memory_access_log
745+
GROUP BY date
746+
ORDER BY date ASC
747+
`).all() as { date: string; count: number }[];
748+
} catch {
749+
// Table absente
750+
}
762751
}
763752

764753
res.json({ period, creations, accesses });
@@ -1011,19 +1000,22 @@ export function createMemoriesRouter(db: DatabaseType, options: RouterOptions =
10111000
const now = Math.floor(Date.now() / 1000);
10121001
metadata.last_accessed_at = now;
10131002

1014-
db.prepare(`
1015-
UPDATE memories
1016-
SET metadata = ?
1017-
WHERE content_hash = ?
1018-
`).run(JSON.stringify(metadata), hash);
1019-
1020-
// Enregistrer l'acces dans le log
1003+
// Mise a jour metadata dans la DB principale (try/catch pour DB readonly)
10211004
try {
10221005
db.prepare(`
1006+
UPDATE memories
1007+
SET metadata = ?
1008+
WHERE content_hash = ?
1009+
`).run(JSON.stringify(metadata), hash);
1010+
} catch {
1011+
// DB en lecture seule, la mise a jour de metadata est ignoree
1012+
}
1013+
1014+
// Enregistrer l'acces dans la DB de log separee
1015+
if (options.accessLogDb) {
1016+
options.accessLogDb.prepare(`
10231017
INSERT INTO memory_access_log (content_hash, accessed_at) VALUES (?, ?)
10241018
`).run(hash, now);
1025-
} catch {
1026-
// Table absente (DB readonly sans migration), on ignore
10271019
}
10281020

10291021
res.json({
@@ -1042,7 +1034,6 @@ export function createMemoriesRouter(db: DatabaseType, options: RouterOptions =
10421034
const { rating, score } = body;
10431035

10441036
const hasScore = score !== undefined && score !== null;
1045-
const hasRating = rating !== undefined && rating !== null;
10461037

10471038
const row = db.prepare(
10481039
'SELECT * FROM memories WHERE content_hash = ? AND deleted_at IS NULL'

server/tests/helpers/test-db.ts

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,6 @@ const SCHEMA = `
7777
value TEXT NOT NULL
7878
);
7979
80-
CREATE TABLE IF NOT EXISTS memory_access_log (
81-
id INTEGER PRIMARY KEY AUTOINCREMENT,
82-
content_hash TEXT NOT NULL,
83-
accessed_at REAL NOT NULL
84-
);
85-
CREATE INDEX IF NOT EXISTS idx_access_log_at ON memory_access_log(accessed_at);
8680
`;
8781

8882
const SEED_DATA: TestMemory[] = [
@@ -248,15 +242,6 @@ const SEED_METADATA = [
248242
{ key: 'fts5_enabled', value: 'true' },
249243
];
250244

251-
// Acces seed : quelques entrees sur 2 jours differents
252-
const SEED_ACCESS_LOG = [
253-
{ content_hash: 'hash_aaa111', accessed_at: 1771000100 }, // 2026-02-13
254-
{ content_hash: 'hash_aaa111', accessed_at: 1771000200 }, // 2026-02-13
255-
{ content_hash: 'hash_bbb222', accessed_at: 1771088500 }, // 2026-02-14
256-
{ content_hash: 'hash_eee555', accessed_at: 1771088600 }, // 2026-02-14
257-
{ content_hash: 'hash_eee555', accessed_at: 1771088700 }, // 2026-02-14
258-
];
259-
260245
export function createTestDb(): DatabaseType {
261246
const db = new Database(':memory:');
262247
sqliteVec.load(db);
@@ -291,15 +276,10 @@ export function createTestDb(): DatabaseType {
291276
INSERT INTO metadata (key, value) VALUES (@key, @value)
292277
`);
293278

294-
const insertAccessLog = db.prepare(`
295-
INSERT INTO memory_access_log (content_hash, accessed_at) VALUES (@content_hash, @accessed_at)
296-
`);
297-
298279
const seedAll = db.transaction(() => {
299280
for (const m of SEED_DATA) insertMemory.run(m);
300281
for (const g of SEED_GRAPH) insertGraph.run(g);
301282
for (const m of SEED_METADATA) insertMeta.run(m);
302-
for (const a of SEED_ACCESS_LOG) insertAccessLog.run(a);
303283

304284
// Inserer des embeddings factices pour les memoires (id 1-5)
305285
for (let i = 1; i <= SEED_DATA.length; i++) {
@@ -315,6 +295,35 @@ export function createTestDb(): DatabaseType {
315295
return db;
316296
}
317297

298+
// Acces seed : quelques entrees sur 2 jours differents
299+
const SEED_ACCESS_LOG = [
300+
{ content_hash: 'hash_aaa111', accessed_at: 1771000100 }, // 2026-02-13
301+
{ content_hash: 'hash_aaa111', accessed_at: 1771000200 }, // 2026-02-13
302+
{ content_hash: 'hash_bbb222', accessed_at: 1771088500 }, // 2026-02-14
303+
{ content_hash: 'hash_eee555', accessed_at: 1771088600 }, // 2026-02-14
304+
{ content_hash: 'hash_eee555', accessed_at: 1771088700 }, // 2026-02-14
305+
];
306+
307+
export function createTestAccessLogDb(): DatabaseType {
308+
const db = new Database(':memory:');
309+
db.exec(`
310+
CREATE TABLE memory_access_log (
311+
id INTEGER PRIMARY KEY AUTOINCREMENT,
312+
content_hash TEXT NOT NULL,
313+
accessed_at REAL NOT NULL
314+
);
315+
CREATE INDEX idx_access_log_at ON memory_access_log(accessed_at);
316+
`);
317+
318+
const insert = db.prepare(
319+
'INSERT INTO memory_access_log (content_hash, accessed_at) VALUES (@content_hash, @accessed_at)'
320+
);
321+
322+
for (const a of SEED_ACCESS_LOG) insert.run(a);
323+
324+
return db;
325+
}
326+
318327
// Nombre de memoires non-supprimees dans le seed
319328
export const ACTIVE_MEMORY_COUNT = SEED_DATA.filter(m => m.deleted_at === null).length;
320329
export const TOTAL_MEMORY_COUNT = SEED_DATA.length;

0 commit comments

Comments
 (0)