# Dashboard & Analytics Feature Design

**Date:** 2025-12-04
**Status:** Ready for implementation
**Branch:** `feature/dashboard-analytics`
**Worktree:** `.worktrees/dashboard-analytics` (port 3007)

## Overview

Add performance metrics to the dealer dashboard showing traffic (pageviews) and inquiries (form submissions). Enhanced and Professional tier dealers see metrics; Starter and Growth tiers see an upgrade prompt.

## Requirements Summary

From Joe's mockup and stakeholder input:

- **Traffic metric:** Pageviews (simple count per request)
- **Inquiries metric:** Form submissions (stored in database)
- **Eligible tiers:** Enhanced and Professional only
- **Time periods:** 7 days, 30 days, 90 days, YTD, custom date range
- **Trend indicators:** Compare current period to previous period (green/red arrows)
- **Click tracking:** All link clicks categorized by type (AMSOIL store, internal, social, external)

## Tier Access

| Tier         | Lead Form | Metrics Dashboard   |
| ------------ | --------- | ------------------- |
| Starter      | No        | No - upgrade prompt |
| Growth       | Yes       | No - upgrade prompt |
| Enhanced     | Yes       | Yes                 |
| Professional | Yes       | Yes                 |

## Data Model

Three new Prisma models:

```prisma
model PageView {
  id        String   @id @default(cuid())
  dealerId  String
  dealer    Dealer   @relation(fields: [dealerId], references: [id], onDelete: Cascade)
  path      String   // e.g., "/", "/about", "/custom-page"
  referrer  String?  // where visitor came from
  userAgent String?  // for bot filtering later
  timestamp DateTime @default(now())

  @@index([dealerId, timestamp])
  @@index([dealerId, path, timestamp])
}

model LinkClick {
  id         String   @id @default(cuid())
  dealerId   String
  dealer     Dealer   @relation(fields: [dealerId], references: [id], onDelete: Cascade)
  sourcePath String   // page where click happened
  targetHref String   // link destination
  clickType  String   // 'amsoil' | 'internal' | 'social' | 'external'
  timestamp  DateTime @default(now())

  @@index([dealerId, timestamp])
  @@index([dealerId, clickType, timestamp])
}

model Lead {
  id        String   @id @default(cuid())
  dealerId  String
  dealer    Dealer   @relation(fields: [dealerId], references: [id], onDelete: Cascade)
  name      String
  email     String
  message   String?
  timestamp DateTime @default(now())

  @@index([dealerId, timestamp])
}
```

### Capacity Planning

- 3,500 dealers max
- ~100 pageviews/day/dealer average = 350,000 rows/day
- ~127 million pageviews/year, ~25 million clicks/year
- PostgreSQL handles this easily with proper indexes
- Dashboard queries: <10ms with composite indexes

### Future: Pre-aggregation

If needed later, add daily rollup table:

```sql
CREATE TABLE daily_pageview_stats (
  dealer_id    TEXT,
  path         TEXT,
  date         DATE,
  view_count   INT,
  PRIMARY KEY (dealer_id, path, date)
);
```

Populate via nightly cron job. Event log remains source of truth.

## API Design

### Recording Endpoint

```
POST /api/stats/events
Content-Type: application/json

# Pageview event
{ "type": "pageview", "path": "/", "referrer": "https://google.com" }

# Click event
{ "type": "click", "sourcePath": "/", "targetHref": "https://amsoil.com/shop", "clickType": "amsoil" }

Response: 204 No Content
```

- Dealer ID derived from subdomain (middleware context or request header)
- Fire-and-forget semantics (non-blocking)
- Silent failure (analytics never break the user experience)

### Dashboard Endpoint

```
GET /api/stats/dashboard?period=30d
Authorization: Bearer <session>

Response:
{
  "traffic": {
    "current": 1234,
    "previous": 1100,
    "changePercent": 12.2
  },
  "inquiries": {
    "current": 56,
    "previous": 48,
    "changePercent": 16.7
  },
  "clicks": {
    "amsoil": { "current": 89, "previous": 72, "changePercent": 23.6 },
    "internal": { "current": 234, "previous": 201, "changePercent": 16.4 },
    "social": { "current": 45, "previous": 38, "changePercent": 18.4 },
    "external": { "current": 12, "previous": 15, "changePercent": -20.0 }
  },
  "period": {
    "start": "2025-11-04",
    "end": "2025-12-04",
    "previousStart": "2025-10-05",
    "previousEnd": "2025-11-04"
  }
}

Headers:
  Cache-Control: private, max-age=300
```

**Period options:** `7d`, `30d`, `90d`, `ytd`, `custom` (with `start` and `end` query params)

**Tier gating:** Returns 403 for Starter/Growth:

```json
{ "error": "upgrade_required", "requiredTier": "enhanced" }
```

## Tracking Implementation

### Server-side Pageview Tracking

In `middleware.ts`, after subdomain detection:

```typescript
if (subdomain) {
  // Fire-and-forget: don't await, don't block the response
  fetch(`${baseUrl}/api/stats/events`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'X-Dealer-Subdomain': subdomain },
    body: JSON.stringify({
      type: 'pageview',
      path: request.nextUrl.pathname,
      referrer: request.headers.get('referer'),
      userAgent: request.headers.get('user-agent'),
    }),
  }).catch(() => {}); // Silent failure

  return NextResponse.rewrite(new URL(`/dealers/${subdomain}`, request.url));
}
```

### Client-side Click Tracking

New component injected into dealer page layout:

```typescript
// components/analytics/ClickTracker.tsx
'use client';

import { useEffect } from 'react';

function categorizeLink(href: string): string {
  if (href.includes('amsoil.com')) return 'amsoil';
  if (href.startsWith('/') || href.includes(window.location.host)) return 'internal';
  if (/facebook|twitter|instagram|linkedin|youtube/.test(href)) return 'social';
  return 'external';
}

export function ClickTracker({ subdomain }: { subdomain: string }) {
  useEffect(() => {
    const handler = (e: MouseEvent) => {
      const link = (e.target as Element).closest('a[href]');
      if (!link) return;

      const href = link.getAttribute('href') || '';
      navigator.sendBeacon(
        '/api/stats/events',
        JSON.stringify({
          type: 'click',
          sourcePath: window.location.pathname,
          targetHref: href,
          clickType: categorizeLink(href),
        })
      );
    };

    document.addEventListener('click', handler);
    return () => document.removeEventListener('click', handler);
  }, [subdomain]);

  return null;
}
```

## Contact Form Modification

Current state: Email-only via Resend (never configured, returns 500).

New behavior: Store lead in database, skip email for now.

```typescript
// app/api/contact/route.ts

export async function POST(request: Request) {
  const { name, email, message } = await request.json();
  const subdomain =
    request.headers.get('x-dealer-subdomain') ||
    extractSubdomainFromHost(request.headers.get('host'));

  const dealer = await prisma.dealer.findUnique({
    where: { subdomain },
  });

  if (!dealer) {
    return Response.json({ error: 'Invalid dealer' }, { status: 400 });
  }

  await prisma.lead.create({
    data: {
      dealerId: dealer.id,
      name,
      email,
      message,
    },
  });

  // Future: email notification if RESEND_API_KEY configured

  return Response.json({ success: true });
}
```

## Dashboard UI

### Enhanced+ Tiers (Metrics Visible)

```tsx
// components/dashboard/PerformanceMetrics.tsx

<section className="your-sites-performance">
  <h3>Your Site's Performance</h3>

  <div className="metrics-grid">
    <MetricCard
      label="Traffic"
      value={stats.traffic.current}
      subLabel="Visits"
      changePercent={stats.traffic.changePercent}
    />
    <MetricCard
      label="Inquiries"
      value={stats.inquiries.current}
      subLabel="Form Submissions"
      changePercent={stats.inquiries.changePercent}
    />
  </div>

  <PeriodSelector
    options={['7d', '30d', '90d', 'ytd']}
    allowCustomRange
    defaultValue="30d"
    onChange={setPeriod}
  />
</section>
```

### Starter/Growth Tiers (Upgrade Prompt)

```tsx
<section className="your-sites-performance upgrade-prompt">
  <h3>Your Site's Performance</h3>
  <p>See how your site is performing with traffic and lead metrics.</p>
  <Button href="/dashboard/subscription">Upgrade to Enhanced</Button>
</section>
```

### MetricCard Component

```tsx
interface MetricCardProps {
  label: string;
  value: number;
  subLabel: string;
  changePercent: number;
}

function MetricCard({ label, value, subLabel, changePercent }: MetricCardProps) {
  const isPositive = changePercent >= 0;

  return (
    <div className="metric-card">
      <span className="label">{label}</span>
      <div className="value-row">
        <span className="value">{value.toLocaleString()}</span>
        <span className={`trend ${isPositive ? 'positive' : 'negative'}`}>
          {isPositive ? '↑' : '↓'} {Math.abs(changePercent).toFixed(1)}%
        </span>
      </div>
      <span className="sub-label">{subLabel}</span>
    </div>
  );
}
```

## Implementation Phases

### Dependency Graph

```
                    ┌─────────────────┐
                    │  Phase 1: Data  │
                    │  (Prisma models)│
                    └────────┬────────┘
                             │
              ┌──────────────┴──────────────┐
              │                             │
              ▼                             ▼
┌─────────────────────────┐   ┌─────────────────────────┐
│  Phase 2: Recording     │   │  Phase 3: Reading       │
│                         │   │                         │
│  - POST /api/stats/     │   │  - GET /api/stats/      │
│    events               │   │    dashboard            │
│  - Middleware tracking  │   │  - Tier gating          │
│  - ClickTracker         │   │  - HTTP caching         │
│  - Contact form mod     │   │                         │
└─────────────────────────┘   └────────────┬────────────┘
                                           │
                                           ▼
                              ┌─────────────────────────┐
                              │  Phase 4: Dashboard UI  │
                              │                         │
                              │  - PerformanceMetrics   │
                              │  - MetricCard           │
                              │  - PeriodSelector       │
                              │  - UpgradePrompt        │
                              └─────────────────────────┘
```

### Parallel Opportunities

| After...         | Can run in parallel                                       |
| ---------------- | --------------------------------------------------------- |
| Phase 1 complete | Phase 2 + Phase 3 (recording and reading are independent) |
| Phase 3 started  | Phase 4 UI components (mock API, wire up later)           |

**Within Phase 2 (after events endpoint exists):**

- Middleware pageview tracking
- ClickTracker component
- Contact form modification

All three are independent and can be built in parallel.

**Within Phase 4:**

- MetricCard component
- PeriodSelector component
- UpgradePrompt component

All pure UI, can be built in parallel.

### Optimal 2-Person Split

```
Person A: Phase 1 → Phase 2 (all recording infrastructure)
Person B: (wait for Phase 1) → Phase 3 → Phase 4
```

### Phase Details

#### Phase 1: Data Layer

- [ ] Add PageView, LinkClick, Lead models to Prisma schema
- [ ] Add relations to Dealer model
- [ ] Run `prisma migrate dev`
- [ ] Verify indexes created

#### Phase 2: Recording

- [ ] Create `POST /api/stats/events` endpoint
- [ ] Add subdomain header injection in middleware
- [ ] Add pageview tracking call in middleware (fire-and-forget)
- [ ] Create `ClickTracker` component
- [ ] Add `ClickTracker` to dealer page layout
- [ ] Modify contact form to store leads in database
- [ ] Add tests for event recording

#### Phase 3: Reading

- [ ] Create `GET /api/stats/dashboard` endpoint
- [ ] Implement period calculation (7d, 30d, 90d, ytd, custom)
- [ ] Implement previous period comparison
- [ ] Add tier gating (Enhanced+ only)
- [ ] Add Cache-Control headers (max-age=300)
- [ ] Add tests for stats calculation

#### Phase 4: Dashboard UI

- [ ] Create `MetricCard` component with trend arrows
- [ ] Create `PeriodSelector` component with date picker
- [ ] Create `PerformanceMetrics` container component
- [ ] Create `UpgradePrompt` component for lower tiers
- [ ] Integrate into dashboard page with tier check
- [ ] Add loading and error states

## Caching Strategy

- **HTTP caching:** `Cache-Control: private, max-age=300` (5 minutes)
- Analytics data doesn't need real-time accuracy
- Reduces database load for repeated dashboard visits
- Pre-aggregation not needed at current scale

## Testing Approach

- Unit tests for link categorization logic
- Unit tests for period calculation (7d, 30d, 90d, ytd, custom ranges)
- Integration tests for event recording endpoints
- Integration tests for stats calculation with sample data
- Component tests for MetricCard trend display

## Future Enhancements (Out of Scope)

- [ ] Pre-aggregation tables for faster queries at scale
- [ ] Unique visitors (requires cookie/fingerprint tracking)
- [ ] Session-based traffic (30-minute timeout grouping)
- [ ] Referrer breakdown (where traffic comes from)
- [ ] Email notifications for new leads (Resend integration)
- [ ] Lead list view and CSV export
- [ ] Click-through heatmaps

## Reference

- **Joe's mockup:** Shows "Traffic: 1,234 Visits" and "Inquiries: 56 Form Submissions" with green trend arrows
- **Existing stats endpoint:** `/api/stats/cms` (page/blog counts for Professional tier)
- **Tier definitions:** `lib/cms/types.ts` - CMS_TIERS, NAV_EDITOR_TIERS
