"use server"; import { revalidatePath } from "next/cache"; import { headers } from "next/headers"; import { prisma } from "@/lib/db"; import { auth } from "@/lib/auth"; import { writeAudit } from "@/lib/audit"; import { createIssue, buildIssueBody } from "@/lib/gitea"; import { pipelineQueue } from "@/lib/queue"; import { alertIssueCreated } from "@/lib/telegram"; const ALLOWED_STATUS = new Set([ "new", "triaged", "in_backlog", "in_progress", "shipped", "validating", "validated", "deferred", "dismissed", "duplicate", "regressed", ]); async function requireSession() { const session = await auth.api.getSession({ headers: await headers() }); if (!session) throw new Error("unauthenticated"); return session; } export async function setStatus(insightId: string, status: string) { await requireSession(); if (!ALLOWED_STATUS.has(status)) throw new Error(`bad status: ${status}`); await prisma.insight.update({ where: { id: insightId }, data: { status }, }); await writeAudit({ endpoint: `/insights/${insightId}/status`, method: "POST", requestPayload: { status }, responseStatus: 200, }); revalidatePath("/insights"); revalidatePath(`/insights/i/${insightId}`); } export async function setFounderNotes(insightId: string, notes: string) { await requireSession(); await prisma.insight.update({ where: { id: insightId }, data: { founderNotes: notes.slice(0, 4000) || null }, }); await writeAudit({ endpoint: `/insights/${insightId}/notes`, method: "POST", requestPayload: { length: notes.length }, responseStatus: 200, }); revalidatePath(`/insights/i/${insightId}`); } export async function setFounderPriority(insightId: string, priority: number | null) { await requireSession(); await prisma.insight.update({ where: { id: insightId }, data: { founderPriority: priority }, }); await writeAudit({ endpoint: `/insights/${insightId}/priority`, method: "POST", requestPayload: { priority }, responseStatus: 200, }); revalidatePath("/insights"); revalidatePath(`/insights/i/${insightId}`); } export async function setSeverityOverride(insightId: string, severity: string | null) { await requireSession(); if (severity !== null && !["P0", "P1", "P2", "P3", "INFO"].includes(severity)) { throw new Error("bad severity"); } await prisma.insight.update({ where: { id: insightId }, data: { founderSeverityOverride: severity, severity: severity ?? undefined }, }); await writeAudit({ endpoint: `/insights/${insightId}/severity`, method: "POST", requestPayload: { severity }, responseStatus: 200, }); revalidatePath("/insights"); revalidatePath(`/insights/i/${insightId}`); } export async function bulkSetStatus(insightIds: string[], status: string) { await requireSession(); if (!ALLOWED_STATUS.has(status)) throw new Error("bad status"); const ids = insightIds.filter(Boolean).slice(0, 200); await prisma.insight.updateMany({ where: { id: { in: ids } }, data: { status }, }); await writeAudit({ endpoint: `/insights/bulk/status`, method: "POST", requestPayload: { count: ids.length, status }, responseStatus: 200, }); revalidatePath("/insights"); } export async function createGithubIssueForInsight( insightId: string, opts?: { titleOverride?: string; labelsExtra?: string[] }, ): Promise<{ url: string; number: number }> { await requireSession(); const insight = await prisma.insight.findUnique({ where: { id: insightId } }); if (!insight) throw new Error("insight not found"); if (insight.githubIssueUrl) throw new Error("issue already exists"); const panelUrl = process.env.BETTER_AUTH_URL ?? "https://sp.semih.ai"; const title = (opts?.titleOverride ?? insight.title).slice(0, 256); const body = buildIssueBody({ insightId: insight.id, panelUrl, type: insight.type, severity: insight.severity, occurrenceCount: insight.occurrenceCount, uniqueUserCount: insight.uniqueUserCount, firstSeenAt: insight.firstSeenAt, lastSeenAt: insight.lastSeenAt, confidence: insight.confidence, model: insight.sourceModel, body: insight.body as Record, relatedSessionIds: insight.relatedSessionIds, }); const labels = [ "insight-driven", `severity-${insight.severity}`, `type-${insight.type}`, `${insight.projectKey}-pilot`, ...(opts?.labelsExtra ?? []), ]; const issue = await createIssue(insight.projectKey, { title, body, labels }); await prisma.insight.update({ where: { id: insightId }, data: { githubIssueUrl: issue.html_url, githubIssueId: BigInt(issue.id), githubIssueNumber: issue.number, githubIssueState: issue.state, status: "in_backlog", }, }); await writeAudit({ endpoint: `/insights/${insightId}/github-issue`, method: "POST", requestPayload: { issueNumber: issue.number, repo: process.env[`GITEA_REPO_${insight.projectKey.toUpperCase()}`] }, responseStatus: 200, }); // Telegram notification (fire-and-forget) void alertIssueCreated({ insightId: insight.id, insightTitle: insight.title, severity: insight.severity, type: insight.type, issueNumber: issue.number, issueUrl: issue.html_url, panelUrl, }); revalidatePath("/insights"); revalidatePath(`/insights/i/${insightId}`); return { url: issue.html_url, number: issue.number }; } export type PromptEditInput = { tag: string; systemPrompt: string; userPromptTemplate: string; outputSchemaJson: object; modelTier: "flash" | "pro"; maxOutputTokens: number; temperature: number; name: string; }; // Create a new version of a prompt and deactivate the old one. export async function publishPromptVersion(input: PromptEditInput): Promise<{ id: string; version: number }> { await requireSession(); const latest = await prisma.promptTemplate.findFirst({ where: { tag: input.tag }, orderBy: { version: "desc" }, }); const nextVersion = (latest?.version ?? 0) + 1; // Deactivate all previous versions of this tag await prisma.promptTemplate.updateMany({ where: { tag: input.tag, active: true }, data: { active: false }, }); const created = await prisma.promptTemplate.create({ data: { tag: input.tag, version: nextVersion, name: input.name || `${input.tag} v${nextVersion}`, systemPrompt: input.systemPrompt, userPromptTemplate: input.userPromptTemplate, outputSchemaJson: input.outputSchemaJson as object, modelTier: input.modelTier, maxOutputTokens: input.maxOutputTokens, temperature: input.temperature, active: true, }, }); await writeAudit({ endpoint: `/insights/prompts/${input.tag}/publish`, method: "POST", requestPayload: { version: nextVersion }, responseStatus: 200, }); revalidatePath("/insights/settings/prompts"); revalidatePath(`/insights/settings/prompts/${created.id}`); return { id: created.id, version: nextVersion }; } export async function setPromptActive(id: string, active: boolean) { await requireSession(); const p = await prisma.promptTemplate.findUnique({ where: { id } }); if (!p) throw new Error("prompt not found"); if (active) { // Deactivate other versions of this tag await prisma.promptTemplate.updateMany({ where: { tag: p.tag, active: true, id: { not: id } }, data: { active: false }, }); } await prisma.promptTemplate.update({ where: { id }, data: { active } }); await writeAudit({ endpoint: `/insights/prompts/${p.tag}/active`, method: "POST", requestPayload: { active, version: p.version }, responseStatus: 200, }); revalidatePath("/insights/settings/prompts"); revalidatePath(`/insights/settings/prompts/${id}`); } export async function createEvalSet(input: { promptTag: string; name: string; description?: string; casesJson: string; }): Promise<{ id: string }> { await requireSession(); let cases: unknown; try { cases = JSON.parse(input.casesJson); } catch (e) { throw new Error(`cases not valid JSON: ${(e as Error).message}`); } if (!Array.isArray(cases)) throw new Error("cases must be an array"); const created = await prisma.evalSet.create({ data: { promptTag: input.promptTag, name: input.name, description: input.description ?? null, cases: cases as object, }, }); await writeAudit({ endpoint: `/insights/eval-sets`, method: "POST", requestPayload: { tag: input.promptTag, cases: (cases as unknown[]).length }, responseStatus: 200, }); revalidatePath("/insights/settings/eval-sets"); return { id: created.id }; } export async function triggerEvalRun( evalSetId: string, promptVersion?: number, ): Promise<{ jobId: string }> { await requireSession(); const queue = pipelineQueue(); const job = await queue.add( "eval-run", { evalSetId, promptVersion: promptVersion ?? null }, { removeOnComplete: 50, removeOnFail: 25 }, ); await writeAudit({ endpoint: `/insights/eval-sets/${evalSetId}/run`, method: "POST", requestPayload: { promptVersion }, responseStatus: 202, }); return { jobId: String(job.id ?? "unknown") }; } export async function updateBudgetSetting(key: string, value: number | boolean) { await requireSession(); const allowed = new Set([ "monthly_hard_cap_usd", "daily_soft_cap_usd", "daily_hard_cap_usd", "per_call_max_usd", "min_score_for_analysis", "cache_ttl_hours", "analysis_paused", // Content generation (separate envelope) — see lib/content-budget.ts "content_monthly_hard_cap_usd", "content_daily_soft_cap_usd", "content_daily_hard_cap_usd", "content_per_call_max_usd", "content_paused", ]); if (!allowed.has(key)) throw new Error("bad setting key"); const existing = await prisma.budgetSetting.findFirst({ where: { projectKey: null, settingKey: key }, }); if (existing) { await prisma.budgetSetting.update({ where: { id: existing.id }, data: { settingValue: value as unknown as object }, }); } else { await prisma.budgetSetting.create({ data: { projectKey: null, settingKey: key, settingValue: value as unknown as object }, }); } await writeAudit({ endpoint: `/insights/budget/${key}`, method: "POST", requestPayload: { value }, responseStatus: 200, }); revalidatePath("/insights/settings/budgets"); revalidatePath("/insights/costs"); revalidatePath("/insights"); if (key.startsWith("content_")) { revalidatePath("/content/costs"); revalidatePath("/content"); } }