Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
150 commits
Select commit Hold shift + click to select a range
7e87c23
evals
izadoesdev Apr 3, 2026
a05319a
cleanup
izadoesdev Apr 3, 2026
18d57c6
cleanup copywriting
izadoesdev Apr 3, 2026
40e7161
fix(tracker): audit fixes — cleanup, flush safety, unload reliability
izadoesdev Apr 3, 2026
dfc5810
chore: clean up .env.example
izadoesdev Apr 3, 2026
51506eb
chore: modernize turbo.json
izadoesdev Apr 3, 2026
60eca9e
chore: standardize script names and clean root deps
izadoesdev Apr 3, 2026
32655dd
refactor(mapper): simplify import system and adapter API
izadoesdev Apr 3, 2026
3ad187d
chore(sdk): rename test file .spec.ts to .test.ts
izadoesdev Apr 3, 2026
9f04009
chore: remove unused gitconfig
izadoesdev Apr 3, 2026
78f9243
chore: upgrade turbo to 2.9.3 and enable future flags
izadoesdev Apr 3, 2026
90c3e86
ci: optimize all GitHub Actions workflows
izadoesdev Apr 3, 2026
22d3cba
fix(ci): replace tsgo with turbo check-types, make lint non-blocking
izadoesdev Apr 3, 2026
cff89d3
fix(ci): add build dependency to check-types turbo task
izadoesdev Apr 3, 2026
dfef6e9
fix(ci): make check-types non-blocking until pre-existing errors fixed
izadoesdev Apr 3, 2026
793d649
fix(docker): use turbo prune in api.Dockerfile to avoid building dash…
izadoesdev Apr 3, 2026
ef0eb21
chore: remove unused cursor skills symlink
izadoesdev Apr 3, 2026
a6fc1c2
chore: add dependabot configuration
izadoesdev Apr 3, 2026
e11d892
chore(vscode): use biome as typescript formatter
izadoesdev Apr 3, 2026
7a70091
fix(docker): standardize bun images to 1.3.4-slim
izadoesdev Apr 3, 2026
1ff5ab2
fix(dashboard): rename SDK clientId to apiKey
izadoesdev Apr 3, 2026
db2ee0b
fix(dashboard): update flag SDK method names
izadoesdev Apr 3, 2026
d9f8981
fix(basket): improve geo-ip test resilience for CI
izadoesdev Apr 3, 2026
19ebd96
fix(status-page): improve OG image accuracy and null safety
izadoesdev Apr 3, 2026
20e56ac
refactor(evals): improve code quality and redesign UI
izadoesdev Apr 3, 2026
783f812
refactor: improve type safety and code quality across codebase
izadoesdev Apr 3, 2026
37c1fb4
style: apply biome linting and formatting
izadoesdev Apr 3, 2026
f13d494
fix(dashboard): react-doctor fixes and ultracite v7 upgrade (#381)
izadoesdev Apr 3, 2026
594f922
feat(docker): add self-hosting support with docker-compose configurat…
sekhar08 Apr 3, 2026
13f0ac3
feat: add healthchecks to Dockerfiles for api, basket, links, and upt…
sekhar08 Apr 3, 2026
c4f0de9
fix(docker): update selfhost compose comment to reflect built-in heal…
izadoesdev Apr 3, 2026
f75b145
fix(docker): remove healthchecks, bump bun to 1.3.11, standardize Doc…
izadoesdev Apr 3, 2026
7bae1db
fix(docker): clean up selfhost compose, add compose-level healthchecks
izadoesdev Apr 3, 2026
6c353fd
fix(docker): remove noisy required-var syntax from selfhost compose
izadoesdev Apr 3, 2026
c167832
fix(docker): use env vars for connection strings instead of hardcoding
izadoesdev Apr 3, 2026
1a1f707
fix(docker): harden selfhost compose for production
izadoesdev Apr 3, 2026
7c1c74c
style(docker): format selfhost compose
izadoesdev Apr 3, 2026
8e243d7
Cleanup
izadoesdev Apr 4, 2026
b72e2d0
fix(basket): fix billing enforcement, desloppify routes and event ser…
izadoesdev Apr 4, 2026
8711dea
test(basket): comprehensive test suite — 41 → 414 tests
izadoesdev Apr 4, 2026
7d06f4f
fix(basket): mock shared bot-detection in user-agent tests
izadoesdev Apr 4, 2026
5aeaa67
ci: bump Bun to 1.3.11, fix auth tests to mock DB deps
izadoesdev Apr 4, 2026
a9b3b02
fix(basket): remove evlog mock that leaked across test files
izadoesdev Apr 4, 2026
f9626e6
cleanup
izadoesdev Apr 4, 2026
3f8d344
fix: ultracite
izadoesdev Apr 4, 2026
ec1a29b
docs: add sidebar refactor design spec
izadoesdev Apr 4, 2026
b5bf648
docs: add sidebar refactor implementation plan
izadoesdev Apr 4, 2026
6a3bd8a
chore: remove spec and plan docs
izadoesdev Apr 4, 2026
3c46b8b
feat(dashboard): add SidebarNavigationProvider and NavigationRenderer
izadoesdev Apr 4, 2026
a1f3f46
refactor(dashboard): simplify sidebar files to consume context
izadoesdev Apr 4, 2026
f6b12d3
perf(dashboard): switch navigation-config to direct icon imports
izadoesdev Apr 4, 2026
b95f04b
ultracite fix
izadoesdev Apr 4, 2026
dd5937f
fix: resolve biome format/check import ordering conflict
izadoesdev Apr 4, 2026
65ba3db
fix: align biome catalog version and apply lint fixes
izadoesdev Apr 4, 2026
e86d436
cleanup
izadoesdev Apr 4, 2026
2951d75
fix: ultracite lint
izadoesdev Apr 4, 2026
50a080a
perf(dashboard): bundle size and rendering optimizations
izadoesdev Apr 5, 2026
2758618
perf(dashboard): defer command search, deduplicate session, remove se…
izadoesdev Apr 5, 2026
33fad25
fix(dashboard): gate sidebar queries on isHydrated to prevent hydrati…
izadoesdev Apr 5, 2026
88670bb
fix(dashboard): use csr icon imports in client components to fix hydr…
izadoesdev Apr 5, 2026
dc0f306
fix(dashboard): revert icon imports to barrel — direct imports cause …
izadoesdev Apr 5, 2026
e8aed06
fix(dashboard): revert session dedup — use authClient.useSession() ev…
izadoesdev Apr 5, 2026
70ddaff
fix(sdk): use automatic JSX runtime in build config
izadoesdev Apr 5, 2026
7fc4e58
fix(dashboard): show loading nav until session resolves to prevent hy…
izadoesdev Apr 5, 2026
5197ee8
perf(dashboard): make sidebar navigation fully static
izadoesdev Apr 5, 2026
01c4b66
refactor(dashboard): merge Websites into Overview section in sidebar
izadoesdev Apr 5, 2026
71552bb
refactor(dashboard): unite Monitors and Status Pages into single section
izadoesdev Apr 5, 2026
9006b84
fix(auth): add rate limiting to all email-sending callbacks and auth …
izadoesdev Apr 5, 2026
39c0579
fix(auth): remove secondaryStorage to fix 401s on staging
izadoesdev Apr 5, 2026
28eadac
fix(auth): use Redis customStorage for rate limiting instead of database
izadoesdev Apr 5, 2026
690693f
fix(auth): parse JSON in rate limit customStorage getter
izadoesdev Apr 5, 2026
70a21e3
fix ultracite lint
izadoesdev Apr 5, 2026
a169bde
refactor(uptime): merge duplicate UptimeData construction and remove …
izadoesdev Apr 5, 2026
a98735a
refactor(uptime): simplify Kafka producer
izadoesdev Apr 5, 2026
658a23e
refactor(uptime): remove commented-out sampling config
izadoesdev Apr 5, 2026
f6dfcba
refactor(uptime): replace type assertion with honest cast
izadoesdev Apr 5, 2026
00cbfb3
refactor(uptime): remove unused schema, no-op wrapper, and redundant …
izadoesdev Apr 5, 2026
ca5ba1a
fix(dashboard): pass organizationId for all-websites custom events query
izadoesdev Apr 5, 2026
3560b46
perf(api): optimize custom events WHERE clause for org-level queries
izadoesdev Apr 5, 2026
9723f92
perf(dashboard): lazy-load property queries on custom events page
izadoesdev Apr 5, 2026
b67c5c5
feat(dashboard): rebuild onboarding as 4-step wizard
izadoesdev Apr 5, 2026
fff568c
feat(dashboard): add AI-first tracking install step with branded copy…
izadoesdev Apr 5, 2026
829a913
feat(api): add agent install telemetry endpoint
izadoesdev Apr 5, 2026
0c72064
feat(dashboard): add custom event tracking to onboarding flow
izadoesdev Apr 5, 2026
582037f
fix(dashboard): remove high-cardinality domain from onboarding event
izadoesdev Apr 5, 2026
2dd904f
feat(uptime): add manual check button with rate limiting
izadoesdev Apr 5, 2026
2739413
feat(dashboard): add per-event breakdown to custom events trend chart
izadoesdev Apr 5, 2026
9710496
docs: remove coming-soon placeholders and add integration pages
izadoesdev Apr 5, 2026
f472c6e
docs(sdk): consolidate feature flags hooks API around useFlag/useFlags
izadoesdev Apr 5, 2026
5f2aca2
chore(sdk): alphabetize flags-manager type imports
izadoesdev Apr 5, 2026
7d19db1
Delete WORKSPACE.md
izadoesdev Apr 5, 2026
805f652
chore(dashboard): remove LLM analytics pages
izadoesdev Apr 6, 2026
1fcce56
refactor(api,dashboard): use device_type column for device categories
izadoesdev Apr 6, 2026
92ebfb7
refactor(dashboard,docs): switch favicons from DuckDuckGo to Google S2
izadoesdev Apr 6, 2026
931b5c5
feat(monitors): allow editing monitor name and surface timeout/cacheBust
izadoesdev Apr 6, 2026
f363f36
refactor(dashboard): consolidate compact number formatting in lib/for…
izadoesdev Apr 6, 2026
cbc4387
refactor(dashboard): simplify dby/l/[slug] link page
izadoesdev Apr 6, 2026
d79d831
fix(auth): trim rate-limit TTL and surface throttled callbacks in evlog
izadoesdev Apr 6, 2026
861dbc7
refactor(mcp): introduce defineMcpTool wrapper and standardize tool s…
izadoesdev Apr 6, 2026
971c8ef
feat(mcp): add 5 insight tools (list, summarize, compare, top_movers,…
izadoesdev Apr 6, 2026
40dddd3
feat(docs): add /oss program page for open source maintainers
izadoesdev Apr 6, 2026
7941705
chore: ignore .gstack/
izadoesdev Apr 6, 2026
cb4dca2
perf(dashboard): skip property queries on org-level events view
izadoesdev Apr 6, 2026
c64e92a
refactor(uptime): simplify pingWebsite and bump timeout to 60s
izadoesdev Apr 6, 2026
8d014a3
feat(mcp): add tool output schemas and cache website lookups
izadoesdev Apr 6, 2026
9f4e70b
feat(dashboard): public mode for BillingProvider
izadoesdev Apr 6, 2026
3a715e2
style(dashboard): compact events stream filter toolbar
izadoesdev Apr 6, 2026
902f6d0
refactor(dashboard): map browser/os icons to explicit file extensions
izadoesdev Apr 6, 2026
95f3ec1
fix(dashboard): avoid nested button HTML in flag and funnel rows
izadoesdev Apr 6, 2026
d7fda93
fix(dashboard): guard Iridescence when WebGL is unavailable
izadoesdev Apr 6, 2026
14c5ca6
chore: tighten stale code comments
izadoesdev Apr 6, 2026
67e13fb
chore(turbo): rename AI_API_KEY to AI_GATEWAY_API_KEY in globalEnv
izadoesdev Apr 6, 2026
9630653
refactor(tracker): unify batch queues and drop unused engagement/bot …
izadoesdev Apr 6, 2026
5c6c49a
refactor(evals): drop obsolete model field from EvalCase
izadoesdev Apr 6, 2026
4b2fbb3
feat(db): add agent_chats table for persisted Databunny conversations
izadoesdev Apr 6, 2026
2672dcc
feat(rpc): add agentChats router for persisted Databunny chats
izadoesdev Apr 6, 2026
698b1ab
refactor(mcp): slim payloads, harden inputs, improve error UX
izadoesdev Apr 6, 2026
265f7d0
feat(api): persist agent chats with titles, followups, and rate limits
izadoesdev Apr 6, 2026
7a43fff
feat(dashboard): server-backed Databunny chat persistence
izadoesdev Apr 6, 2026
d1b4429
chore(agent): drop follow-up suggestions + post-stream sync
izadoesdev Apr 7, 2026
1ea870a
chore(agent): delete dead slash command menu
izadoesdev Apr 7, 2026
d7765e2
refactor(agent): flatten input + inline pending queue
izadoesdev Apr 7, 2026
45e3d7e
feat(agent): inspectable tool steps + active tool label + unified errors
izadoesdev Apr 7, 2026
9258739
feat(agent): welcome state cleanup + header website context
izadoesdev Apr 7, 2026
b1daa82
fix: icon
izadoesdev Apr 7, 2026
90263f4
fix(dashboard): drop text-foreground on empty state chart icons
izadoesdev Apr 7, 2026
4b7c986
fix(dashboard): drop text-accent-foreground on funnels page icons
izadoesdev Apr 7, 2026
9b93745
fix(security): close CodeQL high-severity findings
izadoesdev Apr 7, 2026
918d196
fix(tracker): outgoing-links plugin posted to nonexistent /outgoing r…
izadoesdev Apr 7, 2026
8c79a34
test(tracker): strict basket route allowlist + auto-fixture
izadoesdev Apr 7, 2026
cd59952
fix(tracker): pixel plugin routes all events to /px.jpg
izadoesdev Apr 7, 2026
cb478f5
chore(tracker): desloppify test-utils + pixel plugin
izadoesdev Apr 7, 2026
52a63cc
chore(api): add tokenlens for agent token + cost telemetry
izadoesdev Apr 7, 2026
16cbe06
feat(api): token + cost telemetry on agent stream
izadoesdev Apr 7, 2026
079b5a3
chore(agent): drop redundant + outdated comments
izadoesdev Apr 7, 2026
e62fded
feat(billing): expand autumn config — uptime, status pages, alarms, g…
izadoesdev Apr 7, 2026
04ccdc1
feat(api): enforce agent credits via autumn
izadoesdev Apr 8, 2026
d9fec85
chore(billing): remove seats — team members unlimited on all plans
izadoesdev Apr 8, 2026
68a4636
feat(agent): surface credit balance in header
izadoesdev Apr 8, 2026
bbf47a5
fix(uptime): align heatmap day buckets with user timezone
izadoesdev Apr 8, 2026
3232d33
chore(api/mcp): coerce inputs via z.preprocess
izadoesdev Apr 8, 2026
ddfb783
chore(dashboard): add atmn cli, ignore generated sdk d.ts
izadoesdev Apr 8, 2026
fab0b5f
chore(billing): rescale agent credits 5x cheaper, 5x budgets
izadoesdev Apr 8, 2026
3975967
feat(agent): cost-aware overhaul + thinking effort toggle
izadoesdev Apr 8, 2026
6d22958
revert(billing): restore original per-token credit cost
izadoesdev Apr 8, 2026
90fdd0c
fix(dashboard): gate nav item flag filter on hydration
izadoesdev Apr 8, 2026
126cf5c
polish(dashboard/agent): dropdown thinking control, slimmer header
izadoesdev Apr 8, 2026
c4dc2be
Update page.tsx
izadoesdev Apr 8, 2026
ec6f9b3
feat(insights): redesign insights as AI cockpit (DAT-100) (#402)
izadoesdev Apr 8, 2026
bf500ac
chore: merge main into staging
izadoesdev Apr 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,4 @@ tmp
# next-agents-md
.next-docs/
.gstack/
.superpowers/
9 changes: 4 additions & 5 deletions apps/api/scripts/agent-cost-probe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,11 @@ if (!(websiteId && userId)) {
process.exit(1);
}

// Matches creditSchema in apps/dashboard/autumn.config.ts.
const CURRENT_SCHEMA = {
input: 0.0006,
output: 0.003,
cacheRead: 0.000_06,
cacheWrite: 0.000_75,
input: 0.000_72,
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Warning: CURRENT_SCHEMA comment referencing autumn.config.ts was removed.

The old code had a helpful comment (// Matches creditSchema in apps/dashboard/autumn.config.ts) that tied this probe script to the production pricing config. With the 20% markup applied here, it'd be worth keeping a similar breadcrumb so future edits stay in sync. If someone updates autumn.config.ts later but forgets this file (or vice versa), the probe results will be wrong.

output: 0.0036,
cacheRead: 0.000_072,
cacheWrite: 0.001_44,
};

function computeCredits(
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/ai/agents/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export function createConfig(context: AgentContext): AgentConfig {
currentDateTime: new Date().toISOString(),
chatId: context.chatId,
requestHeaders: context.requestHeaders,
billingCustomerId: context.billingCustomerId,
};

return {
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/ai/agents/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const AGENT_THINKING_LEVELS: readonly AgentThinking[] = [
] as const;

export interface AgentContext {
billingCustomerId?: string | null;
chatId: string;
requestHeaders?: Headers;
thinking?: AgentThinking;
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/ai/config/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
export interface AppContext {
/** Available query builder types */
availableQueryTypes?: string[];
billingCustomerId?: string | null;
chatId: string;
currentDateTime: string;
requestHeaders?: Headers;
Expand Down
18 changes: 18 additions & 0 deletions apps/api/src/ai/tools/web-search.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getAutumn } from "@databuddy/rpc";
import { generateText, tool } from "ai";
import { z } from "zod";
import type { AppContext } from "../config/context";
Expand Down Expand Up @@ -67,6 +68,23 @@ export const webSearchTool = tool({
responseLength: result.text.length,
});

if (appContext?.billingCustomerId) {
getAutumn()
.track({
customerId: appContext.billingCustomerId,
featureId: "agent_web_search_calls",
value: 1,
})
.catch((trackError) => {
logger.error("Failed to track web search usage", {
error:
trackError instanceof Error
? trackError.message
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Suggestion: Fire-and-forget billing could silently under-count usage.

If the Autumn billing service has a prolonged outage, web searches continue unmetered since .catch() only logs. This is probably the right tradeoff for availability, but consider adding a counter/metric for failed tracking calls so you can detect drift between actual usage and billed usage. Even a simple mergeWideEvent({ web_search_billing_track_failed: true }) would help.

: String(trackError),
});
});
}

// Sanitize web content before returning to the agent to prevent
// indirect prompt injection from adversarial web pages.
const sanitized = result.text.replace(
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/routes/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ export const agent = new Elysia({ prefix: "/v1/agent" })
chatId,
requestHeaders: request.headers,
thinking: body.thinking,
billingCustomerId,
})
),
isMemoryEnabled() && lastMessage
Expand Down
189 changes: 187 additions & 2 deletions apps/api/src/routes/insights.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import {
member,
websites,
} from "@databuddy/db";
import { getRedisCache } from "@databuddy/redis";
import { cacheable, getRedisCache } from "@databuddy/redis";
import { getRateLimitHeaders, rateLimit } from "@databuddy/redis/rate-limit";
import { generateText, Output, stepCountIs, ToolLoopAgent } from "ai";
import dayjs from "dayjs";
import { Elysia, t } from "elysia";
Expand All @@ -23,7 +24,7 @@ import type { ParsedInsight } from "../ai/schemas/smart-insights-output";
import { insightsOutputSchema } from "../ai/schemas/smart-insights-output";
import { createInsightsAgentTools } from "../ai/tools/insights-agent-tools";
import { storeAnalyticsSummary } from "../lib/supermemory";
import { mergeWideEvent } from "../lib/tracing";
import { captureError, mergeWideEvent } from "../lib/tracing";
import { executeQuery } from "../query";

const CACHE_TTL = 900;
Expand Down Expand Up @@ -927,6 +928,118 @@ async function invalidateInsightsCacheForOrg(
}
}

const NARRATIVE_RATE_LIMIT = 30;
const NARRATIVE_RATE_WINDOW_SECS = 3600;
const NARRATIVE_CACHE_TTL_SECS = 3600;
const NARRATIVE_INSIGHTS_LIMIT = 5;

function rangeWord(range: "7d" | "30d" | "90d"): string {
if (range === "7d") {
return "week";
}
if (range === "30d") {
return "month";
}
return "quarter";
}

function buildDeterministicNarrative(
range: "7d" | "30d" | "90d",
topInsights: {
title: string;
severity: string;
websiteName: string | null;
}[]
): string {
const word = rangeWord(range);
if (topInsights.length === 0) {
return `All systems healthy this ${word}. No actionable signals detected.`;
}
const headline = topInsights[0];
const siteSuffix = headline.websiteName ? ` on ${headline.websiteName}` : "";
if (topInsights.length === 1) {
return `This ${word}: ${headline.title}${siteSuffix}.`;
}
const extra = topInsights.length - 1;
return `This ${word}: ${headline.title}${siteSuffix}, plus ${extra} more signal${extra === 1 ? "" : "s"} worth reviewing.`;
}

const generateNarrativeCached = cacheable(
async function generateNarrativeCached(
organizationId: string,
range: "7d" | "30d" | "90d"
): Promise<{ narrative: string }> {
const topInsights = await db
.select({
title: analyticsInsights.title,
description: analyticsInsights.description,
severity: analyticsInsights.severity,
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Warning: DB query doesn't filter by range, so the narrative may be misleading.

generateNarrativeCached receives the range parameter (7d/30d/90d) and passes it to the LLM prompt ("summarize the last 7d"), but the actual DB query pulls the top 5 insights by priority for the org with no date filter. This means a "this week" narrative could surface stale insights from months ago.

Consider adding a createdAt filter to scope the query to the requested range:

const cutoff = dayjs().subtract(
  range === "7d" ? 7 : range === "30d" ? 30 : 90,
  "day"
).toDate();

// ...
.where(
  and(
    eq(analyticsInsights.organizationId, organizationId),
    gte(analyticsInsights.createdAt, cutoff)
  )
)

changePercent: analyticsInsights.changePercent,
websiteName: websites.name,
})
Comment on lines +966 to +979
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Range parameter not applied to DB query

generateNarrativeCached accepts range and injects it into the LLM prompt's time context ("over the last 7d/30d/90d"), but the Drizzle query has no gte(analyticsInsights.createdAt, cutoff) filter — it always fetches the top-5 by priority from all-time data. Every range selection returns the same underlying insights, producing narratives that say "this quarter" vs "this week" about the exact same signals. Every other date-scoped query in this file (e.g. the main feed at line 862, the recency check at line 233) adds a cutoff guard; this one is missing it.

// before the `.limit(...)` call, compute and apply a cutoff:
const days = range === "7d" ? 7 : range === "30d" ? 30 : 90;
const cutoff = dayjs().subtract(days, "day").toDate();
.where(
  and(
    eq(analyticsInsights.organizationId, organizationId),
    gte(analyticsInsights.createdAt, cutoff),
  )
)

.from(analyticsInsights)
.innerJoin(websites, eq(analyticsInsights.websiteId, websites.id))
.where(eq(analyticsInsights.organizationId, organizationId))
.orderBy(desc(analyticsInsights.priority))
.limit(NARRATIVE_INSIGHTS_LIMIT);

if (topInsights.length === 0) {
return {
narrative: `All systems healthy this ${rangeWord(range)}. No actionable signals detected.`,
};
}

const insightLines = topInsights.map((ins) => {
const site = ins.websiteName ? ` [${ins.websiteName}]` : "";
const change =
ins.changePercent == null
? ""
: ` (${ins.changePercent > 0 ? "+" : ""}${ins.changePercent.toFixed(0)}%)`;
return `- [${ins.severity}] ${ins.title}${change}${site}: ${ins.description ?? ""}`;
});

const prompt = `You are an analytics assistant summarizing an organization's state over the last ${range}.

Write a crisp 2–3 sentence executive summary of the top insights below.

Rules:
- Lead with the most important change
- Include concrete numbers when available
- Never exceed 60 words total
- State facts, do not editorialize
- If nothing meaningful is happening, say so plainly

Top signals this ${range}:
${insightLines.join("\n")}`;

let narrative = "";
try {
const result = await generateText({
model: models.analytics,
prompt,
temperature: 0.2,
maxOutputTokens: 200,
});
narrative = result.text.trim();
} catch (error) {
useLogger().warn("Narrative LLM call failed", {
insights: { organizationId, range, error },
});
}

if (!narrative) {
narrative = buildDeterministicNarrative(range, topInsights);
mergeWideEvent({ insights_narrative_fallback: true });
}

return { narrative };
},
{
expireInSec: NARRATIVE_CACHE_TTL_SECS,
prefix: "insights-narrative",
}
);

export const insights = new Elysia({ prefix: "/v1/insights" })
.derive(async ({ request }) => {
const session = await auth.api.getSession({ headers: request.headers });
Expand Down Expand Up @@ -1059,6 +1172,78 @@ export const insights = new Elysia({ prefix: "/v1/insights" })
}),
}
)
.get(
"/org-narrative",
async ({ query, user, set }) => {
const userId = user?.id;
if (!userId) {
return { success: false, error: "User ID required" };
}

const { organizationId, range } = query;
mergeWideEvent({
insights_narrative_org_id: organizationId,
insights_narrative_range: range,
});

const memberships = await db.query.member.findMany({
where: eq(member.userId, userId),
columns: { organizationId: true },
});

const orgIds = new Set(memberships.map((m) => m.organizationId));
if (!orgIds.has(organizationId)) {
mergeWideEvent({ insights_narrative_access: "denied" });
set.status = 403;
return { success: false, error: "Access denied to this organization" };
}

const rl = await rateLimit(
`insights:narrative:${organizationId}:${userId}`,
NARRATIVE_RATE_LIMIT,
NARRATIVE_RATE_WINDOW_SECS
);
const rlHeaders = getRateLimitHeaders(rl);
for (const [key, value] of Object.entries(rlHeaders)) {
set.headers[key] = value;
}
if (!rl.success) {
set.status = 429;
return {
success: false,
error: "Rate limit exceeded. Try again later.",
};
}

try {
const { narrative } = await generateNarrativeCached(
organizationId,
range
);
return {
success: true,
narrative,
generatedAt: new Date().toISOString(),
};
} catch (error) {
captureError(error, {
insights_narrative_org_id: organizationId,
insights_narrative_range: range,
});
useLogger().warn("Failed to generate org narrative", {
insights: { organizationId, range, error },
});
set.status = 500;
return { success: false, error: "Could not generate narrative" };
}
},
{
query: t.Object({
organizationId: t.String(),
range: t.Union([t.Literal("7d"), t.Literal("30d"), t.Literal("90d")]),
}),
}
)
.post(
"/clear",
async ({ body, user, set }) => {
Expand Down
Loading
Loading