From 289c17808f7b732e06923491ab7200dba6cbc782 Mon Sep 17 00:00:00 2001 From: gunshiz Date: Wed, 9 Sep 2026 17:38:40 +0700 Subject: [PATCH] feat(discord) : sync latest Lunaris version and report status --- .env.example | 1 + README.md | 8 ++-- backend/discord.ts | 51 ++++++++++++++++++++----- k8s/base/discord-worker-deployment.yaml | 2 + tests/deployment-contract.test.ts | 1 + 5 files changed, 51 insertions(+), 12 deletions(-) diff --git a/.env.example b/.env.example index 648c672..1734305 100644 --- a/.env.example +++ b/.env.example @@ -26,6 +26,7 @@ CATALOG_SYNC_CONCURRENCY=12 # Discord-triggered Lunaris catalog sync DISCORD_BOT_TOKEN=replace-with-discord-bot-token DISCORD_CHANNEL_ID=replace-with-private-channel-id +DISCORD_LOG_CHANNEL_ID=1547193755772125184 # S3-compatible private media storage S3_ENDPOINT=https://s3.example.internal diff --git a/README.md b/README.md index f32308d..0e00556 100644 --- a/README.md +++ b/README.md @@ -122,9 +122,11 @@ source of truth. The Discord catalog worker watches the private channel configured by `DISCORD_CHANNEL_ID`. It parses Lunaris `Version Change` announcements and -synchronizes that exact version. On startup it checks recent channel history, -so it does not need to poll Lunaris while idle. Enable the Discord Message -Content intent and grant the bot View Channel and Read Message History. +then fetches and synchronizes the latest version reported by Lunaris. On startup +it checks recent channel history, so it does not need to poll Lunaris while idle. +Worker and sync results are sent to `DISCORD_LOG_CHANNEL_ID`. Enable the Discord +Message Content intent and grant the bot View Channel, Read Message History, and +Send Messages in the configured channels. Public snapshots use a shared Redis-backed Next.js cache. Admin routes remain dynamic. SSE streams send a 90-second heartbeat and close after 30 minutes so diff --git a/backend/discord.ts b/backend/discord.ts index 02c97d1..635918e 100644 --- a/backend/discord.ts +++ b/backend/discord.ts @@ -7,7 +7,10 @@ import { } from "discord.js"; import { closeDb } from "@/db/client"; -import { processCatalogSync } from "@/lib/catalog/sync"; +import { + getLatestLunarisVersion, + processCatalogSync, +} from "@/lib/catalog/sync"; import { compareLunarisVersions, parseLunarisVersionChange, @@ -54,6 +57,7 @@ function wait(milliseconds: number, signal: AbortSignal): Promise { const token = required("DISCORD_BOT_TOKEN"); const channelId = required("DISCORD_CHANNEL_ID"); +const logChannelId = required("DISCORD_LOG_CHANNEL_ID"); const shutdown = new AbortController(); const client = new Client({ intents: [ @@ -66,40 +70,68 @@ const client = new Client({ let pendingVersion: string | null = null; let runner: Promise | null = null; -async function syncWithRetry(version: string): Promise { +async function notify(event: string, details: Record = {}): Promise { + try { + const channel = await client.channels.fetch(logChannelId); + if (!channel?.isSendable()) { + log("notification_failed", { event, error: "Discord log channel is not sendable." }); + return; + } + await channel.send(`**Lunaris catalog worker — ${event}**\n\`\`\`json\n${JSON.stringify(details, null, 2)}\n\`\`\``); + } catch (cause) { + log("notification_failed", { + event, + error: cause instanceof Error ? cause.message : String(cause), + }); + } +} + +async function syncWithRetry(announcedVersion: string): Promise { for (let attempt = 0; attempt < RETRY_DELAYS_MS.length; attempt += 1) { if (shutdown.signal.aborted) return; if ( pendingVersion && - compareLunarisVersions(pendingVersion, version) > 0 + compareLunarisVersions(pendingVersion, announcedVersion) > 0 ) { - log("sync_superseded", { version, pendingVersion }); + log("sync_superseded", { announcedVersion, pendingVersion }); return; } if (!(await wait(RETRY_DELAYS_MS[attempt], shutdown.signal))) return; if ( pendingVersion && - compareLunarisVersions(pendingVersion, version) > 0 + compareLunarisVersions(pendingVersion, announcedVersion) > 0 ) { - log("sync_superseded", { version, pendingVersion }); + log("sync_superseded", { announcedVersion, pendingVersion }); return; } try { - log("sync_started", { version, attempt: attempt + 1 }); + const version = await getLatestLunarisVersion(); + const details = { announcedVersion, version, attempt: attempt + 1 }; + log("sync_started", details); + await notify("sync started", details); const result = await processCatalogSync(version); log(result === "synced" ? "sync_completed" : "sync_ignored", { + announcedVersion, version, reason: result === "unchanged" ? "current-or-older" : undefined, }); + await notify(result === "synced" ? "sync completed" : "already current", { + announcedVersion, + version, + }); return; } catch (cause) { const message = cause instanceof Error ? cause.message : String(cause); if (attempt === RETRY_DELAYS_MS.length - 1) { - log("sync_failed", { version, attempts: attempt + 1, error: message }); + const details = { announcedVersion, attempts: attempt + 1, error: message }; + log("sync_failed", details); + await notify("sync failed", details); return; } - log("sync_retrying", { version, attempt: attempt + 1, error: message }); + const details = { announcedVersion, attempt: attempt + 1, error: message }; + log("sync_retrying", details); + await notify("sync retrying", details); } } } @@ -153,6 +185,7 @@ try { await Promise.race([ready, stopped]); if (!shutdown.signal.aborted) { log("discord_ready", { userId: client.user?.id, channelId }); + await notify("worker ready", { userId: client.user?.id, channelId }); const channel = await client.channels.fetch(channelId); if ( diff --git a/k8s/base/discord-worker-deployment.yaml b/k8s/base/discord-worker-deployment.yaml index a570670..1471d9d 100644 --- a/k8s/base/discord-worker-deployment.yaml +++ b/k8s/base/discord-worker-deployment.yaml @@ -48,6 +48,8 @@ spec: configMapKeyRef: name: buzz-sheet-config key: NEXT_DEPLOYMENT_ID + - name: DISCORD_LOG_CHANNEL_ID + value: "1547193755772125184" resources: requests: cpu: 100m diff --git a/tests/deployment-contract.test.ts b/tests/deployment-contract.test.ts index f50b49f..20838cf 100644 --- a/tests/deployment-contract.test.ts +++ b/tests/deployment-contract.test.ts @@ -155,6 +155,7 @@ describe("environment template contract", () => { "NEXT_DEPLOYMENT_ID", "DISCORD_BOT_TOKEN", "DISCORD_CHANNEL_ID", + "DISCORD_LOG_CHANNEL_ID", ]; for (const key of requiredKeys) {