because I got bored of customising my CV for every job
1import {
2 AI_PROVIDER,
3 type AIProvider,
4 registeredProviderTypes,
5} from "@cv/ai-provider";
6import { AdminGuard, JwtAuthGuard, VerifiedScopeGuard } from "@cv/auth";
7import { Inject, Optional, UseGuards } from "@nestjs/common";
8import { PageInfo } from "@cv/system";
9import { Args, Int, Query, Resolver } from "@nestjs/graphql";
10import {
11 AiCallLogEntryType,
12 AiProviderStatus,
13 QueueMessageConnection,
14 QueueStats,
15 SystemStatus,
16 WorkerHealth,
17} from "./admin.type";
18import { AiCallLogService } from "./ai-call-log.service";
19import { QueueMonitorService } from "./queue-monitor.service";
20
21@Resolver()
22@UseGuards(JwtAuthGuard, VerifiedScopeGuard, AdminGuard)
23export class AdminResolver {
24 constructor(
25 @Inject(AI_PROVIDER)
26 @Optional()
27 private readonly globalProvider: AIProvider | undefined,
28 private readonly aiCallLogService: AiCallLogService,
29 private readonly queueMonitorService: QueueMonitorService,
30 ) {}
31
32 @Query(() => SystemStatus)
33 async systemStatus(): Promise<SystemStatus> {
34 const status = new SystemStatus();
35 status.serverUptime = process.uptime();
36 status.registeredProviderTypes = registeredProviderTypes();
37
38 if (!this.globalProvider?.getStatus) {
39 status.platformProvider = this.globalProvider
40 ? {
41 healthy: await this.globalProvider.isHealthy(),
42 providerName: this.globalProvider.name,
43 detailsJson: JSON.stringify(null),
44 }
45 : null;
46
47 return status;
48 }
49
50 const providerStatus = await this.globalProvider.getStatus();
51 const platformProvider = new AiProviderStatus();
52 platformProvider.healthy = providerStatus.healthy;
53 platformProvider.providerName = providerStatus.providerName;
54 platformProvider.detailsJson = JSON.stringify(
55 providerStatus.details ?? null,
56 );
57 status.platformProvider = platformProvider;
58
59 return status;
60 }
61
62 @Query(() => [AiCallLogEntryType])
63 aiCallLog(
64 @Args("limit", { type: () => Int, nullable: true }) limit?: number,
65 @Args("status", { type: () => String, nullable: true }) status?: string,
66 @Args("providerName", { type: () => String, nullable: true })
67 providerName?: string,
68 ): AiCallLogEntryType[] {
69 const entries = this.aiCallLogService.getEntries(limit ?? undefined);
70
71 return entries.filter(
72 (e) =>
73 (!status || e.status === status) &&
74 (!providerName || e.providerName === providerName),
75 );
76 }
77
78 @Query(() => [AiCallLogEntryType])
79 async aiCallLogHistory(
80 @Args("limit", { type: () => Int, nullable: true }) limit?: number,
81 @Args("status", { type: () => String, nullable: true }) status?: string,
82 @Args("providerName", { type: () => String, nullable: true })
83 providerName?: string,
84 ): Promise<AiCallLogEntryType[]> {
85 const rows = await this.aiCallLogService.queryHistory({
86 ...(limit != null ? { limit } : {}),
87 ...(status != null ? { status } : {}),
88 ...(providerName != null ? { providerName } : {}),
89 });
90
91 return rows.map((r) => ({
92 id: r.id,
93 timestamp: r.createdAt.toISOString(),
94 providerName: r.providerName,
95 durationMs: r.durationMs,
96 promptTokens: r.promptTokens ?? undefined,
97 completionTokens: r.completionTokens ?? undefined,
98 model: r.model ?? undefined,
99 finishReason: r.finishReason ?? undefined,
100 status: r.status,
101 error: r.error ?? undefined,
102 userId: r.userId ?? undefined,
103 source: r.source ?? undefined,
104 }));
105 }
106
107 @Query(() => QueueStats)
108 async queueStats(): Promise<QueueStats> {
109 const result = await this.queueMonitorService.getStats();
110 return QueueStats.fromDomain(result);
111 }
112
113 @Query(() => QueueMessageConnection)
114 async queueMessages(
115 @Args("limit", { type: () => Int, nullable: true }) limit?: number,
116 ): Promise<InstanceType<typeof QueueMessageConnection>> {
117 const result = await this.queueMonitorService.getMessages(limit ?? undefined);
118 return QueueMessageConnection.fromPaginationResult({
119 edges: result.map((r, i) => ({ node: r, cursor: String(i) })),
120 pageInfo: new PageInfo(false, false, null, null),
121 totalCount: result.length,
122 });
123 }
124
125 @Query(() => [WorkerHealth])
126 async workerHealth(): Promise<WorkerHealth[]> {
127 const results = await this.queueMonitorService.getWorkers();
128 return results.map(WorkerHealth.fromDomain);
129 }
130}