Reuse status dashboard message and simplify settings actions

This commit is contained in:
Eyejoker
2026-05-02 02:06:20 +09:00
committed by GitHub
parent 7576bcd3ff
commit 490ae7d324
8 changed files with 230 additions and 103 deletions

View File

@@ -0,0 +1,30 @@
import fs from 'fs';
import path from 'path';
import { afterEach, describe, expect, it } from 'vitest';
import { CACHE_DIR } from './config.js';
import {
readDashboardStatusMessageId,
writeDashboardStatusMessageId,
} from './status-dashboard.js';
afterEach(() => {
const messagesDir = path.join(CACHE_DIR, 'status-dashboard', 'messages');
fs.rmSync(path.join(messagesDir, 'test-status-channel.json'), {
force: true,
});
});
describe('dashboard status message id state', () => {
it('persists the tracked Discord status message id across restarts', async () => {
expect(readDashboardStatusMessageId('test-status-channel')).toBeNull();
writeDashboardStatusMessageId('test-status-channel', '1499810000000000000');
expect(readDashboardStatusMessageId('test-status-channel')).toBe(
'1499810000000000000',
);
expect(readDashboardStatusMessageId('other-channel')).toBeNull();
});
});

View File

@@ -2,7 +2,7 @@ import fs from 'fs';
import path from 'path';
import { CACHE_DIR } from './config.js';
import { writeJsonFile } from './utils.js';
import { readJsonFile, writeJsonFile } from './utils.js';
import type { GroupStatus } from './group-queue.js';
import type { AgentType } from './types.js';
@@ -37,6 +37,61 @@ export interface StatusSnapshot {
}
const STATUS_SNAPSHOT_DIR = path.join(CACHE_DIR, 'status-dashboard');
const STATUS_MESSAGE_STATE_DIR = path.join(STATUS_SNAPSHOT_DIR, 'messages');
interface StatusMessageState {
messageId: string;
statusChannelId: string;
updatedAt: string;
}
function sanitizeStatusChannelId(statusChannelId: string): string {
return statusChannelId.replace(/[^a-zA-Z0-9_-]/g, '_');
}
function getStatusMessageStatePath(statusChannelId: string): string {
return path.join(
STATUS_MESSAGE_STATE_DIR,
`${sanitizeStatusChannelId(statusChannelId)}.json`,
);
}
export function readDashboardStatusMessageId(
statusChannelId: string,
): string | null {
if (!statusChannelId) return null;
const parsed = readJsonFile<StatusMessageState>(
getStatusMessageStatePath(statusChannelId),
);
if (
parsed?.statusChannelId !== statusChannelId ||
typeof parsed.messageId !== 'string' ||
parsed.messageId.trim() === ''
) {
return null;
}
return parsed.messageId;
}
export function writeDashboardStatusMessageId(
statusChannelId: string,
messageId: string,
): void {
if (!statusChannelId || !messageId) return;
fs.mkdirSync(STATUS_MESSAGE_STATE_DIR, { recursive: true });
const targetPath = getStatusMessageStatePath(statusChannelId);
const tempPath = `${targetPath}.tmp`;
writeJsonFile(
tempPath,
{
messageId,
statusChannelId,
updatedAt: new Date().toISOString(),
} satisfies StatusMessageState,
true,
);
fs.renameSync(tempPath, targetPath);
}
export function writeStatusSnapshot(snapshot: StatusSnapshot): void {
fs.mkdirSync(STATUS_SNAPSHOT_DIR, { recursive: true });

View File

@@ -5,6 +5,7 @@ import {
buildWebUsageRowsForSnapshot,
formatStatusHeader,
renderUsageTable,
shouldPurgeDashboardChannelOnStart,
summarizeWatcherTasks,
} from './unified-dashboard.js';
@@ -61,6 +62,32 @@ describe('formatStatusHeader', () => {
});
});
describe('shouldPurgeDashboardChannelOnStart', () => {
it('keeps the existing status message when a stored message id exists', () => {
expect(
shouldPurgeDashboardChannelOnStart({
purgeOnStart: true,
storedMessageId: 'status-message-1',
}),
).toBe(false);
});
it('purges only when explicitly requested and no stored message id exists', () => {
expect(
shouldPurgeDashboardChannelOnStart({
purgeOnStart: true,
storedMessageId: null,
}),
).toBe(true);
expect(
shouldPurgeDashboardChannelOnStart({
purgeOnStart: false,
storedMessageId: null,
}),
).toBe(false);
});
});
describe('renderUsageTable', () => {
const claudeRow: UsageRow = {
name: 'Claude pro',

View File

@@ -52,7 +52,9 @@ import type { GroupQueue } from './group-queue.js';
import { logger } from './logger.js';
import { isWatchCiTask } from './task-watch-status.js';
import {
readDashboardStatusMessageId,
readStatusSnapshots,
writeDashboardStatusMessageId,
writeStatusSnapshot,
} from './status-dashboard.js';
import type {
@@ -169,6 +171,13 @@ export async function purgeDashboardChannel(
}
}
export function shouldPurgeDashboardChannelOnStart(args: {
purgeOnStart?: boolean;
storedMessageId: string | null;
}): boolean {
return args.purgeOnStart === true && !args.storedMessageId;
}
async function refreshChannelMeta(
opts: UnifiedDashboardOptions,
): Promise<void> {
@@ -708,8 +717,17 @@ export async function startUnifiedDashboard(
const isRenderer = opts.serviceAgentType === 'claude-code';
const statusJid = `dc:${opts.statusChannelId}`;
if (isRenderer) {
statusMessageId = readDashboardStatusMessageId(opts.statusChannelId);
}
if (isRenderer && opts.purgeOnStart) {
if (
isRenderer &&
shouldPurgeDashboardChannelOnStart({
purgeOnStart: opts.purgeOnStart,
storedMessageId: statusMessageId,
})
) {
await purgeDashboardChannel(opts);
}
@@ -746,15 +764,28 @@ export async function startUnifiedDashboard(
},
'Dashboard content empty, skipping render',
);
statusMessageId = null;
return;
}
if (statusMessageId && channel.editMessage) {
await channel.editMessage(statusJid, statusMessageId, content);
} else if (channel.sendAndTrack) {
try {
await channel.editMessage(statusJid, statusMessageId, content);
writeDashboardStatusMessageId(opts.statusChannelId, statusMessageId);
} catch (err) {
logger.warn(
{ err, messageId: statusMessageId },
'Dashboard status message edit failed; sending a fresh tracked message',
);
statusMessageId = null;
}
}
if (!statusMessageId && channel.sendAndTrack) {
const id = await channel.sendAndTrack(statusJid, content);
if (id) statusMessageId = id;
if (id) {
statusMessageId = id;
writeDashboardStatusMessageId(opts.statusChannelId, id);
}
}
if (!dashboardUpdateLogged) {
logger.info(