Files
Cibello-app/packages/database/src/ops-summary.ts
T
Sven (AAMOS AI) c32a7e33c7
CI / Typecheck, test & build (push) Failing after 2s
ci: trigga på master + formatfix inför Gitea Actions
2026-08-13 17:25:14 +07:00

786 lines
26 KiB
TypeScript

import { gte, sql } from "drizzle-orm";
import type {
OpsAppBlock,
OpsActivationBlock,
OpsAiScanBlock,
OpsEngagementBlock,
OpsPaymentBlock,
OpsQueueBlock,
OpsSafetyBlock,
OpsSummary,
OpsEconomyBlock,
OpsUsersBlock,
OpsSubscriptionsBlock,
OpsStoreBlock,
OpsFeedbackBlock,
OpsWallBlock,
OpsWallTile,
OpsAlarm,
OpsAlarmBlock,
OpsGeminiBlock,
} from "@app/shared-types";
import { SUBSCRIPTION_PLANS, type SubscriptionPlan } from "@app/shared-types";
import type { Database } from "./client.js";
import { schema } from "./index.js";
import { geminiUsage } from "./schema/index.js";
const BUDGET_KEY_PREFIX = "gemini:daily:budget:";
export interface OpsQueueSummary {
vantande: number;
aktiva: number;
misslyckade_24h: number;
aldsta_vantande_sek: number | null;
workers_ok: boolean;
}
export interface ComputeOpsSummaryOptions {
db: Database;
/** Gemini daily budget in USD (0 = disabled). */
budgetUsd: number;
/** Current daily spend in USD, from Redis budget store. */
dailySpendUsd?: number | null;
queueSummary?: OpsQueueSummary;
/** Optional override for subscription plan prices in SEK öre per month. */
planPrices?: Partial<Record<SubscriptionPlan, number | null>>;
}
const MICROCENTS_PER_USD = 100_000_000;
const ESTIMATED_SCAN_COST_USD = 0.0015;
const ESTIMATED_SCAN_COST_MICROCENTS = Math.round(ESTIMATED_SCAN_COST_USD * MICROCENTS_PER_USD);
function asNumber(value: unknown): number {
return Number(value ?? 0);
}
function formatSekKr(oer: number | null): string {
if (oer === null || Number.isNaN(oer)) return "—";
const kr = oer / 100;
// Mellanslag (0x20) som tusentalsavgränsare, utan ören.
const formatted = new Intl.NumberFormat("sv-SE", { maximumFractionDigits: 0 }).format(kr);
return `${formatted.replace(/\s/g, " ")} kr`;
}
function formatPercent(ratio: number | null): string {
if (ratio === null || Number.isNaN(ratio)) return "—";
return `${Math.round(ratio * 100)} %`;
}
function formatInt(n: number | null): string {
if (n === null || Number.isNaN(n)) return "—";
return String(n);
}
function safeDiv(numerator: number, denominator: number): number | null {
if (denominator === 0) return null;
return numerator / denominator;
}
function roundRate(value: number | null): number | null {
if (value === null || Number.isNaN(value)) return null;
return Math.round(value * 1_000_000) / 1_000_000;
}
async function countEvents(db: Database, name: string, hours: number): Promise<number> {
const result = await db.execute(sql`
SELECT count(*)::int AS n
FROM product_analytics_events
WHERE event_name = ${name}
AND occurred_at >= now() - make_interval(hours => ${hours})`);
const row = (Array.isArray(result) ? result[0] : result.rows[0]) as { n: number } | undefined;
return asNumber(row?.n);
}
async function aiScanBlock(
db: Database,
budgetUsd: number,
dailySpendUsd: number | null | undefined,
): Promise<OpsAiScanBlock> {
const scans24h = await countEvents(db, "scan_completed", 24);
const scans7d = await countEvents(db, "scan_completed", 24 * 7);
const started24h = await countEvents(db, "scan_started", 24);
const failed24h = await countEvents(db, "scan_failed", 24);
const latency = await db.execute(sql`
SELECT
percentile_cont(0.5) WITHIN GROUP (ORDER BY (properties->>'latencyMs')::int) AS p50,
percentile_cont(0.95) WITHIN GROUP (ORDER BY (properties->>'latencyMs')::int) AS p95
FROM product_analytics_events
WHERE event_name = 'scan_completed'
AND occurred_at >= now() - interval '24 hours'
AND properties->>'latencyMs' IS NOT NULL
`);
const latencyRow = (Array.isArray(latency) ? latency[0] : latency.rows[0]) as
{ p50: string | number | null; p95: string | number | null } | undefined;
const latestErrors = await db.execute(sql`
SELECT properties->>'errorCode' AS code, occurred_at AS tid
FROM product_analytics_events
WHERE event_name = 'scan_failed'
AND occurred_at >= now() - interval '24 hours'
ORDER BY occurred_at DESC
LIMIT 5
`);
const latestErrorsRows = (
Array.isArray(latestErrors) ? latestErrors : latestErrors.rows
) as Array<{
code: string | null;
tid: string | Date;
}>;
const month = await db.execute(sql`
SELECT COALESCE(sum(ai_cost_usd_microcents), 0)::bigint AS total
FROM ai_usage_counters
WHERE month = to_char(now() AT TIME ZONE 'UTC', 'YYYY-MM')
`);
const monthRow = (Array.isArray(month) ? month[0] : month.rows[0]) as
{ total: bigint | number } | undefined;
const monthlyCostMicrocents =
typeof monthRow?.total === "bigint" ? Number(monthRow.total) : asNumber(monthRow?.total);
const exact24h = await db.execute(sql`
SELECT COALESCE(sum(cost_usd), 0)::float AS total
FROM scan_jobs
WHERE cost_usd IS NOT NULL
AND updated_at >= now() - interval '24 hours'
AND status IN ('awaiting_confirmation', 'completed')
`);
const exact24hRow = (Array.isArray(exact24h) ? exact24h[0] : exact24h.rows[0]) as
{ total: number } | undefined;
const exactCostUsd = asNumber(exact24hRow?.total);
let cost24hMicrocents: number | null = null;
let cost24hApprox = false;
if (exactCostUsd > 0) {
cost24hMicrocents = Math.round(exactCostUsd * MICROCENTS_PER_USD);
} else if (scans24h > 0) {
cost24hMicrocents = scans24h * ESTIMATED_SCAN_COST_MICROCENTS;
cost24hApprox = true;
}
let budgetAndel: number | null = null;
if (budgetUsd > 0 && dailySpendUsd != null && !Number.isNaN(dailySpendUsd)) {
budgetAndel = Math.min(1, dailySpendUsd / budgetUsd);
}
return {
scans_24h: scans24h,
scans_7d: scans7d,
lyckandegrad_24h: roundRate(safeDiv(scans24h, started24h)),
latens_p50_ms: latencyRow?.p50 != null ? Math.round(Number(latencyRow.p50)) : null,
latens_p95_ms: latencyRow?.p95 != null ? Math.round(Number(latencyRow.p95)) : null,
felfrekvens_24h: roundRate(safeDiv(failed24h, started24h)),
senaste_fel:
latestErrorsRows.length > 0
? latestErrorsRows.map((r) => ({
kod: r.code,
tid: r.tid instanceof Date ? r.tid.toISOString() : String(r.tid),
}))
: null,
kostnad_usd_mikrocent_manad: monthlyCostMicrocents,
kostnad_usd_mikrocent_24h: cost24hMicrocents,
kostnad_usd_mikrocent_24h_uppskattad: cost24hApprox,
budget_andel: roundRate(budgetAndel),
as_of: new Date().toISOString(),
};
}
async function conversionWithin(
db: Database,
fromEvent: string,
toEvent: string,
cohortDays: number,
withinHours: number,
): Promise<number | null> {
const result = await db.execute(sql`
WITH cohort AS (
SELECT DISTINCT user_id, occurred_at AS created_at
FROM product_analytics_events
WHERE event_name = ${fromEvent}
AND occurred_at >= now() - make_interval(days => ${cohortDays})
AND user_id IS NOT NULL
),
converted AS (
SELECT DISTINCT c.user_id
FROM cohort c
INNER JOIN product_analytics_events e ON e.user_id = c.user_id
WHERE e.event_name = ${toEvent}
AND e.occurred_at >= c.created_at
AND e.occurred_at < c.created_at + make_interval(hours => ${withinHours})
)
SELECT
(SELECT count(*) FROM cohort) AS total,
(SELECT count(*) FROM converted) AS converted
`);
const row = (Array.isArray(result) ? result[0] : result.rows[0]) as
{ total: number; converted: number } | undefined;
if (!row || asNumber(row.total) === 0) return null;
return asNumber(row.converted) / asNumber(row.total);
}
async function householdConversionWithin(
db: Database,
fromEvent: string,
toEvent: string,
cohortDays: number,
): Promise<number | null> {
const result = await db.execute(sql`
WITH cohort AS (
SELECT DISTINCT household_id, occurred_at AS created_at
FROM product_analytics_events
WHERE event_name = ${fromEvent}
AND occurred_at >= now() - make_interval(days => ${cohortDays})
AND household_id IS NOT NULL
),
converted AS (
SELECT DISTINCT c.household_id
FROM cohort c
INNER JOIN product_analytics_events e ON e.household_id = c.household_id
WHERE e.event_name = ${toEvent}
AND e.occurred_at >= c.created_at
AND e.occurred_at < c.created_at + make_interval(days => ${cohortDays})
)
SELECT
(SELECT count(*) FROM cohort) AS total,
(SELECT count(*) FROM converted) AS converted
`);
const row = (Array.isArray(result) ? result[0] : result.rows[0]) as
{ total: number; converted: number } | undefined;
if (!row || asNumber(row.total) === 0) return null;
return asNumber(row.converted) / asNumber(row.total);
}
async function cohortRetention(db: Database, day: number): Promise<number | null> {
const result = await db.execute(sql`
WITH cohort AS (
SELECT DISTINCT user_id,
(occurred_at AT TIME ZONE 'UTC')::date AS cohort_date
FROM product_analytics_events
WHERE event_name = 'account_created'
AND occurred_at >= now() - interval '30 days'
AND user_id IS NOT NULL
),
active AS (
SELECT DISTINCT c.user_id
FROM cohort c
INNER JOIN product_analytics_events e ON e.user_id = c.user_id
WHERE (e.occurred_at AT TIME ZONE 'UTC')::date = c.cohort_date + ${day}::int
)
SELECT
(SELECT count(DISTINCT user_id) FROM cohort) AS total,
(SELECT count(*) FROM active) AS active
`);
const row = (Array.isArray(result) ? result[0] : result.rows[0]) as
{ total: number; active: number } | undefined;
if (!row || asNumber(row.total) === 0) return null;
return asNumber(row.active) / asNumber(row.total);
}
async function activationBlock(db: Database): Promise<OpsActivationBlock> {
const [scan24h, cook7d, householdSecond, d1, d7, d30] = await Promise.all([
conversionWithin(db, "account_created", "scan_completed", 7, 24),
conversionWithin(db, "account_created", "cooking_session_completed", 30, 24 * 7),
householdConversionWithin(db, "household_created", "second_member_first_action", 30),
cohortRetention(db, 1),
cohortRetention(db, 7),
cohortRetention(db, 30),
]);
return {
scan_inom_24h_andel: roundRate(scan24h),
lagad_maltid_inom_7d_andel: roundRate(cook7d),
hushall_andra_medlem_aktiv_andel: roundRate(householdSecond),
retention_d1: roundRate(d1),
retention_d7: roundRate(d7),
retention_d30: roundRate(d30),
};
}
async function engagementBlock(db: Database): Promise<OpsEngagementBlock> {
const result = await db.execute(sql`
SELECT
count(*) FILTER (WHERE event_name = 'cooking_session_completed' AND occurred_at >= now() - interval '24 hours')::int AS c24,
count(*) FILTER (WHERE event_name = 'cooking_session_completed' AND occurred_at >= now() - interval '7 days')::int AS c7,
count(*) FILTER (WHERE event_name = 'recommendation_opened' AND occurred_at >= now() - interval '7 days')::int AS opened,
count(*) FILTER (WHERE event_name = 'recommendations_viewed' AND occurred_at >= now() - interval '7 days')::int AS viewed
FROM product_analytics_events
`);
const row = (Array.isArray(result) ? result[0] : result.rows[0]) as
{ c24: number; c7: number; opened: number; viewed: number } | undefined;
const tips = await db.execute(sql`
SELECT count(*)::int AS n
FROM notifications
WHERE type = 'proactive_tip'
AND created_at >= now() - interval '24 hours'
`);
const tipsRow = (Array.isArray(tips) ? tips[0] : tips.rows[0]) as { n: number } | undefined;
return {
lagade_maltider_24h: asNumber(row?.c24),
lagade_maltider_7d: asNumber(row?.c7),
rek_ctr_7d: roundRate(safeDiv(asNumber(row?.opened), asNumber(row?.viewed))),
puffar_skickade_24h: asNumber(tipsRow?.n),
puffar_atgardsgrad_7d: null,
};
}
async function paymentBlock(db: Database): Promise<OpsPaymentBlock> {
const result = await db.execute(sql`
SELECT
count(*) FILTER (WHERE status = 'expired' OR (status != 'active' AND grace_period_expires_at < now()))::int AS failed,
count(*) FILTER (WHERE status = 'in_grace' OR (grace_period_expires_at IS NOT NULL AND grace_period_expires_at >= now() AND status != 'active'))::int AS grace
FROM subscriptions
`);
const row = (Array.isArray(result) ? result[0] : result.rows[0]) as
{ failed: number; grace: number } | undefined;
const trials = await db.execute(sql`
SELECT count(*)::int AS n
FROM trials
WHERE ends_at >= now()
AND ends_at <= now() + interval '48 hours'
`);
const trialsRow = (Array.isArray(trials) ? trials[0] : trials.rows[0]) as
{ n: number } | undefined;
const store = await db.execute(sql`
SELECT
count(*) FILTER (WHERE lower(notification_type) LIKE '%refund%')::int AS refunds,
count(*) FILTER (WHERE lower(notification_type) LIKE '%chargeback%' OR lower(notification_type) LIKE '%revoke%')::int AS chargebacks
FROM store_notifications
WHERE created_at >= now() - interval '7 days'
`);
const storeRow = (Array.isArray(store) ? store[0] : store.rows[0]) as
{ refunds: number; chargebacks: number } | undefined;
return {
failed_nu: asNumber(row?.failed),
grace_period_nu: asNumber(row?.grace),
trials_utgar_48h: asNumber(trialsRow?.n),
aterbetalningar_7d: storeRow ? asNumber(storeRow.refunds) : null,
chargebacks_7d: storeRow ? asNumber(storeRow.chargebacks) : null,
};
}
function defaultQueueBlock(): OpsQueueBlock {
return {
vantande: 0,
aktiva: 0,
misslyckade_24h: 0,
aldsta_vantande_sek: null,
workers_ok: false,
};
}
async function safetyBlock(db: Database): Promise<OpsSafetyBlock> {
const result = await db
.select()
.from(schema.opsSafetyCanary)
.orderBy(sql`${schema.opsSafetyCanary.occurredAt} DESC`)
.limit(1);
const row = result[0];
if (!row) {
return {
allergen_invariant_brott: 0,
overifierade_visade: 0,
food_safety_lint_avvisade_7d: null,
senaste_kontroll: null,
};
}
return {
allergen_invariant_brott: row.allergenInvariantBrott,
overifierade_visade: row.overifieradeVisade,
food_safety_lint_avvisade_7d: row.foodSafetyLintAvvisade7d,
senaste_kontroll: row.computedAt.toISOString(),
};
}
function appBlock(): OpsAppBlock {
return { app: "cibello", generated_at: new Date().toISOString() };
}
function getPlanPrices(): Record<SubscriptionPlan, number | null> {
const defaults = Object.fromEntries(SUBSCRIPTION_PLANS.map((p) => [p, null])) as Record<
SubscriptionPlan,
number | null
>;
const envJson = process.env.SUBSCRIPTION_PLAN_PRICES_JSON;
if (envJson) {
try {
const parsed = JSON.parse(envJson) as Record<string, number | null>;
for (const plan of SUBSCRIPTION_PLANS) {
if (parsed[plan] !== undefined) defaults[plan] = parsed[plan];
}
} catch {
// Ignorera trasig env-JSON; använd null för alla planer.
}
}
return defaults;
}
async function economyBlock(
db: Database,
planPrices: Record<SubscriptionPlan, number | null>,
): Promise<OpsEconomyBlock> {
const active = await db.execute(sql`
SELECT plan, count(*)::int AS n
FROM subscriptions
WHERE status = 'active'
AND (expires_at IS NULL OR expires_at > now())
GROUP BY plan
`);
const activeRows = (Array.isArray(active) ? active : active.rows) as Array<{
plan: SubscriptionPlan;
n: number;
}>;
let mrr: number | null = 0;
for (const row of activeRows) {
const price = planPrices[row.plan];
if (price === null) {
mrr = null;
break;
}
mrr = (mrr ?? 0) + price * row.n;
}
async function revenueFor(hours: number): Promise<number | null> {
const events = await db.execute(sql`
SELECT properties
FROM product_analytics_events
WHERE event_name = 'subscription_started'
AND occurred_at >= now() - make_interval(hours => ${hours})
`);
const rows = (Array.isArray(events) ? events : events.rows) as Array<{
properties: { priceOer?: number; plan?: SubscriptionPlan } | null;
}>;
let total: number | null = 0;
for (const row of rows) {
const props = row.properties ?? {};
const price =
typeof props.priceOer === "number" ? props.priceOer : planPrices[props.plan ?? "free"];
if (price === null) {
total = null;
break;
}
total = (total ?? 0) + price;
}
return total;
}
const [intakt24h, intakt7d] = await Promise.all([revenueFor(24), revenueFor(24 * 7)]);
return {
mrr,
intakt_24h: intakt24h,
intakt_7d: intakt7d,
valuta: "SEK",
};
}
async function usersBlock(db: Database): Promise<OpsUsersBlock> {
const result = await db.execute(sql`
SELECT
count(DISTINCT user_id) FILTER (WHERE user_id IS NOT NULL AND occurred_at >= now() - make_interval(mins => 5))::int AS aktiva_nu,
count(DISTINCT user_id) FILTER (WHERE user_id IS NOT NULL AND occurred_at >= now() - make_interval(hours => 24))::int AS dau,
count(DISTINCT user_id) FILTER (WHERE user_id IS NOT NULL AND occurred_at >= now() - make_interval(days => 30))::int AS mau,
count(*) FILTER (WHERE event_name = 'account_created' AND occurred_at >= now() - make_interval(hours => 24))::int AS nya_24h,
count(*) FILTER (WHERE event_name = 'account_created' AND occurred_at >= now() - make_interval(days => 7))::int AS nya_7d
FROM product_analytics_events
`);
const row = (Array.isArray(result) ? result[0] : result.rows[0]) as {
aktiva_nu: number;
dau: number;
mau: number;
nya_24h: number;
nya_7d: number;
};
return {
aktiva_nu: asNumber(row.aktiva_nu),
dau: asNumber(row.dau),
mau: asNumber(row.mau),
nya_24h: asNumber(row.nya_24h),
nya_7d: asNumber(row.nya_7d),
};
}
async function subscriptionsBlock(db: Database): Promise<OpsSubscriptionsBlock> {
const [trials, paying] = await Promise.all([
db.execute(sql`SELECT count(*)::int AS n FROM trials WHERE ends_at >= now()`),
db.execute(sql`
SELECT count(*)::int AS n
FROM subscriptions
WHERE status = 'active' AND (expires_at IS NULL OR expires_at > now())
`),
]);
const trialAktiva = asNumber((Array.isArray(trials) ? trials[0] : trials.rows[0]).n);
const betalande = asNumber((Array.isArray(paying) ? paying[0] : paying.rows[0]).n);
const trialConv24h = await db.execute(sql`
SELECT count(DISTINCT t.user_id)::int AS n
FROM product_analytics_events t
INNER JOIN product_analytics_events s ON s.user_id = t.user_id
WHERE t.event_name = 'trial_started'
AND s.event_name = 'subscription_started'
AND s.occurred_at >= now() - make_interval(hours => 24)
AND s.occurred_at >= t.occurred_at
`);
const trialConv7d = await db.execute(sql`
SELECT count(DISTINCT t.user_id)::int AS n
FROM product_analytics_events t
INNER JOIN product_analytics_events s ON s.user_id = t.user_id
WHERE t.event_name = 'trial_started'
AND s.event_name = 'subscription_started'
AND s.occurred_at >= now() - make_interval(days => 7)
AND s.occurred_at >= t.occurred_at
`);
const konv30d = await conversionWithin(db, "trial_started", "subscription_started", 30, 30 * 24);
const avslutade = await db.execute(sql`
SELECT count(*)::int AS n
FROM subscription_events
WHERE event_type IN ('expired', 'cancelled')
AND created_at >= now() - make_interval(hours => 24)
`);
return {
trial_aktiva: trialAktiva,
trial_konverterade_24h: asNumber(
(Array.isArray(trialConv24h) ? trialConv24h[0] : trialConv24h.rows[0]).n,
),
trial_konverterade_7d: asNumber(
(Array.isArray(trialConv7d) ? trialConv7d[0] : trialConv7d.rows[0]).n,
),
konverteringsgrad_30d: roundRate(konv30d),
betalande,
avslutade_24h: asNumber((Array.isArray(avslutade) ? avslutade[0] : avslutade.rows[0]).n),
};
}
function storeBlock(): OpsStoreBlock {
return { butik: null, all_fields_phase_2: null };
}
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",
value: formatSekKr(summary.ekonomi.mrr),
tone: summary.ekonomi.mrr === null ? "warn" : "ok",
},
{
label: "Aktiva nu",
value: formatInt(summary.anvandare.aktiva_nu),
tone: "ok",
},
{
label: "Betalande",
value: formatInt(summary.prenumerationer.betalande),
tone: "ok",
},
{
label: "Trial-konv",
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",
tone: summary.jobb.workers_ok ? "ok" : "warn",
},
{
label: "Allergen-brott",
value: formatInt(summary.sakerhet.allergen_invariant_brott),
tone: summary.sakerhet.allergen_invariant_brott === 0 ? "ok" : "warn",
},
];
return {
boards: [{ title: "{brand}", tiles }],
};
}
async function feedbackBlock(db: Database): Promise<OpsFeedbackBlock> {
const counts = await db.execute(sql`
SELECT
count(*) FILTER (WHERE status = 'oppen')::int AS oppna,
count(*) FILTER (WHERE created_at >= now() - make_interval(hours => 24))::int AS nya_24h
FROM feedback
`);
const countsRow = (Array.isArray(counts) ? counts[0] : counts.rows[0]) as {
oppna: number;
nya_24h: number;
};
const latest = await db
.select({
rubrik: schema.feedback.rubrik,
typ: schema.feedback.typ,
status: schema.feedback.status,
createdAt: schema.feedback.createdAt,
})
.from(schema.feedback)
.orderBy(sql`${schema.feedback.createdAt} DESC`)
.limit(10);
return {
oppna: asNumber(countsRow.oppna),
nya_24h: asNumber(countsRow.nya_24h),
senaste:
latest.length > 0
? latest.map((f) => ({
rubrik: f.rubrik,
typ: f.typ,
status: f.status,
created_at: f.createdAt.toISOString(),
}))
: null,
};
}
async function geminiBlock(db: Database): Promise<OpsGeminiBlock> {
const defaults: OpsGeminiBlock = {
anrop_24h: null,
prompt_tokens_24h: null,
output_tokens_24h: null,
total_tokens_24h: null,
total_tokens_manad: null,
kostnad_usd_mikrocent_24h: null,
andel_pa_aamos: 0.0,
aamos_agreement: null,
};
try {
const now = new Date();
const since24h = new Date(now.getTime() - 24 * 60 * 60 * 1000);
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const [dayAgg, monthAgg] = await Promise.all([
db
.select({
count: sql<number>`count(*)`,
promptTokens: sql<number>`coalesce(sum(${geminiUsage.promptTokens}), 0)`,
outputTokens: sql<number>`coalesce(sum(${geminiUsage.outputTokens}), 0)`,
totalTokens: sql<number>`coalesce(sum(${geminiUsage.totalTokens}), 0)`,
costMicrocents: sql<number>`coalesce(sum(${geminiUsage.costMicrocents}), 0)`,
})
.from(geminiUsage)
.where(gte(geminiUsage.createdAt, since24h)),
db
.select({
totalTokens: sql<number>`coalesce(sum(${geminiUsage.totalTokens}), 0)`,
})
.from(geminiUsage)
.where(gte(geminiUsage.createdAt, startOfMonth)),
]);
return {
anrop_24h: Number(dayAgg[0]?.count ?? 0),
prompt_tokens_24h: Number(dayAgg[0]?.promptTokens ?? 0),
output_tokens_24h: Number(dayAgg[0]?.outputTokens ?? 0),
total_tokens_24h: Number(dayAgg[0]?.totalTokens ?? 0),
total_tokens_manad: Number(monthAgg[0]?.totalTokens ?? 0),
kostnad_usd_mikrocent_24h: Number(dayAgg[0]?.costMicrocents ?? 0),
andel_pa_aamos: 0.0,
aamos_agreement: null,
};
} catch {
return defaults;
}
}
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,
larm,
gemini,
] = await Promise.all([
Promise.resolve(appBlock()),
economyBlock(db, planPrices),
usersBlock(db),
subscriptionsBlock(db),
Promise.resolve(storeBlock()),
feedbackBlock(db),
aiScanBlock(db, budgetUsd, dailySpendUsd),
activationBlock(db),
engagementBlock(db),
paymentBlock(db),
safetyBlock(db),
Promise.resolve(alarmBlock(budgetUsd, dailySpendUsd)),
geminiBlock(db),
]);
const base: OpsSummary = {
app,
ekonomi: economy,
anvandare: users,
prenumerationer: subscriptions,
butik,
feedback: feedbackData,
ai_scan: ai,
aktivering: activation,
engagemang: engagement,
betalning: payment,
jobb: queueSummary ?? defaultQueueBlock(),
sakerhet: safety,
larm,
wall: { boards: [] },
gemini,
as_of: new Date().toISOString(),
};
base.wall = buildWall(base);
return base;
}