diff --git a/src/moderation/backlogSync.ts b/src/moderation/backlogSync.ts index 15d4ad0..5fc993b 100644 --- a/src/moderation/backlogSync.ts +++ b/src/moderation/backlogSync.ts @@ -90,14 +90,27 @@ export async function syncBacklogMessages( } const cutoffTime = Date.now() - config.BACKLOG_SYNC_HOURS * 60 * 60 * 1000; - await guild.channels.fetch().catch(() => null); + logger.info( + { guildId: guild.id, hours: config.BACKLOG_SYNC_HOURS }, + "Starting message backlog sync", + ); + logger.info({ guildId: guild.id }, "Fetching guild channels for backlog sync"); + await guild.channels.fetch().catch((error) => { + logger.warn( + { guildId: guild.id, error: error instanceof Error ? error.message : String(error) }, + "Failed to fetch guild channels before backlog sync", + ); + return null; + }); + + logger.info({ guildId: guild.id }, "Collecting watchable channels for backlog sync"); const channels = await collectWatchableChannels(guild); let total = 0; logger.info( { guildId: guild.id, channels: channels.length, hours: config.BACKLOG_SYNC_HOURS }, - "Starting message backlog sync", + "Watchable channels collected for backlog sync", ); for (const channel of channels) { diff --git a/src/muxer-queue.ts b/src/muxer-queue.ts index 7c2de87..2fa1cf3 100644 --- a/src/muxer-queue.ts +++ b/src/muxer-queue.ts @@ -108,6 +108,12 @@ function initializeDatabase(): SqliteDatabase { CREATE INDEX IF NOT EXISTS idx_attachments_channel ON attachments(channel_id); CREATE INDEX IF NOT EXISTS idx_attachments_message ON attachments(message_id); CREATE INDEX IF NOT EXISTS idx_attachments_status ON attachments(upload_status); + + CREATE TABLE IF NOT EXISTS ui_state ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_at INTEGER NOT NULL + ); `); const migrations = [ @@ -141,6 +147,28 @@ function getDatabase(): SqliteDatabase { export { getDatabase }; +export function getPersistedValue(key: string, fallback: T): T { + const row = getDatabase() + .prepare("SELECT value FROM ui_state WHERE key = ?") + .get(key) as { value: string } | undefined; + if (!row) return fallback; + try { + return JSON.parse(row.value) as T; + } catch { + return fallback; + } +} + +export function setPersistedValue(key: string, value: unknown): void { + getDatabase() + .prepare(` + INSERT INTO ui_state (key, value, updated_at) + VALUES (?, ?, ?) + ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at + `) + .run(key, JSON.stringify(value), Date.now()); +} + export async function enqueueMuxerJob(data: MuxerJobData): Promise { try { const database = getDatabase(); diff --git a/src/webserver.ts b/src/webserver.ts index 66a9bbf..c8291b2 100644 --- a/src/webserver.ts +++ b/src/webserver.ts @@ -3,7 +3,6 @@ import express from "express"; import helmet from "helmet"; import http from "http"; import path from "path"; -import pinoHttp from "pino-http"; import * as prism from "prism-media"; import { WebSocketServer } from "ws"; import { AppError } from "./errors"; @@ -11,7 +10,7 @@ import { createChildLogger, logger } from "./logger"; import { getMetrics, uptimeGauge } from "./metrics"; import { discordPlayer } from "./player"; import type { VoiceController } from "./voiceController"; -import { getDatabase } from "./muxer-queue"; +import { getDatabase, getPersistedValue, setPersistedValue } from "./muxer-queue"; import { getMessagesByChannel, getAttachmentsByChannel } from "./moderation/messageStore"; const wsLogger = createChildLogger("webserver"); @@ -31,7 +30,7 @@ interface SharedUIState { isStreaming: boolean; } -const sharedUIState: SharedUIState = { +const defaultSharedUIState: SharedUIState = { selectedGuild: "", selectedVoiceChannel: "", selectedTextChannel: "", @@ -40,6 +39,8 @@ const sharedUIState: SharedUIState = { isStreaming: false, }; +const sharedUIState: SharedUIState = getPersistedValue("web-ui-state", defaultSharedUIState); + function getSharedUIState(): SharedUIState { return { ...sharedUIState }; } @@ -73,6 +74,7 @@ function patchSharedUIState(patch: Partial) { if (typeof patch.isStreaming === "boolean") { sharedUIState.isStreaming = patch.isStreaming; } + setPersistedValue("web-ui-state", sharedUIState); broadcastUIState(); return getSharedUIState(); } @@ -121,8 +123,21 @@ export function startWebserver( }), ); - // HTTP request logging - app.use(pinoHttp({ logger })); + app.use((req, res, next) => { + if (req.path.startsWith("/api/")) { + res.set("Cache-Control", "no-store"); + } + res.on("finish", () => { + if (req.originalUrl.startsWith("/.well-known/appspecific/")) return; + if (res.statusCode >= 400) { + logger.error( + { method: req.method, url: req.originalUrl, statusCode: res.statusCode }, + "HTTP request failed", + ); + } + }); + next(); + }); app.use(express.json()); app.use(express.static(path.join(__dirname, "../public")));