feat(cost-guard): global Gemini daily budget + EOC alarm; trial requires verified email

This commit is contained in:
Sven (AAMOS AI)
2026-08-12 06:43:32 +07:00
parent 2cd00f0106
commit 6c581bec1a
7 changed files with 96 additions and 10 deletions
+37 -1
View File
@@ -20,6 +20,8 @@ import { SUBSCRIPTION_PLANS, type SubscriptionPlan } from "@app/shared-types";
import type { Database } from "./client.js";
import { schema } from "./index.js";
const BUDGET_KEY_PREFIX = "gemini:daily:budget:";
export interface OpsQueueSummary {
vantande: number;
aktiva: number;
@@ -554,6 +556,19 @@ function storeBlock(): OpsStoreBlock {
}
export function buildWall(summary: OpsSummary): OpsWallBlock {
const budgetUsd = Number(process.env.GEMINI_DAILY_BUDGET_USD ?? 0);
const dailySpendUsd = summary.ai_scan.budget_andel != null && budgetUsd > 0
? summary.ai_scan.budget_andel * budgetUsd
: null;
const budgetLabel = dailySpendUsd != null && budgetUsd > 0
? `${dailySpendUsd.toFixed(2)} / ${budgetUsd} USD`
: "—";
const budgetTone: OpsWallTile["tone"] =
budgetUsd <= 0 ? "ok" :
summary.ai_scan.budget_andel == null ? "warn" :
summary.ai_scan.budget_andel >= 1 ? "crit" :
summary.ai_scan.budget_andel >= 0.8 ? "warn" : "ok";
const tiles: OpsWallTile[] = [
{
label: "MRR",
@@ -575,6 +590,11 @@ export function buildWall(summary: OpsSummary): OpsWallBlock {
value: formatPercent(summary.prenumerationer.konverteringsgrad_30d),
tone: summary.prenumerationer.konverteringsgrad_30d === null ? "warn" : "ok",
},
{
label: "AI-budget",
value: budgetLabel,
tone: budgetTone,
},
{
label: "Workers",
value: summary.jobb.workers_ok ? "OK" : "NERE",
@@ -630,10 +650,24 @@ async function feedbackBlock(db: Database): Promise<OpsFeedbackBlock> {
};
}
function alarmBlock(budgetUsd: number, dailySpendUsd: number | null | undefined): OpsAlarmBlock {
const alarms: OpsAlarm[] = [];
if (budgetUsd > 0 && dailySpendUsd != null && dailySpendUsd >= budgetUsd) {
alarms.push({
typ: "budget_exceeded",
rubrik: "Gemini dagsbudget förbrukad",
detalj: `Dagens spend ${dailySpendUsd.toFixed(4)} USD når eller överskrider taket ${budgetUsd} USD. Inga nya AI-jobb körs tills imorgon.`,
allvarlighetsgrad: "crit",
created_at: new Date().toISOString(),
});
}
return { larm: alarms };
}
export async function computeOpsSummary(options: ComputeOpsSummaryOptions): Promise<OpsSummary> {
const { db, budgetUsd, dailySpendUsd, queueSummary, planPrices: planPricesOverride } = options;
const planPrices = { ...getPlanPrices(), ...(planPricesOverride ?? {}) };
const [app, economy, users, subscriptions, butik, feedbackData, ai, activation, engagement, payment, safety] =
const [app, economy, users, subscriptions, butik, feedbackData, ai, activation, engagement, payment, safety, larm] =
await Promise.all([
Promise.resolve(appBlock()),
economyBlock(db, planPrices),
@@ -646,6 +680,7 @@ export async function computeOpsSummary(options: ComputeOpsSummaryOptions): Prom
engagementBlock(db),
paymentBlock(db),
safetyBlock(db),
Promise.resolve(alarmBlock(budgetUsd, dailySpendUsd)),
]);
const base: OpsSummary = {
@@ -661,6 +696,7 @@ export async function computeOpsSummary(options: ComputeOpsSummaryOptions): Prom
betalning: payment,
jobb: queueSummary ?? defaultQueueBlock(),
sakerhet: safety,
larm,
wall: { boards: [] },
as_of: new Date().toISOString(),
};