- Business page returned 500: getTrialFunnel's `ORDER BY CASE bucket ...`
referenced the SELECT-list alias inside an expression, which Postgres
can't resolve (error 42703 "column bucket does not exist"). Wrap the
GROUP BY in a subquery so `bucket` is a real column the ORDER BY can use.
Verified against prod: original errors, fixed query returns ordered buckets.
- Remove the "Çözülen / Çözülemeyen VIN'ler" dashboard buttons — the VIN
list already has a success/error status filter, so they were redundant.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Systematize Faz 4 + close three observability gaps:
- WMI opportunity radar (getWmiOpportunities): group query_logs by
substring(vin,1,3) = WMI, rank low-success WMIs by distinct-user demand.
brandMatched=0 flags fully-uncovered manufacturer codes. Surfaced on the
Trends page (#wmi) + a dashboard header shortcut. Replaces the manual Faz 4.
- Per-source p50: add percentile_cont(0.50) to getProviderAttempts and
getProviderDeepStats; show P50·P95 on the dashboard and provider drill-down.
- unknown_vin_spike anomaly: track "tanınamadı" rate in the 15min/baseline
windows, fire when it jumps >=2x baseline (or surges from ~0), and attribute
the dominant failing source (+aborted count) in the Telegram message. Runs
in the existing 5min anomaly cron; worker/telegram unchanged (generic type).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add two header buttons on the VIN decode dashboard linking to the
existing /vins list pre-filtered by success=true / success=false,
carrying the current time-range window via the `from` param so the
list matches the dashboard view. Shows actual VIN codes (not counts).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes Phase 8: long-term capacity view + per-provider deep dive.
Alert-threshold config UI deferred to teknikborc — the cron defaults
are well-calibrated enough to ship without a config table.
Trends page — /projects/sase/vin-decode/trends
1. Son 90 gün — daily decode volume sparkline, success/total ratio
shading.
2. Haftalık büyüme — WoW % change for last 12 weeks, color-coded.
3. Provider yük dağılımı — stacked area for the last 30 days, one bar
per day decomposed by winning provider; legend below.
4. Düşük destekli markalar — brands with ≥20 30d volume but ≤75%
success. New-provider/decoder investment signal.
5. Peak heatmap — 7×24 grid (day-of-week × hour) for the last 30d,
intensity-shaded; B2B working-hours pattern + infra scaling guide.
6. Cache hit trend — daily cache hit rate sparkline + 30d average.
7. Verimlilik composite — daily score combining success (50%), cache
(20%), latency (20%, 1.0 at <500ms decaying to 0 at >10s), and
1-fallback (10%). 14-day table view.
Provider drill-down — /providers/[name]?range=7d|30d
- Four KPIs: wins total, success rate, avg/P95 latency, chain-attempt
count (separately tracked for chain key vs winning-source).
- Daily wins sparkline scoped to the range.
- Top 10 brands the provider successfully decoded for.
- Fallback chains the provider participated in.
- 20 most recent failures with sanitized VIN + user-link drill.
Dashboard wiring
- Winning-provider and provider-attempts cards on the main VIN
dashboard now link each provider row to /providers/[name]. The
timings-key aliases (pcat → parts-catalogs, vin_api → vin-api) are
remapped so the link reaches the right page.
- New "Trends →" pill in the header next to "Business →".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sase.tr has no monthly VIN quota — access is gated by plan.brandCount
+ user_brands assignments. The PRD's Faz 3 quota-centric metrics don't
apply; this commit ships the 8 business signals that DO apply to that
model. All queries are RO joins of query_logs to subscription/plan/brand.
1. Tier × decode volume (30d) — unique users, decode count, decodes/user,
and success rate per plan. Plan derived from each user's most-recent
active/trial subscription.
2. Brand-tier alignment — for plans with brandCount > 0, the fraction
of decodes that landed on a brand the user actually has access to via
user_brands. Low alignment = wrong tier or upsell signal.
3. Dormant payers — active subscription + no decode in the last 14 days.
Sorted by lastDecodeAt ASC (most dormant first), 30 rows.
4. Empty-handed payers — active subscription + zero lifetime decodes.
Onboarding-broken signal, sorted oldest start_date first.
5. Trial → paid funnel — bucket trial users by decodes-during-trial
(0, 1–2, 3–5, 6–10, 11–25, 26+) and show the conversion rate (any
subsequent active/cancelled subscription) per bucket. Answers
"how many decodes is the aha moment".
6. Retention cohort — weekly signup cohorts (mature ≥30d), split by
"had a successful decode in first 7 days" vs not; columns show 30d
active rate per bucket and the retention lift (pp difference).
7. Power users — top 20 by 30d decode count: plan, decode count,
brand diversity, top brand + occurrences, success rate.
8. Tier mismatch — brand-spesifik plan kullanıcıları whose 30d decodes
include ≥3 hits on brands they don't have access to. Upsell candidates
(or refund/downgrade conversation).
All tables link emails to /projects/sase/users/[id] (existing detail page),
so the dashboard is a "find users to talk to" tool, not just numbers.
VIN dashboard header now has a "Business →" pill next to the time range
picker.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three additions to the VIN Decode Observability dashboard, all read-only.
Faz 2a — Error fingerprint drill-down
- New route /projects/sase/vin-decode/errors/[key] where [key] is one
of the eight bucket keys (BUDGET_EXCEEDED, UNKNOWN_VIN, TIMEOUT, …).
- getErrorBucketDetail(key, range) returns: total + unique-user count,
hourly time series, top 10 affected brands (joined to brands.name/
slug), top providers (winning + chain attempts from timings keys),
top 20 affected users with link to their detail page, six distinct
sample error messages, last 30 raw failures (sanitized VIN, source,
RT, message).
- Error breakdown cards on the main dashboard now link to this page
with the current time range preserved.
- ERROR_BUCKET_KEYS + ErrorBucketKey exported so the [key] route
validates against the same list.
Faz 2b — Deploy regression analysis
- listSaseDeploys(limit) fetches the Sase.tr Coolify app's recent
finished deployments via the Coolify API (COOLIFY_API_TOKEN).
- analyzeDeployRegressions(deploys) slices a 30-minute window
before deploy.startedAt and after deploy.finishedAt, computes the
success-rate delta, and flags rows where the drop ≥ 10pp (and
both windows have ≥ 5 samples).
- Dashboard card lists last ~10 deploys with before/after rates and
Δ pp, color-coded; "regression" badge on flagged rows.
Faz 2c — BIP integration
- getVinRelatedInsights() reads from the panel-pg `insights` table:
type='provider_quality' (always VIN-related by prompt-tag routing)
+ bug_triage insights whose body mentions VIN/decode/provider
keywords. Deduped, severity-then-recency sorted.
- New dashboard card surfaces insight titles with severity + type
badges, occurrence/user counts, confidence, and a link to the
Gitea issue if one was opened.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Süper Panel VIN Decode Observability Module (SP-VIN-001), Faz 1 MVP.
Reads directly from Sase.tr's query_logs via DATABASE_URL_SASE_RO —
no PostHog hop, no ingestion worker, no aggregation tables. Sase.tr's
existing query_logs.timings jsonb already captures the full chain
context (per-provider durations, cache source, circuit-breaker state).
Repo (apps/web/src/lib/sase/vin-decode.ts)
- getOperationalHealth(range) — total/success/failure counts, success
& error rates, P50/P95/P99 + avg response time, cache hit rate,
timeout rate, unknown-VIN rate. Single raw query with FILTER aggs.
- getWinningProviders(range) — group by source (decoded by), share +
avg response time per provider.
- getProviderAttempts(range) — group by timings jsonb keys
(pcat/emex/pl24/vin_api). A row may contribute to multiple providers
if the chain ran through several.
- getFallbackChains(range) — multi-provider rows grouped into chain
strings (e.g. 'pcat→emex→pl24') with frequency.
- getErrorBreakdown(range) — bucket errors via regex
(BUDGET_EXCEEDED, UNKNOWN_VIN, TIMEOUT, …) on errorMessage.
- getDailyTrend(days) — date_trunc('day') volume + success + failure
+ avg latency. Filled with zero-rows so the sparkline is continuous.
- getBrandBreakdown(range, limit) — top brands joined to brands.name/slug
with per-brand success rate.
Dashboard (/projects/sase/vin-decode)
- 4-button time range picker (1h/24h/7d/30d) via URL search-params.
- 6 KPI cards (Success, Error, P95 RT, Cache hit, Timeout, Unknown VIN)
color-coded by threshold.
- Two parallel provider views: "winning provider" (decode resolver)
vs "provider attempt" (timings keys — wider, includes fallthroughs).
- Fallback chain frequency table.
- Error bucket cards with example messages.
- Top brands by query volume with success rate cells (green/red).
- 30-day daily-volume sparkline with success/total ratio shading.
Sase landing gets a "VIN Decode →" link next to the Users link.
No new tables, no migrations, no workers. RO-only by design.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>