mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-05 08:55:54 +08:00
Add waitlist admin view and log OpenAI decision traces
This commit is contained in:
116
server/api/admin/waitlist/index.get.ts
Normal file
116
server/api/admin/waitlist/index.get.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { defineEventHandler, getQuery } from 'h3'
|
||||
import type { FilterQuery } from 'mongoose'
|
||||
import { requireAdmin } from '../../../utils/auth'
|
||||
import { WaitlistEntry, type WaitlistEntryDocument } from '../../../models/WaitlistEntry'
|
||||
|
||||
type WaitlistListItem = {
|
||||
id: string
|
||||
email: string
|
||||
name?: string
|
||||
notes?: string
|
||||
source?: string
|
||||
joinedAt: string
|
||||
activatedAt?: string
|
||||
wantsProductUpdates: boolean
|
||||
updatesOptedInAt?: string
|
||||
}
|
||||
|
||||
function escapeRegExp(input: string) {
|
||||
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
}
|
||||
|
||||
function mapWaitlistEntry(doc: any): WaitlistListItem {
|
||||
return {
|
||||
id: String(doc._id),
|
||||
email: doc.email,
|
||||
name: doc.name || undefined,
|
||||
notes: doc.notes || undefined,
|
||||
source: doc.source || undefined,
|
||||
joinedAt: doc.joinedAt ? new Date(doc.joinedAt).toISOString() : new Date().toISOString(),
|
||||
activatedAt: doc.activatedAt ? new Date(doc.activatedAt).toISOString() : undefined,
|
||||
wantsProductUpdates: Boolean(doc.wantsProductUpdates),
|
||||
updatesOptedInAt: doc.updatesOptedInAt ? new Date(doc.updatesOptedInAt).toISOString() : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await requireAdmin(event)
|
||||
|
||||
const query = getQuery(event)
|
||||
const search = typeof query.search === 'string' ? query.search.trim() : ''
|
||||
const updatesFilterRaw = typeof query.updates === 'string' ? query.updates.trim() : 'all'
|
||||
const statusFilterRaw = typeof query.status === 'string' ? query.status.trim() : 'all'
|
||||
|
||||
const updatesFilter = ['updates', 'waitlist'].includes(updatesFilterRaw) ? updatesFilterRaw : 'all'
|
||||
const statusFilter = ['activated', 'pending'].includes(statusFilterRaw) ? statusFilterRaw : 'all'
|
||||
|
||||
const page = Number.parseInt(String(query.page ?? '1'), 10) || 1
|
||||
const pageSizeRaw = Number.parseInt(String(query.pageSize ?? query.limit ?? '20'), 10)
|
||||
const pageSize = Math.min(Math.max(pageSizeRaw || 20, 1), 100)
|
||||
const skip = (page - 1) * pageSize
|
||||
|
||||
const filter: FilterQuery<WaitlistEntryDocument> = {}
|
||||
const andConditions: FilterQuery<WaitlistEntryDocument>[] = []
|
||||
|
||||
if (search) {
|
||||
const regex = new RegExp(escapeRegExp(search), 'i')
|
||||
andConditions.push({ $or: [{ email: regex }, { name: regex }, { notes: regex }] })
|
||||
}
|
||||
|
||||
if (updatesFilter === 'updates') {
|
||||
andConditions.push({ wantsProductUpdates: true } as FilterQuery<WaitlistEntryDocument>)
|
||||
} else if (updatesFilter === 'waitlist') {
|
||||
andConditions.push({
|
||||
$or: [
|
||||
{ wantsProductUpdates: { $exists: false } },
|
||||
{ wantsProductUpdates: { $ne: true } },
|
||||
],
|
||||
} as FilterQuery<WaitlistEntryDocument>)
|
||||
}
|
||||
|
||||
if (statusFilter === 'activated') {
|
||||
andConditions.push({ activatedAt: { $exists: true, $ne: null } } as FilterQuery<WaitlistEntryDocument>)
|
||||
} else if (statusFilter === 'pending') {
|
||||
andConditions.push({
|
||||
$or: [
|
||||
{ activatedAt: null },
|
||||
{ activatedAt: { $exists: false } },
|
||||
],
|
||||
} as FilterQuery<WaitlistEntryDocument>)
|
||||
}
|
||||
|
||||
if (andConditions.length) {
|
||||
filter.$and = andConditions
|
||||
}
|
||||
|
||||
const [total, items, overallTotal, updatesCount, activatedCount] = await Promise.all([
|
||||
WaitlistEntry.countDocuments(filter),
|
||||
WaitlistEntry.find(filter)
|
||||
.sort({ joinedAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(pageSize)
|
||||
.lean(),
|
||||
WaitlistEntry.countDocuments(),
|
||||
WaitlistEntry.countDocuments({ wantsProductUpdates: true }),
|
||||
WaitlistEntry.countDocuments({ activatedAt: { $exists: true, $ne: null } }),
|
||||
])
|
||||
|
||||
const pendingCount = Math.max(0, overallTotal - activatedCount)
|
||||
|
||||
return {
|
||||
items: items.map(mapWaitlistEntry),
|
||||
pagination: {
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
pages: Math.ceil(total / pageSize) || 1,
|
||||
},
|
||||
stats: {
|
||||
total: overallTotal,
|
||||
updates: updatesCount,
|
||||
activated: activatedCount,
|
||||
pending: pendingCount,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
@@ -5,7 +5,7 @@ import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { execFile } from "node:child_process";
|
||||
import { getOpenAIClient, routeDecision } from "../../utils/openai";
|
||||
import { getOpenAIClient, routeDecision, type LLMDecisionResult } from "../../utils/openai";
|
||||
import { createReadStream } from "node:fs";
|
||||
import { TransmissionLog } from "../../models/TransmissionLog";
|
||||
import { getUserFromEvent } from "../../utils/auth";
|
||||
@@ -139,6 +139,7 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
const shouldAutoDecide = body.autoDecide !== false;
|
||||
|
||||
let decisionResult: LLMDecisionResult | null = null;
|
||||
let decision: PTTResponse['decision'];
|
||||
|
||||
if (shouldAutoDecide) {
|
||||
@@ -148,7 +149,8 @@ export default defineEventHandler(async (event) => {
|
||||
pilot_utterance: transcribedText
|
||||
};
|
||||
|
||||
decision = await routeDecision(decisionInput);
|
||||
decisionResult = await routeDecision(decisionInput);
|
||||
decision = decisionResult.decision;
|
||||
}
|
||||
|
||||
// 5. Cleanup
|
||||
@@ -169,6 +171,7 @@ export default defineEventHandler(async (event) => {
|
||||
moduleId: body.moduleId,
|
||||
lessonId: body.lessonId,
|
||||
decision,
|
||||
decisionTrace: decisionResult?.trace,
|
||||
autoDecide: shouldAutoDecide,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// server/api/llm/decide.post.ts
|
||||
import { readBody, createError } from 'h3'
|
||||
import { routeDecision, type LLMDecisionInput } from '../../utils/openai'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody<LLMDecisionInput | undefined>(event)
|
||||
@@ -11,7 +12,7 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const decision = await routeDecision(body)
|
||||
const { decision, trace } = await routeDecision(body)
|
||||
|
||||
// Log für Debugging bei off-schema oder radio check
|
||||
if (decision.off_schema) {
|
||||
@@ -21,6 +22,10 @@ export default defineEventHandler(async (event) => {
|
||||
console.log(`[ATC] Radio check processed: "${body.pilot_utterance}"`)
|
||||
}
|
||||
|
||||
if (trace?.calls?.length) {
|
||||
console.log('[ATC] Decision trace captured with', trace.calls.length, 'call(s)')
|
||||
}
|
||||
|
||||
return decision
|
||||
} catch (err: any) {
|
||||
console.error('Router failed:', err)
|
||||
|
||||
Reference in New Issue
Block a user