From 72fbbd55d49059e1305bce57bc8613abbda03f2c Mon Sep 17 00:00:00 2001 From: Maurice Date: Wed, 7 Oct 2026 16:47:53 +0200 Subject: [PATCH] fix: a trashed note file leaves its note, quieter MCP sweep, bounded compose logs A note's attachment moved to the trash from the file manager kept showing on the note; the hydration read now skips it, as the chat one already did. The MCP session sweep logged a line every minute while any session was open; it logs only when it cleaned something. The compose file caps the container log, and three test mocks that pointed at modules long gone are removed. --- client/src/components/Tours/TourDetailDialog.test.tsx | 1 - .../components/Tours/planner/TourPlannerPanels.test.tsx | 1 - client/src/pages/TripPlannerPage.test.tsx | 4 ---- docker-compose.yml | 6 ++++++ server/src/db/repositories/CollabNotes.repository.ts | 8 +++++++- server/src/mcp/index.ts | 4 +++- server/src/websocket.ts | 2 +- server/tests/integration/journey.test.ts | 4 ---- server/tests/unit/nest/collab.service.test.ts | 4 ++++ 9 files changed, 21 insertions(+), 13 deletions(-) diff --git a/client/src/components/Tours/TourDetailDialog.test.tsx b/client/src/components/Tours/TourDetailDialog.test.tsx index b0bf855ef..e259a4014 100644 --- a/client/src/components/Tours/TourDetailDialog.test.tsx +++ b/client/src/components/Tours/TourDetailDialog.test.tsx @@ -9,7 +9,6 @@ import TourDetailDialog from './TourDetailDialog' vi.mock('../../../../server/src/config', () => { throw new Error('Config initialization is forbidden in client tests') }) vi.mock('../../../../server/src/db/database', () => { throw new Error('Legacy database imports are forbidden in client tests') }) -vi.mock('../../../../server/src/nest/database/database.service', () => { throw new Error('Database service imports are forbidden in client tests') }) vi.mock('better-sqlite3', () => { throw new Error('SQLite imports are forbidden in client tests') }) const place = buildPlace({ diff --git a/client/src/components/Tours/planner/TourPlannerPanels.test.tsx b/client/src/components/Tours/planner/TourPlannerPanels.test.tsx index 93d0c9053..81bb601e6 100644 --- a/client/src/components/Tours/planner/TourPlannerPanels.test.tsx +++ b/client/src/components/Tours/planner/TourPlannerPanels.test.tsx @@ -12,7 +12,6 @@ import { TourPlannerRail, TourPlannerToursRail } from './TourPlannerPanels' vi.mock('../../../../../server/src/config', () => { throw new Error('Config initialization is forbidden in client tests') }) vi.mock('../../../../../server/src/db/database', () => { throw new Error('Legacy database imports are forbidden in client tests') }) -vi.mock('../../../../../server/src/nest/database/database.service', () => { throw new Error('Database service imports are forbidden in client tests') }) vi.mock('better-sqlite3', () => { throw new Error('SQLite imports are forbidden in client tests') }) const tour: TourListItem = { diff --git a/client/src/pages/TripPlannerPage.test.tsx b/client/src/pages/TripPlannerPage.test.tsx index 234d51a96..3834e7598 100644 --- a/client/src/pages/TripPlannerPage.test.tsx +++ b/client/src/pages/TripPlannerPage.test.tsx @@ -87,10 +87,6 @@ vi.mock('../components/Planner/DayDetailPanel', () => ({ }, })); -vi.mock('../components/Memories/MemoriesPanel', () => ({ - default: () => React.createElement('div', { 'data-testid': 'memories-panel' }), -})); - vi.mock('../components/Collab/CollabPanel', () => ({ default: () => React.createElement('div', { 'data-testid': 'collab-panel' }), })); diff --git a/docker-compose.yml b/docker-compose.yml index 51580510d..0a88479ad 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -68,6 +68,12 @@ services: - ./uploads:/app/uploads # - ./storage-config.json:/app/data/storage-config.json:ro # optional seed-once storage provisioning (wiki: Admin-Storage) restart: unless-stopped + # Docker's json-file driver keeps every line forever by default. + logging: + driver: json-file + options: + max-size: 10m + max-file: "3" healthcheck: # Through a shell so a changed PORT is followed. test: ["CMD-SHELL", "wget -qO- http://localhost:$${PORT:-3000}/api/health || exit 1"] diff --git a/server/src/db/repositories/CollabNotes.repository.ts b/server/src/db/repositories/CollabNotes.repository.ts index 4a67f5ba2..68da7eb88 100644 --- a/server/src/db/repositories/CollabNotes.repository.ts +++ b/server/src/db/repositories/CollabNotes.repository.ts @@ -192,12 +192,18 @@ export class CollabNotesRepository extends TrekRepository { // `TripFilesRepository`. // ------------------------------------------------------------------------- - /** CB6 (`formatNote`'s hydration read) — `SELECT id, filename, original_name, file_size, mime_type FROM trip_files WHERE note_id = ?`. */ + /** + * CB6 (`formatNote`'s hydration read) — `SELECT id, filename, original_name, + * file_size, mime_type FROM trip_files WHERE note_id = ? AND deleted_at IS + * NULL`. A note's file can be moved to the trash from the file manager; it + * leaves the note then, as a chat attachment leaves its message. + */ async listAttachmentsForNote(note_id: number): Promise { return await this.kysely() .selectFrom('trip_files') .select(['id', 'filename', 'original_name', 'file_size', 'mime_type']) .where('note_id', '=', note_id) + .where('deleted_at', 'is', null) .execute(); } diff --git a/server/src/mcp/index.ts b/server/src/mcp/index.ts index 9334c888b..87f4ad419 100644 --- a/server/src/mcp/index.ts +++ b/server/src/mcp/index.ts @@ -56,7 +56,9 @@ const sessionSweepInterval = setInterval(() => { for (const [key, entry] of rateLimitMap) { if (entry.windowStart < rateCutoff) rateLimitMap.delete(key); } - if (cleaned > 0 || sessions.size > 0) { + // Only when something was cleaned: logging every minute while any session + // is open wrote 1,440 lines a day into a log nothing rotates. + if (cleaned > 0) { console.log(`[MCP] Session sweep: cleaned ${cleaned}, active ${sessions.size}`); } }, 60 * 1000); // sweep every 1 minute diff --git a/server/src/websocket.ts b/server/src/websocket.ts index f273122ea..5d00a5cbb 100644 --- a/server/src/websocket.ts +++ b/server/src/websocket.ts @@ -4,7 +4,7 @@ * The implementation moved to nest/realtime: the connection lifecycle and the * handshake are RealtimeGateway, the wire protocol is TrekWsAdapter, and the * socket registry is ws-state.ts. This file stays because it is the import path - * the rest of the tree knows, including 115 test files that `vi.mock` it to + * the rest of the tree knows, including the more than 160 test files that `vi.mock` it to * assert on broadcasts. Repointing them all would be a large diff that proves * nothing, and this re-export keeps every one of those mocks working. * diff --git a/server/tests/integration/journey.test.ts b/server/tests/integration/journey.test.ts index 7d962769c..1d659a459 100644 --- a/server/tests/integration/journey.test.ts +++ b/server/tests/integration/journey.test.ts @@ -28,10 +28,6 @@ vi.mock('../../src/websocket', () => ({ broadcastToUser: vi.fn(), getOnlineUserIds: vi.fn(() => []), })); -vi.mock('../../src/services/memories/immichService', () => ({ - uploadToImmich: vi.fn(async () => null), - getImmichCredentials: vi.fn(() => null), -})); import { db as testDb } from '../../src/db/database'; import { buildApp } from '../../src/bootstrap'; diff --git a/server/tests/unit/nest/collab.service.test.ts b/server/tests/unit/nest/collab.service.test.ts index 88e49b2fc..96bf75526 100644 --- a/server/tests/unit/nest/collab.service.test.ts +++ b/server/tests/unit/nest/collab.service.test.ts @@ -874,6 +874,10 @@ describe('repository parity', () => { // CB6's narrow attachment projection. const legacyAttachments = testDb.prepare('SELECT id, filename, original_name, file_size, mime_type FROM trip_files WHERE note_id = ? ORDER BY id ASC').all(note.id); expect(await notesRepo.listAttachmentsForNote(note.id)).toEqual(legacyAttachments); + + // A file moved to the trash from the file manager leaves the note. + testDb.prepare("UPDATE trip_files SET deleted_at = CURRENT_TIMESTAMP WHERE note_id = ? AND original_name = 'b.png'").run(note.id); + expect((await notesRepo.listAttachmentsForNote(note.id)).map(a => a.original_name)).toEqual(['a.pdf']); }); it('COLLAB-REPO-002: CollabPollsRepository/CollabPollVotesRepository match the legacy queries for a multiple-choice poll voted on every option', async () => {