diff --git a/.env.example b/.env.example
index 1498847..e460a78 100644
--- a/.env.example
+++ b/.env.example
@@ -1,2 +1,4 @@
# Same-origin by default. Vite proxies /api to http://127.0.0.1:8080 locally.
PUBLIC_API_BASE_URL=/api/v1
+# Keep disabled until the backend growth-report capability and budgets are configured.
+PUBLIC_AI_GROWTH_ENABLED=false
diff --git a/src/lib/components/AnalyticsPage.svelte b/src/lib/components/AnalyticsPage.svelte
index ea28619..873b004 100644
--- a/src/lib/components/AnalyticsPage.svelte
+++ b/src/lib/components/AnalyticsPage.svelte
@@ -30,7 +30,9 @@
retryOpportunity,
} from '$lib/insights';
import { formatPeerMetric, peerMetricLabel } from '$lib/peer-metrics';
+ import { growthFeatureEnabled } from '$lib/growth-feature';
import KpiCard from './KpiCard.svelte';
+ import GrowthTeaser from './GrowthTeaser.svelte';
import FeeShareDonut from './FeeShareDonut.svelte';
import PageHeader from './PageHeader.svelte';
import PaymentHistoryChart from './PaymentHistoryChart.svelte';
@@ -204,6 +206,7 @@
+ {#if growthFeatureEnabled}{/if}
{:else if kind === 'payments' && performance.state === 'READY'}
{@const retryRate = retryEngagementRate(
performance.data.retriedSessions.value,
diff --git a/src/lib/components/AppShell.svelte b/src/lib/components/AppShell.svelte
index 9e06ab5..7f5f52b 100644
--- a/src/lib/components/AppShell.svelte
+++ b/src/lib/components/AppShell.svelte
@@ -9,6 +9,7 @@
import MoonIcon from '@lucide/svelte/icons/moon';
import PanelsTopLeftIcon from '@lucide/svelte/icons/panels-top-left';
import SunIcon from '@lucide/svelte/icons/sun';
+ import TrendingUpIcon from '@lucide/svelte/icons/trending-up';
import UsersRoundIcon from '@lucide/svelte/icons/users-round';
import WalletCardsIcon from '@lucide/svelte/icons/wallet-cards';
@@ -18,6 +19,7 @@
import * as Sidebar from '$lib/components/ui/sidebar';
import { useDashboardContext } from '$lib/dashboard-context.svelte';
import { setDashboardQuery } from '$lib/dashboard-filters';
+ import { growthFeatureEnabled } from '$lib/growth-feature';
let { children } = $props();
const dashboard = useDashboardContext();
@@ -29,6 +31,7 @@
{ href: '/payments', label: 'عملکرد پرداخت', icon: ActivityIcon },
{ href: '/fees', label: 'کارمزدها', icon: HandCoinsIcon },
{ href: '/peers', label: 'همگروهها', icon: UsersRoundIcon },
+ ...(growthFeatureEnabled ? [{ href: '/growth', label: 'تحلیلگر رشد', icon: TrendingUpIcon }] : []),
];
function navUrl(path: string) {
diff --git a/src/lib/components/GrowthForecastChart.svelte b/src/lib/components/GrowthForecastChart.svelte
new file mode 100644
index 0000000..c25d461
--- /dev/null
+++ b/src/lib/components/GrowthForecastChart.svelte
@@ -0,0 +1,264 @@
+
+
+
+
+
+
+
+ چشمانداز ۹۰ روز آینده
+
+
+ خط میانی برآورد معمول و ناحیه کمرنگ بازه محتمل را نشان میدهد.
+
+
+
+
{formatConfidence(confidence)}
+
+
+
+
+
+
+
+
+ {#if chart.points.length}
+ (active = active ?? 0)}
+ onblur={() => (active = null)}
+ >
+
+ {#if selected}
+
+ {dateLabel(selected.localDate)}
+ برآورد معمول: {valueLabel(selected.p50)}
+ بازه محتمل: {valueLabel(selected.p10)} تا {valueLabel(selected.p90)}
+
+ {/if}
+
+
+ شروع پیشبینی: {dateLabel(startDate)}. این برآورد قطعی نیست و بر پایه الگوی دادههای کامل واردشده است.
+
+
+ جدول دادههای نمودار
+
+
+ | تاریخ | کمینه محتمل | برآورد معمول | بیشینه محتمل |
+
+ {#each chart.points as point}
+ | {dateLabel(point.localDate)} | {valueLabel(point.p10)} | {valueLabel(point.p50)} | {valueLabel(point.p90)} |
+ {/each}
+
+
+
+
+ {:else}
+
+ برای این بازه پیشبینی قابل اتکایی آماده نشده است.
+
+ {/if}
+
+
+
+
diff --git a/src/lib/components/GrowthForecastChart.test.ts b/src/lib/components/GrowthForecastChart.test.ts
new file mode 100644
index 0000000..4de7943
--- /dev/null
+++ b/src/lib/components/GrowthForecastChart.test.ts
@@ -0,0 +1,31 @@
+import { fireEvent, render, screen } from '@testing-library/svelte';
+import { describe, expect, it } from 'vitest';
+
+import GrowthForecastChart from './GrowthForecastChart.svelte';
+
+const points = [
+ {
+ date: '2026-07-01T00:00:00Z',
+ paidSessions: { p10: 80, p50: 100, p90: 130 },
+ paidAmountRial: { p10: 8_000_000, p50: 10_000_000, p90: 13_000_000 },
+ },
+ {
+ date: '2026-07-02T00:00:00Z',
+ paidSessions: { p10: 90, p50: 120, p90: 150 },
+ paidAmountRial: { p10: 9_000_000, p50: 12_000_000, p90: 15_000_000 },
+ },
+];
+
+describe('GrowthForecastChart', () => {
+ it('offers an accessible table alternative and amount mode', async () => {
+ render(GrowthForecastChart, {
+ points,
+ confidence: 'MEDIUM',
+ startDate: '2026-07-01T00:00:00Z',
+ });
+
+ expect(screen.getByText('جدول دادههای نمودار')).not.toBeNull();
+ await fireEvent.click(screen.getByRole('button', { name: 'مبلغ' }));
+ expect(screen.getAllByText(/ریال/).length).toBeGreaterThan(0);
+ });
+});
diff --git a/src/lib/components/GrowthReportSkeleton.svelte b/src/lib/components/GrowthReportSkeleton.svelte
new file mode 100644
index 0000000..99a60fb
--- /dev/null
+++ b/src/lib/components/GrowthReportSkeleton.svelte
@@ -0,0 +1,30 @@
+
+
+
+
{refreshing
+ ? 'تحلیل قبلی نمایش داده میشود و نسخه تازه در حال آمادهشدن است.'
+ : 'اطلاعات تحلیل رشد در حال بارگذاری است.'}
+ {#if refreshing}
+
+ {:else}
+
+
+
+
+
+
+ {/if}
+
diff --git a/src/lib/components/GrowthTeaser.svelte b/src/lib/components/GrowthTeaser.svelte
new file mode 100644
index 0000000..025d115
--- /dev/null
+++ b/src/lib/components/GrowthTeaser.svelte
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
تحلیلگر رشد
+
+ فرصتهای پیش رو و دورههای کمرونق احتمالی را با تکیه بر دادههای خودتان ببینید.
+
+
+
+
+ دیدن تحلیل ۹۰ روزه
+
+
+
+
diff --git a/src/lib/components/GrowthTeaser.test.ts b/src/lib/components/GrowthTeaser.test.ts
new file mode 100644
index 0000000..5716982
--- /dev/null
+++ b/src/lib/components/GrowthTeaser.test.ts
@@ -0,0 +1,15 @@
+import { render, screen } from '@testing-library/svelte';
+import { describe, expect, it } from 'vitest';
+
+import GrowthTeaser from './GrowthTeaser.svelte';
+
+describe('GrowthTeaser', () => {
+ it('links to the growth analyst without starting a generation request', () => {
+ render(GrowthTeaser, { href: '/growth?terminal=terminal-1' });
+
+ expect(screen.getByRole('link', { name: /دیدن تحلیل ۹۰ روزه/ }).getAttribute('href')).toBe(
+ '/growth?terminal=terminal-1',
+ );
+ expect(screen.getByText('تحلیلگر رشد')).not.toBeNull();
+ });
+});
diff --git a/src/lib/growth-chart.test.ts b/src/lib/growth-chart.test.ts
new file mode 100644
index 0000000..6f88fcb
--- /dev/null
+++ b/src/lib/growth-chart.test.ts
@@ -0,0 +1,34 @@
+import { describe, expect, it } from 'vitest';
+
+import { buildForecastChart, formatConfidence } from './growth-chart';
+
+describe('growth forecast chart presentation', () => {
+ it('maps the p10, p50 and p90 series into a shared bounded plot', () => {
+ const chart = buildForecastChart(
+ [
+ { localDate: '2026-07-01', p10: 80, p50: 100, p90: 130 },
+ { localDate: '2026-07-02', p10: 90, p50: 120, p90: 150 },
+ ],
+ 760,
+ 300,
+ );
+
+ expect(chart.points).toHaveLength(2);
+ expect(chart.points[0].highY).toBeLessThan(chart.points[0].medianY);
+ expect(chart.points[0].medianY).toBeLessThan(chart.points[0].lowY);
+ expect(chart.points.every((point) => point.x >= chart.padding.left)).toBe(true);
+ });
+
+ it('keeps the chart useful when every forecast value is zero', () => {
+ const chart = buildForecastChart([{ localDate: '2026-07-01', p10: 0, p50: 0, p90: 0 }], 760, 300);
+
+ expect(chart.max).toBe(1);
+ expect(chart.medianPath).toContain('M');
+ });
+
+ it('translates forecast confidence without implying certainty', () => {
+ expect(formatConfidence('HIGH')).toBe('اطمینان بالا');
+ expect(formatConfidence('MEDIUM')).toBe('اطمینان متوسط');
+ expect(formatConfidence('INSUFFICIENT')).toBe('داده ناکافی');
+ });
+});
diff --git a/src/lib/growth-chart.ts b/src/lib/growth-chart.ts
new file mode 100644
index 0000000..53b3b17
--- /dev/null
+++ b/src/lib/growth-chart.ts
@@ -0,0 +1,47 @@
+export type ForecastChartDatum = {
+ localDate: string;
+ p10: number;
+ p50: number;
+ p90: number;
+};
+
+export type ForecastChartPoint = ForecastChartDatum & {
+ x: number;
+ lowY: number;
+ medianY: number;
+ highY: number;
+};
+
+export function buildForecastChart(data: ForecastChartDatum[], width: number, height: number) {
+ const padding = { top: 24, right: 24, bottom: 52, left: 72 };
+ const innerWidth = width - padding.left - padding.right;
+ const innerHeight = height - padding.top - padding.bottom;
+ const max = Math.max(1, ...data.map((point) => point.p90));
+ const y = (value: number) => height - padding.bottom - (value / max) * innerHeight;
+ const points: ForecastChartPoint[] = data.map((point, index) => ({
+ ...point,
+ x: padding.left + (index * innerWidth) / Math.max(1, data.length - 1),
+ lowY: y(point.p10),
+ medianY: y(point.p50),
+ highY: y(point.p90),
+ }));
+ const medianPath = points
+ .map((point, index) => `${index ? 'L' : 'M'} ${point.x} ${point.medianY}`)
+ .join(' ');
+ const bandPath = points.length
+ ? `${points.map((point, index) => `${index ? 'L' : 'M'} ${point.x} ${point.highY}`).join(' ')} ${[
+ ...points,
+ ]
+ .reverse()
+ .map((point) => `L ${point.x} ${point.lowY}`)
+ .join(' ')} Z`
+ : '';
+
+ return { points, max, padding, medianPath, bandPath };
+}
+
+export function formatConfidence(value: string): string {
+ if (value === 'HIGH') return 'اطمینان بالا';
+ if (value === 'MEDIUM') return 'اطمینان متوسط';
+ return 'داده ناکافی';
+}
diff --git a/src/lib/growth-feature.ts b/src/lib/growth-feature.ts
new file mode 100644
index 0000000..03c7a1f
--- /dev/null
+++ b/src/lib/growth-feature.ts
@@ -0,0 +1 @@
+export const growthFeatureEnabled = import.meta.env.PUBLIC_AI_GROWTH_ENABLED === 'true';
diff --git a/vite.config.ts b/vite.config.ts
index fc1e193..0bf5c52 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -7,6 +7,9 @@ export default defineConfig(({ mode }) => {
const env = loadEnv(mode, '.', '');
return {
plugins: [tailwindcss(), sveltekit()],
+ resolve: {
+ conditions: mode === 'test' ? ['browser'] : undefined,
+ },
server: {
host: '127.0.0.1',
port: 5173,