Reuse status dashboard message and simplify settings actions
This commit is contained in:
30
src/status-dashboard.test.ts
Normal file
30
src/status-dashboard.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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 });
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user