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>; } 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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; 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, ): Promise { 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 { 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 { 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 { 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 { 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 { 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`count(*)`, promptTokens: sql`coalesce(sum(${geminiUsage.promptTokens}), 0)`, outputTokens: sql`coalesce(sum(${geminiUsage.outputTokens}), 0)`, totalTokens: sql`coalesce(sum(${geminiUsage.totalTokens}), 0)`, costMicrocents: sql`coalesce(sum(${geminiUsage.costMicrocents}), 0)`, }) .from(geminiUsage) .where(gte(geminiUsage.createdAt, since24h)), db .select({ totalTokens: sql`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 { 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; }