# Program analytics (Ordio Loop, admin-only)

**Last Updated:** 2026-04-10

Admin-only **Programm-Analyse** aggregates partner registry data and the HubSpot **cache** for program-level monitoring. There are **no live HubSpot API calls** when you open the page or call the metrics API.

**Canonical URL:** `/partner/program-analytics`  
**Page:** `v2/pages/partner-program-analytics.php`  
**API:** `GET /v2/api/affiliate-admin-program-metrics.php`  
**Aggregation:** `v2/helpers/affiliate-program-aggregates.php`

## Research & KPI framework

B2B partner dashboards often group metrics into **recruitment**, **activation**, **engagement**, and **revenue**. This v1 maps them to available cache/registry fields as follows:

| Bereich        | Was du hier siehst | Datenquelle |
|----------------|--------------------|-------------|
| **Recruitment** | Partner gesamt / aktiv, Verteilung Status & effektives Level | `affiliate_partners.json` + Cache (Level aus Deals) |
| **Activation** | Neue aktive Partner (Fenster) mit 0 Leads im Cache | Registry-Datum + Lead-Count |
| **Engagement** | „Inaktive“ aktive Partner (kein `last_active_at` / `last_login_at` im Schwellen-Fenster) | Registry |
| **Revenue**     | Programm-MRR, Deals (KPI vs. Roh), MRR nach Abschlussmonat (Partner-Anteil) | Cache Deals + `calculatePartnerMRR` |

**Revenue / Deals:** Die Kachel **„Deals“** zeigt **Offen / Gewonnen** (Pipeline vs. Closed-Won mit MRR). Zusätzlich liefert die API `deals_open_pipeline_total`, `deals_closed_lost_total` (nur intern/Admin) und weiterhin `deals_kpi_total` / `deals_raw_total`. Details: [DATA_GLOSSARY.md](./DATA_GLOSSARY.md).

## Cache limits & trends

- **Aktueller Stand:** Der Cache ist ein **Snapshot**. Metriken wie „Leads pro Monat“ oder „Abschlüsse pro Monat“ werden aus **Zeitstempeln** auf Leads/Deals **rekonstruiert**, nicht aus historischen Cache-Versionen.
- **Keine Klickdaten:** Keine Referral-Link-Clicks oder Sessions im Cache – **keine EPC- oder Click-to-Lead-Kennzahlen** ohne separates Tracking.
- **UTM:** Aggregation pro Dimension mit Top 15 + **Sonstige**; fehlende Werte erscheinen als `(ohne Quelle)` / `(ohne Medium)` / `(ohne Kampagne)`. Sparse Daten sind normal.
- **Zeitraum:** Query `from` / `to` als `YYYY-MM-DD`; maximal **24 Monate** (sonst Clamp serverseitig). Standard ohne Parameter: **rollierende 90 Tage** bis „heute“ (UTC-Tagesgrenzen; siehe `affiliateProgramAnalytics_parseDateRange`). UI-Schnellauswahl: Kalender („Letzte Woche“, „Letzter Monat“, **3 Monate** = volle Monate) sowie **Letzte 7 / 30 / 90 Tage** rückwärts ab heute.
- **Trend-Zeitreihen (Verlauf):** Optional `grain=day|week|month`. **API** ohne Parameter: `month` (Abwärtskompatibilität). **Programm-Analyse-UI:** Auswahl **Täglich / Wöchentlich / Monatlich** oben rechts in der **Zeitraum**-Karte; Standard **Woche** (`grain=week` in der URL, sobald die Seite die Parameter setzt). Buckets in **UTC** (Kalendertag, ISO-Woche, Kalendermonat). Sehr lange Zeiträume werden automatisch vergröbert: mehr als **90** Tage bei Tagesansicht → Woche; mehr als **52** Wochen bei Wochenansicht → Monat. Dann liefert `meta.trend_granularity_note` einen kurzen deutschen Hinweis; `granularity_effective` in `trends` und `meta` beschreibt die tatsächliche Auflösung.
- **Monats-/Perioden-Buckets:** UTC (`gmdate`); Kurztext im UI ist lokalisiert, Bucket-Zuordnung ist UTC.

## API

`GET /v2/api/affiliate-admin-program-metrics.php?from=YYYY-MM-DD&to=YYYY-MM-DD&grain=month`

- **Auth:** Affiliate-Session. **403** wenn nicht Admin.
- **Cache-Control:** `private, max-age=60` (wie andere Affiliate-APIs).

Antwort `data` (Kurzüberblick):

- `range` – `from_date`, `to_date`, `clamped`
- `meta` – `last_sync`, `last_sync_age_seconds`, `timezone_note`, `trend_granularity_requested`, `trend_granularity_effective`, optional `trend_granularity_note`, `generated_at`, **`hubspot`** – `portal_id`, `crm_ui_base` (EU UI host), `record_types` (`contact`, `deal`, `affiliate_partner` object type ids for CRM record URLs)
- `kpis` – u. a. `partners_active`, `admins_count`, `leads_in_period`, `deals_kpi_total`, `deals_raw_total`, **`deals_open_pipeline_total`**, **`deals_closed_lost_total`**, `program_mrr_total` / `program_mrr_active` (Partner-Anteil), `program_ordio_mrr_total` / `program_ordio_mrr_active` (Ordio-Anteil), `program_gross_mrr_total` (Kunden-MRR der gewonnenen Deals)
- `funnel_snapshot` – alle gecachten Leads (nicht nur Zeitraum): `counts` (`total`, `new`, `qualified`, `deal`, `won`), `stage_sum`, `stage_matches_total` (Invariante: Summe der vier Stufen = `total`), `rates` (Übergangsquoten), `note`
- `trends` – `granularity_requested`, `granularity_effective`, optional `granularity_note`, `period_keys`, `leads_per_period`, `closed_won_per_period`, `partner_mrr_close_period_eur`; **Legacy-Aliase** (gleiche Arrays): `months`, `leads_per_month`, `closed_won_per_month`, `partner_mrr_close_month_eur`
- `distribution` – `partner_status`, `effective_level`
- `top_partners` – `by_mrr`, `by_leads` (jeweils Einträge mit `hubspot_affiliate_record_id`, wenn bekannt)
- `activation_engagement` – Zähler + Fenster-Tage (Konstanten im Helper)
- `utm` – `source`, `medium`, `campaign` (Arrays `{ key, count }`)
- `period_leads` – `items` (max. `AFFILIATE_PROGRAM_ANALYTICS_LIST_MAX`, neueste zuerst), `total`, `limit`, `truncated`. Pro Zeile: u. a. `hubspot_contact_id`, `hubspot_affiliate_record_id` (wenn in der Registry gesetzt), `partner_id`, `contact`, `status`, `referral_date`, `utm_source`. Ein Lead zählt, wenn **Referral-/Erstell-Zeitpunkt** im gewählten `from`–`to` liegt (gleiche Logik wie `leads_in_period`).
- `period_deals` – wie oben; nur **closed-won**-Deals mit **Abschlussdatum** im Zeitraum und **positivem MRR**. Pro Zeile: u. a. `hubspot_deal_id`, `hubspot_affiliate_record_id`, `partner_id`, `deal_name`, MRR-Felder, `close_date`, `subscription_status`.
- `open_pipeline_deals` – **aktueller Cache-Stand** (ohne Zeitraumfilter): offene Pipeline-Deals (nicht gewonnen, nicht verloren); `items`, `total`, `limit`, `truncated`, `note`.

**HubSpot-Deep-Links (UI):** Die Programm-Analyse rendert Links nur wenn die jeweilige ID gesetzt ist. Partner-Objekt-IDs kommen aus `hubspot_affiliate_record_id` / Legacy `hubspot_object_id` in `affiliate_partners.json` (Schreiben bei Registrierung, OAuth-Create und Sync-Backfill).

**Validierung:** `php v2/scripts/affiliate/validate-program-analytics-consistency.php` prüft die Funnel-Invariante und ob die Summe der Trend-Buckets für Leads mit `kpis.leads_in_period` übereinstimmt (für `grain` month/week/day). Optional: `php v2/scripts/affiliate/validate-affiliate-deal-pipeline-invariants.php` für Deal-Pipeline-Summen.

## UI-Labels vs. API-Felder

Die Seite **Programm-Analyse** nutzt auf dem Bildschirm **verkaufs- und führungsfreundliche** Texte. Technische Begriffe (Cache, Roh-Deals, UTC, Sync) stehen in dieser Doku und im [DATA_GLOSSARY.md](./DATA_GLOSSARY.md), nicht als Primär-KPI in der Oberfläche.

| Anzeige auf der Seite | API / Feld |
|-----------------------|------------|
| Aktive Partner | `kpis.partners_active` |
| … registriert gesamt | `kpis.partners_total` |
| Programm-Admins | `kpis.admins_count` (`is_admin` oder E-Mail in `AFFILIATE_ADMIN_EMAILS`) |
| Empfehlungen im Zeitraum | `kpis.leads_in_period` |
| Leads gesamt | `kpis.leads_total_cache` |
| Deals (Offen / Gewonnen; gleiche Kachel-Logik wie Partner-Dashboard) | `kpis.deals_open_pipeline_total` / `kpis.deals_kpi_total` (Anzeige „a / b“); optional Zeile Verloren: `kpis.deals_closed_lost_total` |
| Ordio-Umsatz aus Deals | `kpis.program_ordio_mrr_total`, aktiv `program_ordio_mrr_active` |
| Kunden-MRR (Brutto der Deals) | `kpis.program_gross_mrr_total` |
| Partner-Provisionen | `kpis.program_mrr_total`, aktiv `program_mrr_active` |

**Hinweis:** `kpis.deals_raw_total` (Anzahl Deal-Datensätze im Cache) wird in der **API** weiterhin geliefert, erscheint auf der **öffentlichen Admin-Seite** aber nicht mehr als eigene Kachel – bei Bedarf für Debugging die JSON-Antwort oder dieses Guide nutzen.

## Troubleshooting: Laden & Diagramme

- **Endloser Ladezustand:** Der Client blendet `#program-analytics-loading` in einem **`finally`-Block** nach jedem Fetch-Versuch aus und nutzt ein **Request-Timeout** (AbortController, ca. 55 s). Hängende PHP-Antworten oder Netzwerkprobleme enden damit in einer **deutschen Fehlermeldung**, nicht im Dauerspinner. **CSS-Falle:** Ein eigenes `display: flex` (o. ä.) auf dem Loader-Element **ohne** Regel für `[hidden]` überschreibt das Browser-Standardverhalten von `hidden` – der Lade-Text bliebe sichtbar, obwohl `hidden` gesetzt ist. In `affiliate-program-analytics.css` daher `.program-analytics-loading[hidden] { display: none !important; }`.
- **Chart.js / `AffiliateCharts` fehlt:** Wenn die Chart-Bibliothek nicht geladen werden kann (CDN blockiert, Adblocker, strenges Netzwerk), zeigt die Seite einen **klaren Hinweis** und blendet den Loader aus – kein „Spinner für immer“.
- **Kein JSON / HTML-Fehlerseite:** Die Antwort wird als Text gelesen und per `JSON.parse` geprüft; bei ungültigem Inhalt erscheint eine **Lesbare-Fehler-Meldung**, der Loader wird beendet.
- **401:** Weiterleitung zum Login; der Loader wird vorher im `finally` ausgeblendet.

## Betrieb

HubSpot-Sync und Diagnose bleiben in der **Verwaltung** (`/partner/admin`): `affiliate-admin-trigger-sync.php`, `affiliate-admin-diagnose-sync.php`.

## Performance

Aggregation läuft als **ein PHP-Durchlauf** über Partner, Leads und Deals im Cache. Für typische Registry-Größen ist das unkritisch. Falls die Laufzeit steigt, kann ein **TTL-Aggregat-File** (v2) ergänzt werden – derzeit nicht implementiert.

## Related

- [CACHE_ARCHITECTURE.md](./CACHE_ARCHITECTURE.md) – Cache-Schema und Konsumenten
- [DASHBOARD_GUIDE.md](./DASHBOARD_GUIDE.md) – Sidebar / API-Überblick
- [ARCHITECTURE.md](./ARCHITECTURE.md) – Dateien und Layer
- [DATA_GLOSSARY.md](./DATA_GLOSSARY.md) – Begriffe Deals / MRR / Leads
