feat(brief): Sase 360° evening brief — page + worker job + Telegram@20:00

Daily Sase brief aggregating VIN decode health, catalog coverage gaps,
revenue (Stripe/iyzico TRY), users/subscriptions, PostHog behavior + UTM
source, Meta ads (env-gated), and the insight pipeline into one
critical-points-first report.

- Brief model (panel DB): one row/project/day (date, headline, summary,
  health score, structured body, raw 360 metrics).
- Panel: getSaseBriefMetrics() (saseDb half) + /api/internal/sase-brief-data.
- Worker: sase-evening-brief job collects panel metrics + PostHog
  (hogqlQuery) + Meta (lib/meta, env-gated) + pipeline stats -> DeepSeek
  structured brief -> upsert Brief -> Telegram. Scheduled 17:00 UTC
  (20:00 Europe/Istanbul).
- UI: /projects/sase/briefs (table of days) + /briefs/[id] (detail:
  critical-first, sections by status, recommendations, raw metrics).

Meta dormant until META_ACCESS_TOKEN/META_AD_ACCOUNT_ID set on the worker.
Needs a `briefs` table migration on the panel DB before first run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Semih
2026-06-10 19:17:01 +03:00
parent 1d5daf2e5c
commit 01c93daf6a
10 changed files with 637 additions and 1 deletions

View File

@@ -292,6 +292,30 @@ model Insight {
@@map("insights")
}
// Daily evening brief — one 360° report per project per day. Generated by the
// worker (sase-evening-brief job) from Sase DB + PostHog + Meta + pipeline, then
// rendered at /projects/sase/briefs and pushed to Telegram at 20:00.
model Brief {
id String @id @default(cuid())
projectKey String @default("sase")
briefDate DateTime @db.Date
status String @default("generated") // generated | failed
headline String @db.Text
summary String @db.Text
healthScore Int @default(0)
body Json // structured: { critical[], sections[], recommendations[] }
metrics Json // raw 360 aggregates that fed the LLM (audit + page render)
telegramSent Boolean @default(false)
sourceModel String @default("")
sourceCostUsd Float @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([projectKey, briefDate])
@@index([projectKey, briefDate(sort: Desc)])
@@map("briefs")
}
model CostLedger {
id String @id @default(cuid())
insightId String?