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:
1 parent
633bb0ec04
commit
2260388dc8
15 files changed
+675
-41
No files matched your search
@@ -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 });
|
||||
|
||||
Reference in new issue
Block a user