feat: status dashboard categories/ordering + live usage dashboard
- Add getChannelMeta to Discord channel (batch guild fetch) - Status dashboard groups by Discord category, sorts by position - Add live usage dashboard (CPU, memory, disk, uptime) with message edit - Fix 4 failing discord.test.ts tests (sendMessage format, image fetch mock)
This commit is contained in:
@@ -494,13 +494,38 @@ describe('DiscordChannel', () => {
|
||||
// --- Attachments ---
|
||||
|
||||
describe('attachments', () => {
|
||||
let originalFetch: typeof globalThis.fetch;
|
||||
|
||||
beforeEach(() => {
|
||||
originalFetch = globalThis.fetch;
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
arrayBuffer: () => Promise.resolve(new ArrayBuffer(8)),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
it('stores image attachment with placeholder', async () => {
|
||||
const opts = createTestOpts();
|
||||
const channel = new DiscordChannel('test-token', opts);
|
||||
await channel.connect();
|
||||
|
||||
const attachments = new Map([
|
||||
['att1', { name: 'photo.png', contentType: 'image/png' }],
|
||||
[
|
||||
'att1',
|
||||
{
|
||||
id: 'att1',
|
||||
name: 'photo.png',
|
||||
contentType: 'image/png',
|
||||
url: 'https://cdn.example.com/photo.png',
|
||||
},
|
||||
],
|
||||
]);
|
||||
const msg = createMessage({
|
||||
content: '',
|
||||
@@ -512,7 +537,7 @@ describe('DiscordChannel', () => {
|
||||
expect(opts.onMessage).toHaveBeenCalledWith(
|
||||
'dc:1234567890123456',
|
||||
expect.objectContaining({
|
||||
content: '[Image: photo.png]',
|
||||
content: expect.stringMatching(/^\[Image: .+\.png\]$/),
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -569,7 +594,15 @@ describe('DiscordChannel', () => {
|
||||
await channel.connect();
|
||||
|
||||
const attachments = new Map([
|
||||
['att1', { name: 'photo.jpg', contentType: 'image/jpeg' }],
|
||||
[
|
||||
'att1',
|
||||
{
|
||||
id: 'att1',
|
||||
name: 'photo.jpg',
|
||||
contentType: 'image/jpeg',
|
||||
url: 'https://cdn.example.com/photo.jpg',
|
||||
},
|
||||
],
|
||||
]);
|
||||
const msg = createMessage({
|
||||
content: 'Check this out',
|
||||
@@ -581,7 +614,7 @@ describe('DiscordChannel', () => {
|
||||
expect(opts.onMessage).toHaveBeenCalledWith(
|
||||
'dc:1234567890123456',
|
||||
expect.objectContaining({
|
||||
content: 'Check this out\n[Image: photo.jpg]',
|
||||
content: expect.stringMatching(/^Check this out\n\[Image: .+\.jpg\]$/),
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -592,7 +625,15 @@ describe('DiscordChannel', () => {
|
||||
await channel.connect();
|
||||
|
||||
const attachments = new Map([
|
||||
['att1', { name: 'a.png', contentType: 'image/png' }],
|
||||
[
|
||||
'att1',
|
||||
{
|
||||
id: 'att1',
|
||||
name: 'a.png',
|
||||
contentType: 'image/png',
|
||||
url: 'https://cdn.example.com/a.png',
|
||||
},
|
||||
],
|
||||
['att2', { name: 'b.txt', contentType: 'text/plain' }],
|
||||
]);
|
||||
const msg = createMessage({
|
||||
@@ -605,7 +646,7 @@ describe('DiscordChannel', () => {
|
||||
expect(opts.onMessage).toHaveBeenCalledWith(
|
||||
'dc:1234567890123456',
|
||||
expect.objectContaining({
|
||||
content: '[Image: a.png]\n[File: b.txt]',
|
||||
content: expect.stringMatching(/^\[Image: .+\.png\]\n\[File: b\.txt\]$/),
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -702,8 +743,14 @@ describe('DiscordChannel', () => {
|
||||
await channel.sendMessage('dc:1234567890123456', longText);
|
||||
|
||||
expect(mockChannel.send).toHaveBeenCalledTimes(2);
|
||||
expect(mockChannel.send).toHaveBeenNthCalledWith(1, 'x'.repeat(2000));
|
||||
expect(mockChannel.send).toHaveBeenNthCalledWith(2, 'x'.repeat(1000));
|
||||
expect(mockChannel.send).toHaveBeenNthCalledWith(1, {
|
||||
content: 'x'.repeat(2000),
|
||||
files: undefined,
|
||||
});
|
||||
expect(mockChannel.send).toHaveBeenNthCalledWith(2, {
|
||||
content: 'x'.repeat(1000),
|
||||
files: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ import { registerChannel, ChannelOpts } from './registry.js';
|
||||
import {
|
||||
AgentType,
|
||||
Channel,
|
||||
ChannelMeta,
|
||||
OnChatMetadata,
|
||||
OnInboundMessage,
|
||||
RegisteredGroup,
|
||||
@@ -438,6 +439,59 @@ export class DiscordChannel implements Channel {
|
||||
}
|
||||
}
|
||||
|
||||
async getChannelMeta(jids: string[]): Promise<Map<string, ChannelMeta>> {
|
||||
const result = new Map<string, ChannelMeta>();
|
||||
if (!this.client) return result;
|
||||
|
||||
const dcJids = jids.filter((j) => j.startsWith('dc:'));
|
||||
if (dcJids.length === 0) return result;
|
||||
|
||||
const channelIdToJid = new Map<string, string>();
|
||||
for (const jid of dcJids) {
|
||||
channelIdToJid.set(jid.replace(/^dc:/, ''), jid);
|
||||
}
|
||||
|
||||
try {
|
||||
// Fetch one channel to discover its guild, then batch-fetch all channels
|
||||
const firstId = dcJids[0].replace(/^dc:/, '');
|
||||
const firstChannel = await this.client.channels.fetch(firstId);
|
||||
if (!firstChannel || !('guild' in firstChannel)) return result;
|
||||
|
||||
const guild = (firstChannel as TextChannel).guild;
|
||||
const allChannels = await guild.channels.fetch();
|
||||
|
||||
for (const [id, channel] of allChannels) {
|
||||
const jid = channelIdToJid.get(id);
|
||||
if (!jid || !channel) continue;
|
||||
result.set(jid, {
|
||||
position: channel.position,
|
||||
category: channel.parent?.name || '',
|
||||
categoryPosition: channel.parent?.position ?? 999,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Fallback: individual fetches
|
||||
for (const jid of dcJids) {
|
||||
try {
|
||||
const channelId = jid.replace(/^dc:/, '');
|
||||
const channel = await this.client.channels.fetch(channelId);
|
||||
if (channel && 'position' in channel) {
|
||||
const tc = channel as TextChannel;
|
||||
result.set(jid, {
|
||||
position: tc.position,
|
||||
category: tc.parent?.name || '',
|
||||
categoryPosition: tc.parent?.position ?? 999,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
/* skip inaccessible channels */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async editMessage(
|
||||
jid: string,
|
||||
messageId: string,
|
||||
|
||||
Reference in New Issue
Block a user