Blur gallery images by default, sync progress UI, and add chat/monitor tests

- Blur NSFW media gallery images until hover/reveal
- Surface chat sync progress in the dashboard header
- Extend chat service, cache, and monitor with delta-media handling
- Add tests for chat cooldown, delta media, and monitor flows

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jjbruntonandClaude Opus 4.7 committed 2026-05-22 10:01:09 +01:00
1 parent 633bb0ec04
commit 2260388dc8
15 files changed
+675 -41

No files matched your search

+47 -1
View File
@@ -1,7 +1,7 @@
import fs from 'fs/promises';
import path from 'path';
import type { Logger } from '../../utils/logger.js';
import type { ActiveChat, ChatCacheFile } from './types.js';
import type { ActiveChat, ChatCacheFile, InvitationCooldown } from './types.js';
export interface SyncMarkers {
nextBatch: string | null;
@@ -22,6 +22,7 @@ export class ChatCache {
private nextBatch: string | null = null;
private lastFullSyncAt: number | null = null;
private lastDeltaSyncAt: number | null = null;
private cooldown: InvitationCooldown | null = null;
constructor(private logger: Logger, filePath?: string) {
this.filePath = filePath ?? path.join(process.cwd(), 'data', 'chat-cache.json');
@@ -61,6 +62,14 @@ export class ChatCache {
this.lastDeltaSyncAt = at;
}
getCooldown(): InvitationCooldown | null {
return this.cooldown;
}
setCooldown(cooldown: InvitationCooldown | null): void {
this.cooldown = cooldown;
}
async load(): Promise<void> {
try {
const raw = await fs.readFile(this.filePath, 'utf-8');
@@ -74,6 +83,7 @@ export class ChatCache {
this.nextBatch = parsed.nextBatch ?? null;
this.lastFullSyncAt = parsed.lastFullSyncAt ?? null;
this.lastDeltaSyncAt = parsed.lastDeltaSyncAt ?? null;
this.cooldown = parsed.cooldown ?? null;
this.logger.info(
`Loaded ${parsed.chats.length} chats from cache` +
(this.lastFullSyncAt
@@ -89,6 +99,41 @@ export class ChatCache {
}
}
/**
* Persist only the cooldown field, preserving whatever chats / sync markers
* are already on disk. Used when a rate-limit fires before the in-memory
* cache has been populated — a plain `save()` would clobber a prior full
* sync with `chats: []` and null markers.
*/
async persistCooldown(): Promise<void> {
let existing: ChatCacheFile | null = null;
try {
const raw = await fs.readFile(this.filePath, 'utf-8');
existing = JSON.parse(raw) as ChatCacheFile;
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
this.logger.warn('Failed to read chat cache for cooldown update:', error);
}
}
const payload: ChatCacheFile = existing
? { ...existing, cooldown: this.cooldown, savedAt: Date.now() }
: {
version: 1,
savedAt: Date.now(),
chats: [],
nextBatch: null,
lastFullSyncAt: null,
lastDeltaSyncAt: null,
cooldown: this.cooldown,
};
try {
await fs.mkdir(path.dirname(this.filePath), { recursive: true });
await fs.writeFile(this.filePath, JSON.stringify(payload, null, 2), 'utf-8');
} catch (error) {
this.logger.warn('Failed to save chat cache cooldown:', error);
}
}
async save(chats: ActiveChat[] = this.chats): Promise<void> {
const payload: ChatCacheFile = {
version: 1,
@@ -97,6 +142,7 @@ export class ChatCache {
nextBatch: this.nextBatch,
lastFullSyncAt: this.lastFullSyncAt,
lastDeltaSyncAt: this.lastDeltaSyncAt,
cooldown: this.cooldown,
};
try {
await fs.mkdir(path.dirname(this.filePath), { recursive: true });