Restructure chat service, web routes, and add test suite

Split monolithic chat service into modular structure with Matrix client
support, extract web server into per-resource route modules, introduce
bootstrap/main-loop separation, and add comprehensive vitest coverage
across services, repositories, and utilities. Adds project docs and
CLAUDE.md agent instructions.
This commit is contained in:
jjbrunton committed 2026-05-21 01:32:10 +01:00
1 parent dfa0580859
commit 633bb0ec04
84 files changed
+19278 -3888

No files matched your search

+88
View File
@@ -0,0 +1,88 @@
import { Router } from 'express';
import type { WebContext } from '../context.js';
export function createMediaRouter(ctx: WebContext): Router {
const router = Router();
router.get('/media', async (_req, res) => {
if (!ctx.services.media) {
res.json([]);
return;
}
try {
const items = await ctx.services.media.getMediaItems();
// Build a roomId → current chat lookup so we can override stale
// stored usernames (e.g. items captured before username enrichment ran)
// and surface the recipient even for rooms that were originally
// ingested as `unknown`.
const liveChats = ctx.services.chat
? await ctx.services.chat.getCachedConversations()
: [];
const byRoom = new Map(liveChats.map((c) => [c.roomId ?? '', c] as const));
res.json(items.map((item) => {
const live = item.chatRoomId ? byRoom.get(item.chatRoomId) : undefined;
const liveName = live?.username && live.username !== 'unknown' ? live.username : null;
const username = liveName ?? item.chatUsername ?? null;
return {
...item,
chatUsername: username,
urlPath: `/media/${encodeURIComponent(item.relativePath)}`,
};
}));
} catch (error) {
ctx.logger.error('Failed to fetch media index:', error);
res.status(500).json({ error: String(error) });
}
});
router.get('/media/sync', (_req, res) => {
if (!ctx.services.mediaSync) {
res.status(503).json({ error: 'Media sync service not initialized' });
return;
}
res.json(ctx.services.mediaSync.getStatus());
});
router.post('/media/sync', async (req, res) => {
if (!ctx.services.mediaSync) {
res.status(503).json({ error: 'Media sync service not initialized' });
return;
}
try {
const full = req.body?.full === true;
const started = await ctx.services.mediaSync.triggerSync(full);
if (!started) {
res.json({ success: false, reason: 'Sync already in progress' });
return;
}
ctx.broadcaster.push('info', `Media sync triggered from dashboard (${full ? 'full' : 'incremental'})`);
res.json({ success: true });
} catch (error) {
ctx.logger.error('Media sync trigger failed:', error);
res.status(500).json({ error: String(error) });
}
});
router.post('/media/sync/full', async (_req, res) => {
if (!ctx.services.mediaSync) {
res.status(503).json({ error: 'Media sync service not initialized' });
return;
}
try {
const started = await ctx.services.mediaSync.triggerSync(true);
if (!started) {
res.json({ success: false, reason: 'Sync already in progress' });
return;
}
ctx.broadcaster.push('info', 'Full media sync triggered from dashboard (all conversations)');
res.json({ success: true });
} catch (error) {
ctx.logger.error('Full media sync trigger failed:', error);
res.status(500).json({ error: String(error) });
}
});
return router;
}