Skip to content

Commit 92c33fa

Browse files
committed
feat: enhance daemon status management and UI integration
- Introduced a new daemon status management system in the BackendsGateway, allowing for real-time updates and broadcasting of daemon status to connected clients. - Updated the footer and layout components to display daemon status, including online status and ping metrics. - Refactored the settings and cron pages to integrate daemon status checks and UI elements for improved user experience. - Removed deprecated debug network functionality and streamlined related components for better maintainability.
1 parent ce52628 commit 92c33fa

10 files changed

Lines changed: 612 additions & 116 deletions

File tree

apps/api/src/core/backends/backends.gateway.ts

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import { Logger, UnauthorizedException } from '@nestjs/common';
2-
import { OnGatewayConnection, OnGatewayDisconnect, WebSocketGateway, WebSocketServer } from '@nestjs/websockets';
2+
import {
3+
ConnectedSocket,
4+
OnGatewayConnection,
5+
OnGatewayDisconnect,
6+
SubscribeMessage,
7+
WebSocketGateway,
8+
WebSocketServer,
9+
} from '@nestjs/websockets';
310
import { hash } from 'crypto';
411
import { Server, Socket } from 'socket.io';
512
import { Public } from '~/_common/decorators/public.decorator';
@@ -10,7 +17,15 @@ import { BackendsService } from './backends.service';
1017

1118
type JobChannel = 'job:added' | 'job:completed' | 'job:failed' | 'job:progress' | 'job:active';
1219

20+
type DaemonStatusPayload = {
21+
online: boolean;
22+
pingMs: number | null;
23+
error?: string;
24+
version?: string;
25+
};
26+
1327
const IDENTITY_JOB_TYPES = [ActionType.IDENTITY_UPDATE, ActionType.IDENTITY_CREATE, ActionType.IDENTITY_DELETE];
28+
const DAEMON_STATUS_INTERVAL_MS = 20_000;
1429

1530
@Public()
1631
@WebSocketGateway({
@@ -20,6 +35,9 @@ const IDENTITY_JOB_TYPES = [ActionType.IDENTITY_UPDATE, ActionType.IDENTITY_CREA
2035
export class BackendsGateway implements OnGatewayConnection, OnGatewayDisconnect {
2136
private readonly logger = new Logger(BackendsGateway.name);
2237
private readonly clientSubscriptions = new Map<string, () => void>();
38+
private lastDaemonStatus: DaemonStatusPayload | null = null;
39+
private daemonStatusInterval: NodeJS.Timeout | null = null;
40+
private daemonStatusPingInFlight = false;
2341

2442
@WebSocketServer()
2543
server: Server;
@@ -50,6 +68,10 @@ export class BackendsGateway implements OnGatewayConnection, OnGatewayDisconnect
5068

5169
const cleanup = this.subscribeClient(client);
5270
this.clientSubscriptions.set(client.id, cleanup);
71+
this.ensureDaemonStatusWatcher();
72+
if (this.lastDaemonStatus) {
73+
this.emitDaemonStatus(client, this.lastDaemonStatus);
74+
}
5375
this.logger.debug(`WebSocket connected: ${client.id}`);
5476
} catch {
5577
client.disconnect(true);
@@ -60,9 +82,20 @@ export class BackendsGateway implements OnGatewayConnection, OnGatewayDisconnect
6082
const cleanup = this.clientSubscriptions.get(client.id);
6183
cleanup?.();
6284
this.clientSubscriptions.delete(client.id);
85+
this.stopDaemonStatusWatcherIfIdle();
6386
this.logger.debug(`WebSocket disconnected: ${client.id}`);
6487
}
6588

89+
@SubscribeMessage('daemon:status')
90+
public async handleDaemonStatusRequest(@ConnectedSocket() client: Socket): Promise<void> {
91+
if (this.lastDaemonStatus) {
92+
this.emitDaemonStatus(client, this.lastDaemonStatus);
93+
return;
94+
}
95+
96+
await this.refreshDaemonStatus();
97+
}
98+
6699
private subscribeClient(client: Socket): () => void {
67100
const fireMessage = (channel: JobChannel, message: unknown) => {
68101
try {
@@ -122,4 +155,66 @@ export class BackendsGateway implements OnGatewayConnection, OnGatewayDisconnect
122155
this.backendsService.queueEvents.off('failed', onFailed);
123156
};
124157
}
158+
159+
private ensureDaemonStatusWatcher(): void {
160+
if (this.daemonStatusInterval) {
161+
return;
162+
}
163+
164+
void this.refreshDaemonStatus();
165+
this.daemonStatusInterval = setInterval(() => void this.refreshDaemonStatus(), DAEMON_STATUS_INTERVAL_MS);
166+
}
167+
168+
private stopDaemonStatusWatcherIfIdle(): void {
169+
const connectedClients = this.server?.sockets?.sockets?.size ?? 0;
170+
if (connectedClients > 0) {
171+
return;
172+
}
173+
174+
if (this.daemonStatusInterval) {
175+
clearInterval(this.daemonStatusInterval);
176+
this.daemonStatusInterval = null;
177+
}
178+
}
179+
180+
private async refreshDaemonStatus(): Promise<void> {
181+
if (this.daemonStatusPingInFlight) {
182+
return;
183+
}
184+
185+
this.daemonStatusPingInFlight = true;
186+
try {
187+
const status = await this.backendsService.pingDaemon();
188+
this.lastDaemonStatus = status;
189+
this.broadcastDaemonStatus(status);
190+
} catch (err) {
191+
this.logger.error(`Daemon status refresh failed: ${err}`, BackendsGateway.name);
192+
} finally {
193+
this.daemonStatusPingInFlight = false;
194+
}
195+
}
196+
197+
private emitDaemonStatus(client: Socket, status: DaemonStatusPayload): void {
198+
try {
199+
client.emit('message', { channel: 'daemon:status', payload: status });
200+
this.logger.debug(`Emit to <daemon:status> with data <${JSON.stringify(status)}>`, BackendsGateway.name);
201+
} catch (err) {
202+
this.logger.error(
203+
`Emit error from <daemon:status> with data <${JSON.stringify(status)}>. Error: ${err}`,
204+
BackendsGateway.name,
205+
);
206+
}
207+
}
208+
209+
private broadcastDaemonStatus(status: DaemonStatusPayload): void {
210+
try {
211+
this.server.emit('message', { channel: 'daemon:status', payload: status });
212+
this.logger.debug(`Broadcast <daemon:status> with data <${JSON.stringify(status)}>`, BackendsGateway.name);
213+
} catch (err) {
214+
this.logger.error(
215+
`Broadcast error from <daemon:status> with data <${JSON.stringify(status)}>. Error: ${err}`,
216+
BackendsGateway.name,
217+
);
218+
}
219+
}
125220
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<template lang="pug">
2+
template(v-if="debug")
3+
q-btn(
4+
flat
5+
round
6+
dense
7+
icon="mdi-bug-outline"
8+
:color="color"
9+
:size="size"
10+
@click="openDebugNetwork"
11+
)
12+
q-tooltip.text-body2(anchor="top middle" self="bottom middle") Debug Application
13+
q-btn(
14+
flat
15+
round
16+
dense
17+
icon="mdi-lan-connect"
18+
color="orange-8"
19+
:size="size"
20+
@click="openDebugSocket"
21+
)
22+
q-tooltip.text-body2(anchor="top middle" self="bottom middle") Debug Socket.IO
23+
</template>
24+
25+
<script lang="ts">
26+
export default defineNuxtComponent({
27+
name: 'CoreAppDebugButtonsComponent',
28+
props: {
29+
size: {
30+
type: String,
31+
default: undefined,
32+
},
33+
color: {
34+
type: String,
35+
default: 'orange',
36+
},
37+
},
38+
setup() {
39+
const { debug } = useDebug()
40+
const { openDebugNetwork, openDebugSocket } = useAppDebugPanels()
41+
42+
return {
43+
debug,
44+
openDebugNetwork,
45+
openDebugSocket,
46+
}
47+
},
48+
})
49+
</script>
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<template lang="pug">
2+
q-dialog(v-model="debugDialog" @show="onDebugDialogShow")
3+
q-card(style="min-width: 340px; max-width: min(560px, 92vw)")
4+
q-toolbar.bg-orange-8.text-white(dense)
5+
q-toolbar-title.text-subtitle2 Debug Application
6+
q-btn(icon="mdi-close" flat round dense v-close-popup)
7+
q-separator
8+
q-card-section.q-pa-md
9+
q-linear-progress(v-if="debugNetworkLoading" indeterminate color="orange" class="q-mb-md")
10+
pre.text-body2.q-ma-none(v-else style="white-space: pre-wrap; word-break: break-all; font-family: ui-monospace, monospace;") {{ debugNetworkFormatted }}
11+
q-dialog(
12+
v-model="debugSocketDialog"
13+
seamless
14+
position="bottom"
15+
allow-focus-outside
16+
no-route-dismiss
17+
no-shake
18+
no-refocus
19+
@show="onDebugSocketDialogShow"
20+
)
21+
q-card.column.no-wrap.debug-socket-panel(:style="debugSocketPanelStyle")
22+
.debug-socket-resize-handle(@mousedown.prevent="startSocketDebugResize")
23+
q-icon(name="mdi-drag-horizontal" size="18px" color="grey-6")
24+
q-toolbar.bg-orange-8.text-white(dense)
25+
q-toolbar-title.text-subtitle2 Debug Socket.IO
26+
q-space
27+
q-btn(icon="mdi-delete-outline" flat round dense @click="clearSocketDebugEntries")
28+
q-tooltip Effacer le journal
29+
q-btn(icon="mdi-close" flat round dense v-close-popup)
30+
q-tooltip Fermer
31+
q-separator
32+
q-card-section.col.q-pa-none(style="overflow: auto; min-height: 0;")
33+
.text-caption.text-grey-7.q-pa-md(v-if="socketDebugEntries.length === 0") Aucun événement Socket.IO enregistré.
34+
q-list.q-pa-none(v-else bordered separator)
35+
q-expansion-item(
36+
v-for="entry in socketDebugEntries"
37+
:key="entry.id"
38+
dense
39+
expand-separator
40+
header-class="q-py-sm"
41+
:model-value="expandedSocketEntryId === entry.id"
42+
@update:model-value="(expanded) => onSocketEntryExpand(entry.id, expanded)"
43+
)
44+
template(#header)
45+
q-item-section(avatar)
46+
q-icon(
47+
:name="getSocketDebugDirectionMeta(entry.direction).icon"
48+
:color="getSocketDebugDirectionMeta(entry.direction).color"
49+
size="20px"
50+
)
51+
q-item-section
52+
q-item-label
53+
span.text-grey-7 {{ formatSocketDebugTime(entry.at) }}
54+
| {{ entry.namespace }}
55+
q-badge.q-ml-xs(color="orange-8" :label="entry.event")
56+
q-badge.q-ml-xs(outline color="grey-7" :label="getSocketDebugDirectionMeta(entry.direction).label")
57+
q-item-label(caption lines="2") {{ formatSocketDebugSummary(entry) }}
58+
q-card-section.q-pa-sm(:class="$q.dark.isActive ? 'bg-grey-9' : 'bg-grey-1'")
59+
pre.text-caption.q-ma-none(
60+
style="white-space: pre-wrap; word-break: break-all; font-family: ui-monospace, monospace;"
61+
) {{ formatSocketDebugPayload(entry.payload) }}
62+
</template>
63+
64+
<script lang="ts">
65+
export default defineNuxtComponent({
66+
name: 'CoreAppDebugPanelsComponent',
67+
setup() {
68+
const panels = useAppDebugPanels()
69+
70+
onUnmounted(() => {
71+
panels.stopSocketDebugResize()
72+
})
73+
74+
return panels
75+
},
76+
})
77+
</script>
78+
79+
<style scoped>
80+
.debug-socket-panel {
81+
box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.18);
82+
border-top-left-radius: 8px;
83+
border-top-right-radius: 8px;
84+
}
85+
86+
.debug-socket-resize-handle {
87+
display: flex;
88+
align-items: center;
89+
justify-content: center;
90+
height: 12px;
91+
flex-shrink: 0;
92+
cursor: ns-resize;
93+
user-select: none;
94+
background: rgba(0, 0, 0, 0.04);
95+
}
96+
</style>

0 commit comments

Comments
 (0)