<!-- AUTO-GENERATED by scripts/wi-claude-rules-sync.sh — do not edit. -->
<!-- bundle: 40-release-ship | sources: 5 .mdc files -->
---
name: release
description: WI Freemius, wp.org, commit scope, pre-release smoke, git hygiene
---

<!-- source: .cursor/rules/cloudways-freemius-premium-qa.mdc -->

# Freemius `is_premium` — permanenter Stand

## Standard: `true` — immer, überall

## Versionierung — Staging vs Freemius/wp.org (Tobias 2026-06-12)

**Zwei Ebenen — nicht verwechseln:**

| Kontext | Bump | Beispiel | Zweck |
|---------|------|----------|--------|
| **Staging-Deploy / QA / Screenshot** | Patch in **aktueller Zeile** | `4.12.458` → `4.12.459` | Cache-Bust, Upload-Verify — siehe `wi-agent-workflow.mdc` *Autonomer Version-Bump* |
| **Freemius-Upload + wp.org publizieren** (großer Release) | **Minor-Zeile hoch** | **`4.13`** (nächste) | Kein endloses `4.12.xxx` als „Release" — Marketing/Changelog/Updater-sichtbar |
| **Nur Bugfix-Hotfix** (kein Feature-Release) | Patch in Zeile ok | `4.12.459` oder nach `4.13` → `4.13.1` | Bewusst klein halten — nicht als großen Release verkaufen |

**Freemius / wp.org — Pflicht vor Upload:**

1. **Minor hoch** bei publiziertem Release mit echtem Nutzen (Features, größere Journey-Fixes) — **nächste Zeile: `4.13`**, danach `4.14`, …
2. **Format:** `4.13` reicht (Patch-Teil optional); Header `Version:` und `WEBINARIGNITION_VERSION` **identisch**.
3. **`readme.txt` `Stable tag:`** = dieselbe Version (wp.org-Handbook + `docs/wi-wporg-deploy.md`).
4. **WordPress.org:** Kein Pflicht-SemVer — entscheidend ist `version_compare()` (neu **>** alt). `4.13` > `4.12.458` ✓.
5. **Nicht:** Zehnmal `4.12.xxx` als Freemius-Release hochladen, wenn es faktisch ein **neuer Minor** wäre.

## readme.txt — Datum + Changelog-Wichtigkeit (Tobias 2026-06-14)

**Leerzeile nach Überschriften (hart):** Nach jeder -Zeile immer eine Leerzeile vor dem ersten Satz/Bullet — sonst rendert wp.org die Überschrift nicht als Heading.

**Datumsformat (Changelog-Titel):** `= 4.x.x — 14 June 2026 =` — Tag + englischer Monat + Jahr. International eindeutig, wp.org-lesbar. **Nicht** `14.06.2026` (US/EU-Mehrdeutigkeit). Monat-only (`June 2026`) ok für große Minor-Zeilen.

**Wichtigkeits-Regeln — nur oben in `== CHANGELOG ==`:**

| ✅ Rein | ❌ Raus |
|--------|--------|
| Reg → TY → Live → Replay → Mails → CTAs | Freemius-Modal-Copy, Preview-URLs, Pricing-UI |
| Redirects, Zeiten, Kalender, Formular | Upgrade-Notices, Pro-Badges, Admin-Chrome ohne TN-Effekt |
| Video/Chat/Q&A fürs Publikum | reine Dev-Tooling-/Docs-Fixes |
| Host-Setup, wenn TN-Mails/Seiten betroffen | Admin-only Kosmetik |

**Stil (End-User):** wie die gruppierten Sektionen unter `= Since 4.06.09 =` — kurzer Narrativ-Satz (`**4.12.372 → 4.13** — …`), dann Themen-Überschriften + optional **What's in it for you:** + Bullets `**Feature** — Nutzen in Klartext`. Keine Meta-Zeilen („10× Fixed“, „Participant journey…“), keine Git-States.


**readme.txt = End-User-Text (hart):** Kein „committed“, „wip“, „not yet on wp.org“, „see screenshot“, Patch-Nummern-Spam oder interne Git-States. Jeder Bullet = spürbarer Nutzen für Host oder Teilnehmer in klarem Englisch. Interne Drafts (committed/local) nur im Chat oder `.cursor/wi-current-ideas.md` — **nicht** in `readme.txt`.

**Changelog-Ton (positiv, nicht Bug-Liste):** Statt „Fixed: X no longer breaks“ → „X works reliably now“. Drei Fragen pro Bullet:
1. Was kann der Host/TN jetzt tun oder worauf verlassen? → positiv formulieren
2. Muss er etwas überprüfen/einstellen? → dann explizit: **"Check: …"** oder **"Works automatically."**
3. Ist es wirklich bemerkenswert für einen End-User? → sonst weglassen

**Bekannte Selbstverständlichkeiten weglassen:** Weiterleitungen die „funktionieren“ sind kein Feature. Nur aufnehmen wenn das Thema vorher bekannt problematisch war (Timezone, E-Mail-Versand, URL-Routing) — dann positiv: „Dates and timezones are consistent across registration, confirmation, and emails.“

**Leitfrage (vor jedem Bullet):** „Liest ein Host das und denkt: gut zu wissen, ich vertraue diesem Plugin mehr?“ — wenn nein: weglassen oder umformulieren.

**Released-Block oben (ruhiger):** Mehrere veröffentlichte Patch-Versionen derselben Release-Welle **zusammenfassen**:

```
= 4.12.369 - 4.12.372, 5 June 2026 - 14 June 2026 =

* **Fixed:** …
```

Version **von - bis**, Komma, Datum **von - bis** (ASCII `-`, kein Em-Dash zwischen Version und Datum) (Format `14 June 2026`). Ein Bullet = ein Journey-Effekt.

**Darunter (noch nicht auf wp.org):** zwei getrennte Blöcke — `(committed, in git — not yet on wp.org)` und `(local only — not committed / no working ✓ yet)` — wie im Chat 2026-06-14.


## readme.txt vor Freemius-Upload (hart — Tobias 2026-06-14)

**Vor jedem Freemius-Upload** (pending oder released) — **Agent-Pflicht**, nicht optional:

1. **`Stable tag:`** (readme-Kopf) = exakt dieselbe Version wie `webinarignition.php` Header `Version:` und `WEBINARIGNITION_VERSION`.
2. **`== CHANGELOG ==`** — neuen Block **oben** einfügen (direkt unter `== CHANGELOG ==`), Format wie bestehende Einträge:

```
= 4.13.1 — June 2026 =

* **Fixed:** …
* **Added:** …
```

   - **Minor-Zeile** (z. B. `4.13`): Kurzzeile + Verweis auf akkumulierte Änderungen seit letztem Minor/Patch — wie `= 4.13 — June 2026 =` + „Release line bump …".
   - **Patch/Hotfix** (z. B. `4.13.1`): konkrete Bullet(s) pro Fix/Feature in diesem Upload.
   - Monat/Jahr im Titel (`June 2026`) aktuell halten.
3. **Kein Upload**, wenn `Stable tag` oder Changelog-Top-Version **nicht** zur hochzuladenden Plugin-Version passen — erst readme.txt anpassen, dann `./scripts/freemius-upload-premium-zip.sh`.

**Optional bei wp.org im selben Release:** `== UPGRADE NOTICE ==` mit passendem `= 4.x.x =`-Block mitziehen.


**Agent bei Unklarheit (kurz fragen):** „Release-Zeile **4.13** / **4.14** oder noch **4.12.x** Patch-only?" — besonders vor `push to Freemius` / `push to wp.org`.

**Stand Repo (Chat 2026-06-12):** Code `4.12.458` · publizierte Minor-Zeile als nächstes: **`4.13`**.


`'is_premium' => true,` ist der **dauerhafte Zustand** in `webinarignition.php` (in `fs_dynamic_init`):

- Im **Repo** (committed, gepusht)
- Auf **Cloudways Staging** nach jedem Deploy
- Bei **jedem Commit / Push** auf Bitbucket

Nicht mehr manuell umschalten — `true` bleibt stehen.

## Einzige Ausnahme — explizites Free-Version-Testing

Nur wenn der Task **ausdrücklich** lautet, das **wp.org-Free-Build** zu testen:

1. Kurz auf `false` setzen
2. Testen
3. **Danach sofort wieder auf `true` zurücksetzen und committen**

## Summary

| Szenario | `is_premium` |
|---|---|
| Normaler Betrieb / Premium-QA / jeder Commit | `true` |
| Explizit Free-Build-Test auf wp.org | temporär `false` → danach zurück auf `true` |


## Vor Freemius-Upload / Release (Pflichtkette — Tobias 2026-05)

**Reihenfolge nicht umsortieren.** Marketing erst nach Freemius-Release live (`wi-north-star.mdc`).

| Schritt | Was | Befehl |
|--------|-----|--------|
| 0 | **Syntax (hart)** — ohne Exit 0 kein Upload | `./scripts/wi-syntax-check.sh` |
| 1 | **Lint-Baseline (informativ)** — Zahlen dokumentieren, kein Massen-Fix | `npm run lint:once` → `scripts/reports/wi-lint-baseline.txt` |
| 2 | **Staging + Smoketest** | Deploy (`wi-staging-infra.mdc`) + Registry [`scripts/reports/wi-release-smoke-registry.yaml`](../scripts/reports/wi-release-smoke-registry.yaml) (UPLOAD-ZIP, JOURNEY-8, …) — siehe [`wi-pre-release-smoketest.mdc`](wi-pre-release-smoketest.mdc) |
| 3 | **`readme.txt`** | `Stable tag:` + `== CHANGELOG ==` Top-Block = Upload-Version (siehe § readme.txt vor Freemius-Upload) |
| 4 | **Git sauber** | `git status --short` leer (Release-Scope) |
| 5 | **Freemius-Upload** | unten „push to Freemius“ |
| 6 | **Freemius released** (nur Tobias manuell im Dashboard) | siehe Regel unten — Agent setzt **nie** `release_mode=released` |

## Freemius-Upload Gate — explizite Ansage Pflicht (Tobias 2026-05-25)

**Hart, kein Spielraum:** Der Agent **lädt nichts** zu Freemius hoch — auch nicht als `pending` — solange Tobias **nicht im aktuellen Chat explizit** sagt:

- "push to Freemius"
- "upload to Freemius"
- "Freemius upload"
- **"push to wp.org" / "push to wordpress.org" / "push-to-wordpress.org"** — löst **autonom** die komplette Free-Kette aus (siehe unten); **kein** separates „push to Freemius" nötig
- oder eine direkt gleichwertige Formulierung

**Plan-Texte zählen nicht als Trigger.** Selbst wenn ein Plan/Task `freemius-upload` als Phase nennt, **wartet** der Agent auf das ausdrückliche Chat-Kommando. Der Plan dokumentiert die Phase nur — er löst sie nicht aus.

**Auch nicht-auslösend:** `go`, `autonomous`, `alle Phasen`, „alles ausführen" — diese gelten nur für Code/Deploy/Staging, **nicht** für Freemius — **Ausnahme:** wenn Tobias **wp.org**-Push sagt (siehe Kette unten).

## wp.org-Push = Freemius pending + SVN (autonom, Tobias 2026-06)

**Trigger (gleichwertig):** „push to wp.org", „push to wordpress.org", „push-to-wordpress.org".

**Agent:** **Nicht** nach „push to Freemius" fragen — Kette **ohne Rückfrage** durchlaufen, solange lokale Secrets (`freemius-upload.local.env`, `.env.wporg.local`) vorhanden sind.

| Phase | Was | Default |
|-------|-----|---------|
| 0 | `./scripts/wi-release-preflight.sh` | Syntax grün melden |
| 1 | Freemius Premium upload | **`FREEMIUS_RELEASE_MODE=pending`** (Unreleased) |
| 2 | Free-ZIP fetch | `./scripts/lib/wi-freemius-fetch-free-zip.sh` |
| 3 | SVN sync | `WI_WPORG_DO_COMMIT=1` + `./scripts/wporg-upload-free.sh` (oder w3/w4 aus `docs/wi-wporg-deploy.md`) |
| 4 | Freemius **Released** | **Nie** automatisch — Tobias manuell nach wp.org-Smoke |

**Fertig-Meldung:** Version, Freemius-Status (pending/unreleased), wp.org trunk+tag committed, Smoke-URL Pricing.

**Reihenfolge richtig (hart):**

| Schritt | Trigger | Agent darf |
|-----|-----|-----|
| Code-Fix lokal | Plan/Task/`go` | Edit |
| Staging-Deploy (Cloudways) | implizit nach Code-Fix | Deploy + Screenshot |
| Git-Commit + Push | **`#Axxx working ✓`** vom User | nur Plugin-Allowlist stagen + commit + push |
| Freemius-Upload (pending) | **explizit** „push to Freemius" **oder** wp.org-Push-Kette | Build + Upload als `pending` |
| Freemius released | **niemals automatisch** | Tobias manuell im Dashboard |

## Release-Status — manuell only (Tobias 2026-05-25)

**Hart:** Der Agent setzt `release_mode=released` **niemals** automatisch — auch nicht wenn ein Plan, eine Task-Beschreibung oder ein Auftrag den Wert `released` enthält. Releases werden **ausschliesslich** von Tobias **manuell im Freemius-Dashboard** umgeschaltet (oder explizit per separater Chat-Anweisung wie "set Freemius release_mode released fuer tag XYZ").

| Situation | Agent-Verhalten |
|-----|-----|
| Plan/Task sagt `release_mode=released` | **Ignorieren** + Upload als `pending` (Default) + Hinweis im Chat: "Tag im Dashboard manuell auf released schalten" |
| Tobias sagt "push to Freemius" | Upload als `pending` (Unreleased) |
| Tobias sagt im aktuellen Chat explizit "release_mode released" oder "set released" | Erlaubt — nur dann |
| Auto-Hooks, Pipelines, Scripts | Muessen `pending` als Default haben; kein automatisches `released` |

**Begruendung:** Marketing/Launch-Timing, QA-Stichprobe nach Upload, Bestandskunden-Auto-Updater. Ein versehentliches `released` zieht 10 000+ Sites mit, bevor Tobias den Tag auf Stichproben pruefen kann.

**Ein Kommando für Schritt 0+1:** `./scripts/wi-release-preflight.sh` bzw. `npm run release:preflight`.

Agent: Bei „push to Freemius“ / Release-Task **zuerst** `release:preflight` ausführen und im Chat **Syntax grün + Lint-Report-Pfad** melden, **danach** Upload-Skript.

## Freemius Upload — schneller Standardlauf

Wenn Tobias sagt **„push to Freemius“**:

1. **`readme.txt`:** `Stable tag:` + neuer Changelog-Top-Block = `WEBINARIGNITION_VERSION` (§ readme.txt vor Freemius-Upload).
2. `git status --short` muss leer sein oder bewusst dokumentierte lokale Dateien ausserhalb des Release-Scopes zeigen.
3. Lokale Secrets laden und Upload-Skript ausfuehren:
   ```bash
   set -a; source scripts/env/freemius-upload.local.env; set +a
   FREEMIUS_CURL_TIMEOUT=1800 FREEMIUS_CURL_CONNECT_TIMEOUT=120 ./scripts/freemius-upload-premium-zip.sh
   ```
4. Erfolgreicher Build zeigt ZIP-Groesse (typisch ca. 17 MB). Freemius-API-Antwort **`duplicate_plugin_version`** bedeutet: Version ist bereits hochgeladen; als Upload-OK behandeln.
5. **Release-Status (Default: pending/unreleased):** Uploads gehen immer als `pending` (= Unreleased) raus, ausser der Task-Text enthaelt **explizit** `release_mode=released` oder „set released". Nach jedem Upload zeigt der Agent den Freemius-Status (`release_mode`, `is_released`) aus der API-Antwort — ohne Aufforderung zum manuellen Umschalten.
6. Nicht nochmal Zeit mit `tags/latest` verlieren: diese API kann noch eine aeltere released Version zeigen, solange der neue Tag im Dashboard `Unreleased` ist.

Keine Secrets im Chat ausgeben; nur Plugin-ID, Version, ZIP-Groesse und Freemius-Status melden.

---

<!-- source: .cursor/rules/wi-free-version-release-test.mdc -->

# WI — Free Version Release Test

Before every public release, the Free version must be tested explicitly, not inferred from Premium staging.

## Free vs Premium — Code-Architektur (Tobias 2026-07-07)

**Lokales Repo = Premium-Entwicklung.** Free entsteht **nicht** durch separates Branching, sondern durch **Freemius beim Upload/Deploy**: Premium-ZIP hochladen → Freemius liefert gestrippte **wp.org-Free-ZIP** zum Download.

## Free-Build — kanonischer Ablauf (hart, Tobias 2026-07-07)

**VERBOT: Hand-stripped / lokales Free-ZIP.** Dateien manuell löschen (`@fs_premium_only`, `*__premium_only.php`) und als „Free“ zippen **funktioniert nicht** — PHP-`include`/`is__premium_only()`-Strip fehlt → Fatal (`WebinarignitionLicense not found`, …). **Niemals** `*-free-local.zip`, rsync+delete, oder Agent-Ad-hoc-Strip auf Matrix-Hosts.

**Einziger erlaubter Free-Pfad:**

1. Premium **unreleased** zu Freemius: `FREEMIUS_RELEASE_MODE=pending ./scripts/freemius-upload-premium-zip.sh` (User sagt „upload unreleased to freemius“ oder Pipeline-P1).
2. Free von Freemius holen: `WI_PLUGIN_VERSION=<ver> ./scripts/lib/wi-freemius-fetch-free-zip.sh` → `wi-dev/local/wporg/builds/webinar-ignition-<ver>-free.zip`
3. Dieses ZIP auf Test-Matrix installieren (`WI_UPLOAD_FLAVOR=free`, `WI_UPLOAD_ZIP_PATH` = Fetch-Output).

Alles andere (lokaler Free-Build, hand-stripped, Premium-Ordner umbenennen) → **abbrechen** und auf Schritt 1–3 umleiten. Siehe [`wi-dev/local/reports/FREE-BUILD-CANON.md`](../wi-dev/local/reports/FREE-BUILD-CANON.md).

### Drei Trenn-Mechanismen (Agent muss alle kennen)

| Mechanismus | Wo | Free-Ergebnis |
|-------------|-----|---------------|
| **`@fs_premium_only`** (Plugin-Header) | `webinarignition.php` Zeile `@fs_premium_only` — kommagetrennte **Pfade/Ordner** | Ganze Dateien/Ordner fehlen in Free-ZIP (z. B. `vendor/stripe`, `vendor/webinarignitiongrid`, Webhooks, License-Klassen, Opt-in-UI) |
| **Dateiname `*__premium_only.php`** | z. B. `inc/wi-100ms-streaming__premium_only.php` | Freemius entfernt die Datei aus Free; Loader fällt auf Stub zurück |
| **Runtime `is__premium_only()`** | `if ( webinarignition_fs()->is__premium_only() ) { … Premium … } else { … }` | Inline-Blöcke: Freemius strippt den **`if`-Zweig** in Free; **`else`** bleibt hardcoded (Upgrade-Platzhalter) |

### Zwei Ebenen: Build-Split **und** License-Gate (nicht nur „pro-notice im Free-Build“)

Derselbe `if ( is__premium_only() ) { … } else { pro-notice }`-Block existiert im **Premium-Quellcode**. Freemius entscheidet zur Build-Zeit, welcher Zweig im jeweiligen ZIP landet:

| Build | `is__premium_only()` zur Laufzeit | Verhalten im `else`-Zweig |
|-------|-----------------------------------|---------------------------|
| **Free (wp.org)** | `false` | **`else` hardcoded** — `templates/notices/pro-notice.php` für alle Freemius-gestrippten UI-Bereiche (z. B. tab6 Custom JS/CSS). Kein Premium-Feature-Code dahinter. |
| **Premium (Freemius)** | `true` | **`else` wird nicht ausgeführt** — stattdessen läuft der `if`-Zweig. Dort sitzen oft **zusätzliche** License-Checks (`can_use_premium_code()`, `wi_license_*()`, `Webinar_Ignition_Notices_Manager`). |

**Premium + niedrige/fehlende Lizenz:** Feature darf **nicht** einfach „an“ sein. Entweder Platzhalter (`pro-notice.php`), Stub/No-Op, oder Participant-seitig blockiert. Beispiel 100ms: Essential → kein 100ms-Tab (Platzhalter); Unlimited-Base → 100ms mit **40-Min-Watch-Cap** (Code); volles/unbegrenztes 100ms → Plus / Plus VIP (+ Trial = Plus-Verhalten).

**Referenz-Code:** `inc/wi-100ms-license-gate.php` · `wi_license_includes_100ms_admin_ui()` · `wi_license_has_unlimited_100ms()` · `templates/notices/pro-notice.php` · tab6: `UI/app/tab6.php` · tab2 100ms: `UI/app/tab2.php` · Webhooks: `admin/views/tabs/webhooks.php`

### Produkt-Tiers (Marketing ↔ Freemius-Slug)

**Public Pricing:** [https://webinarignition.com/#pricing](https://webinarignition.com/#pricing) (Popup ggf. schließen vor Scrape/QA)

| Marketing (Pricing-Seite) | Preis (monthly) | Freemius-Slug | Premium-Code | Plus-only Highlights (Pricing) |
|---------------------------|-----------------|---------------|--------------|--------------------------------|
| **Unlimited** | $49 | `ultimate_powerup` | ja | ∞ registrations/time/attendees; Webhooks; Paid webinars; WP-only in-webinar CTAs |
| **Plus** (= Unlimited Plus) | $99 | `ultimate_powerup_tier3a` | ja | Grid views; **100ms** Meet & Stream; **External-page CTAs**; **WooCommerce checkout CTAs**; Done-for-you setup |
| **Plus VIP** | $199 | `ultimate_powerup_tier4a` | ja | VIP support/meetings; Groundhogg Pro 1-site |
| **Essential** (legacy/entry) | — | `ultimate_powerup_tier1a` | ja | Kein 100ms-Admin-UI; 5-Min-Live-Watch-Cap |
| **Free (wp.org)** | — | *(leer)* | nein | Kern-Journey ohne Premium-Module |

Vollständige Slug-Matrix + Caps: [`wi-license-plan-matrix.mdc`](wi-license-plan-matrix.mdc) · [`docs/wi-license-plan-matrix.md`](../docs/wi-license-plan-matrix.md)

### Beispiel 100ms (kanonisch, beide Ebenen)

**Ebene 1 — Build (Free vs Premium):**

- **Premium:** `inc/wi-100ms-streaming__premium_only.php` + HMS-Assets; Integration-Accordion tab2 nur innerhalb `is__premium_only()`.
- **Free:** Datei **weg** → `inc/class-webinarignition.php` lädt `inc/wi-100ms-streaming-stub.php` (No-Ops, `wi_webinar_uses_100ms_video_integration()` → `false`). **Kein** 100ms-Player hinter Paywall im Free-Tree.

**Ebene 2 — License (nur Premium-Build):**

- `inc/wi-100ms-license-gate.php` bleibt im Free-ZIP (Plan-Matrix), liefert auf Free/wp.org aber `false` / leer.
- tab2 100ms-Accordion: `wi_license_includes_100ms_admin_ui()` — **deny** Essential + `onetimepro`; **allow** Unlimited (`ultimate_powerup`, 40m cap), Plus (`tier3a`), Plus VIP (`tier4a`).
- Volles/unbegrenztes 100ms: `wi_license_has_unlimited_100ms()` — nur `tier3a` + `tier4a` (+ Trial).
- Live-Raum bei konfiguriertem 100ms ohne passende Lizenz: Host sieht `pro-notice`; TN sieht neutrale Block-Meldung (`wi_render_100ms_license_blocked_*`).

### Beispiel tab6 Custom JS/CSS (nur Ebene 1)

`UI/app/tab6.php`: `if ( is__premium_only() ) { Textareas } else { pro-notice }`.

- **Free-Build:** immer Platzhalter (Freemius strippt `if`-Zweig).
- **Premium-Build:** Textareas für **jede** aktive Premium-Lizenz (kein separater Plus-Gate in tab6).

### Harte Produktregel (wp.org / GPL)

- **Kein Premium-Code in Free mit Paywall:** Entfernte Features (100ms, Webhooks, Grid, …) müssen **weg oder No-Op** sein, nicht „bezahle um denselben Code zu entsperren“.
- **Free ohne Lizenz nutzbar:** Kern-Journey (Reg, TY, Embed/YouTube-Live, Mails, Evergreen) läuft **ohne** Freemius-Opt-in. Freemius-Opt-in = Analytics/Updates; ohne Opt-in bleibt SDK minimal.

### Dev-Default

- Repo + Staging: `'is_premium' => true` in `fs_dynamic_init` — siehe [`cloudways-freemius-premium-qa.mdc`](cloudways-freemius-premium-qa.mdc).
- Free testen: Freemius **pending** upload → **Free-ZIP fetchen** → auf Free-Testsite installieren (nicht Premium-Staging als Proxy).

## Smoke checks (Pflicht vor Release / Free-Matrix)

Zwei **getrennte** Prüfstränge — beide müssen grün sein:

### A) Free-ZIP — Premium-Leak (Build-Split)

Free-ZIP von Freemius (nach pending upload + fetch). **🔴/🟠** bei Verletzung:

1. Keine `*__premium_only.php`, keine `@fs_premium_only`-Pfade im Tree (grep/list).
2. Kein aufrufbarer 100ms/HMS-SDK-Pfad, kein `vendor/stripe`, kein Grid/Webhook-Premium-Stack.
3. Kein Participant-Flow mit „upgrade für entferntes Feature“ statt fehlendem Feature.
4. Runtime: `webinarignition_fs()->is__premium_only()` === **false** (Slug `webinar-ignition`).

**Grün** = saubere Strip-Trennung, keine Premium-Funktion im Free-Tree.

### B) Premium-Build + niedrige/fehlende Lizenz — Feature gated (License)

Premium-Slug (`webinarignition-premium`) auf Testsite; Lizenz **bewusst** niedrig halten oder deaktivieren. Spot-Checks (mindestens):

| Check | Lizenz | Erwartung |
|-------|--------|-----------|
| tab2 → 100ms-Integration | Essential (`tier1a`) oder keine Lizenz | **pro-notice**, kein 100ms-Settings-Formular |
| tab2 → 100ms live (falls konfiguriert) | Essential | Kein funktionierender 100ms-Player für TN; Host ggf. pro-notice |
| tab6 Custom JS/CSS | Free-Build (Separat-Check A) | pro-notice |
| tab6 Custom JS/CSS | Premium + gültige Lizenz | Textareas sichtbar |
| External / Woo CTAs, Grid | Unlimited-Base (`ultimate_powerup`) | Pricing: Plus-Features — Code prüfen: `wi_license_has_multi_cta()` / `can_use_premium_code()`; **🔴** wenn Plus-only UI ohne Gate „einfach an“ |
| 100ms unlimited usage | Unlimited-Base | 40m cap ok; **🔴** wenn unbegrenzt wie Plus |
| 100ms unlimited usage | Plus / Plus VIP / Trial | Volles 100ms-UI + unlimited usage |

**Grün** = Premium-Features sind bei zu niedriger Lizenz **nicht** still aktiv; Platzhalter, Cap oder Stub wie dokumentiert.

Cross-Ref: [`wi-license-plan-matrix.mdc`](wi-license-plan-matrix.mdc), [`wi-commit-scope.mdc`](wi-commit-scope.mdc), [`wi-multiserver-mail-smoke.mdc`](wi-multiserver-mail-smoke.mdc)

---

Default:

- Premium development and staging stay `is_premium => true`.
- Free testing happens only in the release lane, before publishing.
- Free needs a Freemius upload/fetch path and must default to unpublished/unreleased unless Tobias explicitly requests release.
- Upload to Freemius is allowed only when Tobias explicitly says `push to Freemius`, `upload to Freemius`, or `push to wp.org`.
- For normal code tasks, do not switch the repo permanently to Free mode.

Best release gate:

1. Premium staging smoke green.
2. Build/upload Premium to Freemius as unpublished/pending when explicitly requested.
3. Fetch/build Free package from Freemius.
4. Install Free on a clean/free test site — **Smoke A (leak grep)**.
5. Premium test site with controlled license tiers — **Smoke B (gated features)**.
6. Run journey matrix: activation, dashboard, create webinar, registration, TY, live/replay where available.
7. Only after Free **A** + Premium **B** + journey smoke are green: Tobias decides public release.

---

<!-- source: .cursor/rules/wi-commit-scope.mdc -->

# WI — Commit / Deploy Scope (Plugin only)

## Plugin ship-set (allow)

Only these paths may be `git add` / committed / included in Freemius ZIP / uploaded to customer `wp-content/plugins/webinarignition-premium/`:

- `webinarignition.php`
- `inc/`
- `UI/`
- `admin/`
- `assets/`
- `templates/`
- `css/`
- `languages/`
- `images/`
- `font/`

## Never commit or upload (local dev only)

- `.cursor/` (rules, plans, advisors, hooks)
- `scripts/` (deploy, smoke, lint, Freemius upload runner, env examples) — run locally, do not deploy
- `scripts/debug/`
- **`wi-dev/`** — **niemals** committen (hart). Lokaler Ordner nur; steht in `.gitignore`. Freemius/Deploy-Runner liegen unter **`scripts/`** (im Repo, aber nicht ins Plugin-ZIP / nicht auf Kunden-Staging).
- `docs/`, `.cursor-wi-screenshots/`, `ss/`
- `*ACCESS*.md`, `FELIX-ACCESS*`, `STAGING-ACCESS*`
- `phpcs.xml` / `phpcs.xml.dist` unless explicit release-hygiene task

Exception: `.gitignore` updates that block re-adding dev paths may be committed (meta, not in plugin ZIP).

## Channels (same allowlist)

| Channel | Rule |
|---------|------|
| Bitbucket `main` | `git add` only allowlist after `working` / `working ✓` |
| Freemius Premium | ZIP built from allowlist; upload via local `scripts/freemius-upload-premium-zip.sh` |
| WordPress.org | Future SVN/commits — same plugin files only |
| Customer staging (Felix, Cloudways) | Incremental lftp/SCP of changed allowlist files + `webinarignition.php` on version bump; never whole repo |

## Customer-site debug

- **WP_DEBUG / logs:** staging `wp-config` or hosting panel — not committed, not plugin upload.
- **WI debug snippets:** do not upload `scripts/debug/` or ad-hoc snippets unless user explicitly asks; never commit them.


## Intern — Standard kein Commit

**Alles Nicht-Plugin** (`.cursor/`, `.cursorrules`, `wi-dev/`, `docs/`, Pläne, Rules, Screenshots, Secrets) bleibt **lokal**. Kein `git add` / Push dafür — **auch nicht** nach `working ✓` — außer Tobias sagt **explizit** z. B. „commit rules“ / „commit docs“.

Bereits getrackte `.cursor/rules/*` in Git: nicht still erweitern; neue Agent-Artefakte nur unter ignorierten Pfaden.

---

## Interner Datei-Check (Pflicht vor jeder Datei & vor `git add`)

| Schritt | Agent |
|--------|--------|
| 1 | **Neue Datei?** Nur anlegen wenn Pfad im **Plugin ship-set** (oben) — sonst unter **`wi-dev/local/`**, **`.cursor/plans/`** oder **`.cursor/`** (gitignored). |
| 2 | **Versehentlich** außerhalb (Repo-Root, `scripts/` ohne Team-Auftrag): **verschieben** nach `wi-dev/local/` + Zeile in **`.gitignore`** (musterhaft `/<name>.md` oder Ordner). |
| 3 | **`git status`:** Jede Zeile `??` oder `M` außerhalb Allowlist → **nicht** stagen. Nur Plugin-Pfade stagen. |
| 4 | **`.gitignore` anpassen** erlaubt als einzige Meta-Datei am Root, wenn sie neue lokale Pfade blockiert (kein Plugin-Inhalt). |

**Lokale Ordner (kanonisch):**

| Ordner | Zweck |
|--------|--------|
| `wi-dev/` | Skripte-Entwürfe, Loadtests, lokale Tools (`wi-dev/local/` = Ad-hoc) |
| `.cursor/` | Rules, Plans, Advisors, WIP-Backups |
| `.cursor-wi-screenshots/`, `ss/` | QA-PNG |
| `docs/` | Lokale Notizen (nicht Kunden-Doku im Plugin) |

**Deploy/Freemius:** unverändert nur Plugin ship-set — interne Dateien nie hochladen.

## Agent workflow

1. Fix locally → deploy allowlist only → test URL.
2. After user `working ✓`: `git status` → stage **only** allowlist paths → commit → `git pull` → `push origin main`.

---

<!-- source: .cursor/rules/wi-pre-release-smoketest.mdc -->

# WI — Pre-Release Smoketest (Reg → TY → Webinar-Raum)

Vor jedem Release auf Staging prüfen. Jeder Fall im Inkognito (neuer Lead) + mit WP-Admin offen für Master-Switch.

## Setup je Webinar
- Kampagne „Talha and Tobias“ (ID 38 auf Staging) mit 3 Reg-Page-Varianten.
- Default-Reg-Page konsistent: `Dashboard → Registration Page → Default`.
- TY-Page verlinkt: `custom_thankyou_page` gesetzt.
- Master Switch zunächst auf gewünschten Zustand (Countdown / Live).

## Pflicht-Fälle

1. Reg → TY bei Default-Reg = Haupt-Slug
   - `…/talha-and-tobias/` Formular absenden → Redirect zu TY (`custom_thankyou_page` oder `…?confirmed&lid=…`).
2. Reg → TY bei SC Custom Page
   - `…/wi-38-reg-custom-2/` (Shortcode `[wi_webinar_block]` mit `page=registration`) → Redirect zu TY-Page.
3. Reg → TY bei GB-Reg-Page (WI-generiert)
   - `…/talha-and-tobias-register-now-3/` → Redirect zu TY-Page.
4. TY mit gültiger lid zeigt Thank-you
   - `…/talha-and-tobias-thank-you/?lid=<valid>` + Countdown sichtbar.
5. TY ohne lid / mit invalid lid → Registrierung
   - `…/talha-and-tobias-thank-you/` oder mit unbekannter `lid` → Redirect zur Default-Reg.
6. TY → Webinar-Raum, Master Switch = Live, VOR Startzeit
   - Master Switch „Live“ setzen, TY mit gültiger lid laden → sofortiger Redirect in den Webinar-Raum (`…/<webinar>/?live&webinar&lid=…&watch_type=live`).
7. Direkter Live-Link funktioniert
   - `…/talha-and-tobias/?live&webinar&lid=<valid>` → Webinar-Raum.
8. Direkter Live-Link mit invalid lid → Registrierung
   - Invalid `lid` → Redirect zur Reg.

## Mail-Smoke (fest, Multi-Server)

Pflicht **zusätzlich** zu Reg → TY → Raum. Details: [wi-multiserver-mail-smoke.mdc](wi-multiserver-mail-smoke.mdc).

- **MAIL-V:** Remote-Plugin-Version = lokales `webinarignition.php`.
- **MAIL-C:** Confirmation/Signup direkt nach Reg (IMAP oder Mail-Log).
- **MAIL-R1:** Reminder Slot 1 (2 Min vor Live, MAIL-6MIN) — **nur** via Cron/Action Scheduler, nicht Sofort nach Reg.
- **MAIL-R2:** Reminder Slot 2 (1 Min vor Live, MAIL-6MIN) — **nur** via Cron/AS.

Runner: `./scripts/wi-multiserver-mail-smoke.sh version` bzw. `full` (Kompression auf Smoke-Hosts). Alle fünf Matrix-Hosts: [wi-ai-webinar-smoke-scope.mdc](wi-ai-webinar-smoke-scope.mdc).

## Journey Smoke (Default ab 2026-07-05)

Volle Matrix **Reg → TY → Live (Modern Room) → Replay → Ended** + Admin-Preview + Mail-Join auf allen 5 Server-Typen: [wi-modern-room-journey-smoke-default.mdc](wi-modern-room-journey-smoke-default.mdc). Orchestrator: `./scripts/wi-modern-room-journey-smoke-default.sh`.

## Abnahmekriterium pro Fall
- HTTP 302 / 200 je nach Erwartung; Zielseite im Browser-Titel/URL passt.
- Console sauber (keine JS-Errors blockieren den Flow).
- Screenshot in `ss/` ablegen (`ss/ss-<case>-<viewport>.png`).

## Vor dem Smoketest (Release-Preflight)

1. `./scripts/wi-syntax-check.sh` — **Exit 0** (Parse-Gate, siehe `scripts/wi-syntax-check.sh`).
2. `npm run lint:once` — Baseline-Report (informativ, kein Merge-Gate).
3. Optional gebündelt: `npm run release:preflight`.
4. **Release Smoke Registry lesen:** [`scripts/reports/wi-release-smoke-registry.yaml`](../scripts/reports/wi-release-smoke-registry.yaml) (lokal, gitignored). Vorlage: [`scripts/env/wi-release-smoke-registry.example.yaml`](../scripts/env/wi-release-smoke-registry.example.yaml). Je Sub-Smoke: `last_checked` + `last_version` — nur erneut laufen wenn Version älter als Release-Kandidat oder `trigger_rerun_when` im Registry-Eintrag zutrifft.

Erst danach Deploy + die Smoketest-Fälle unten. Freemius-Upload: **`cloudways-freemius-premium-qa.mdc`**.

## Release Smoke Registry (Gesamt-Check vor Release)

Pflicht **vor Freemius / wp.org**: alle Sub-Smokes mit `when_before_release: Pflicht` grün oder bewusst dokumentiert `partial`/`skip` mit Grund.

| ID | Was | Wann erneut? | Runner |
|----|-----|--------------|--------|
| **UPLOAD-ZIP** | wp-admin ZIP install/replace auf 5 Hosts (Compat) | Freemius/vendor, ZIP excludes, Bootstrap-Guard | `node wi-dev/local/wi-upload-smoke-matrix.mjs` |
| **ZIP-BUILD** | ZIP enthält Freemius + keine Windows-ungültigen Pfade | Jeder Release-ZIP-Build | `./scripts/lib/wi-zip-integrity-check.sh` |
| **JOURNEY-8** | 8 Fälle Reg→TY→Raum (Cloudways) | Redirect/Routing/Master-Switch | manuell, diese Rule |
| **JOURNEY-MATRIX** | Modern Room 5-Host-Kette | Journey/Mail-Join/Preview | `./scripts/wi-modern-room-journey-smoke-default.sh` |
| **MAIL-MULTI** | Confirmation + Reminder Cron (**MAIL-6MIN**) | Mail/Cron-Änderungen | `./scripts/wi-multiserver-mail-smoke.sh` |
| **AI-MATRIX** | AI Live/Evergreen/Fixed-date pro Host | Create/AI-Import | siehe `wi-ai-webinar-smoke-scope.mdc` |
| **REDIRECT-CURL** | Redirect curl J1–J4 (5 Hosts) | Routing/URL-Resolver | `./scripts/wi-multiserver-redirect-smoke.sh` |
| **P22-SUBMIT** | Reg→Submit→TY GB Live+EG | Registration/Form | `node wi-dev/local/wi-customer-flow-suite.mjs --submit` |
| **P24-TY-LIVE** | TY Countdown + Live CTAs | TY/Live-CTA | `node wi-dev/local/wi-p24-full-round-smoke.mjs` |
| **CALENDAR-ICS** | ICS/Google/TY Calendar | Calendar/ICS | `node wi-dev/local/wi-customer-flow-suite.mjs` |
| **MAIL-LOG** | Admin-Logs day/hour-before | Mail/Cron | tab8 / SSH scrape |
| **MULTILANG-7x5** | 7 Locales × 5 Hosts | i18n/Loco-UI | `node wi-dev/local/wi-multilang-smoke-orchestrate.mjs` |
| **SYNTAX-PREFLIGHT** | PHP parse gate | Jeder Release | `./scripts/wi-syntax-check.sh` |

**Quick-Scan:** [`plans/SMOKE-DASHBOARD.md`](../../plans/SMOKE-DASHBOARD.md) — Refresh: `node scripts/wi-smoke-dashboard-refresh.mjs`

## Upload-Gate — Phase 0 (Multi-Sub-Smoke-Session)

1. **UPLOAD-ZIP zuerst** — volle Plugin-ZIP auf allen 5 Matrix-Hosts parallel (`node wi-dev/local/wi-upload-smoke-matrix.mjs`)
2. **1× pro Gesamt-Run** — Sub-Smokes danach ohne erneuten Upload, solange UPLOAD-ZIP grün blieb
3. **Bei Fehler in einem Sub-Smoke** — scoped Full-ZIP nur betroffener Host; **globaler Fehler** → Full-ZIP alle 5
4. Registry + Dashboard: `node scripts/wi-smoke-dashboard-refresh.mjs`

## Host-Isolation (5-Host-Matrix)

- **Ein Host fail** → sofort melden (`⚠️ Host <id> FAIL — … — andere laufen weiter`), **andere Hosts im Hintergrund weiter**
- **3/5 grün** = OK Zwischenstand — Dashboard refresh, dann nur Problem-Hosts fixen + Rerun
- Registry: `hosts[].status` pro Host; Gesamt-`status: partial` wenn gemischt (nicht alles fail wegen einem Host)
- Mittel = wp-admin-ZIP, öffentliche TN-URLs, Admin tab8 — wie Admin/Teilnehmer

**MAIL-MULTI** nutzt Profil **MAIL-6MIN** (`timing_profile: mail-6min` in Registry) — siehe [`wi-smoke-test-webinar-timing.mdc`](wi-smoke-test-webinar-timing.mdc).

**Agent-Pflicht nach jedem Sub-Smoke:** Registry aktualisieren (`last_run_at`, `last_checked`, `last_version`, `status`; bei UPLOAD-ZIP auch pro `hosts[]`) + Dashboard-Refresh.

**Entscheidung „braucht es einen Rerun?“**

- `last_version` < `WEBINARIGNITION_VERSION` des Release-Kandidaten → **ja**
- Diff trifft `trigger_rerun_when` des Eintrags → **ja**
- Sonst → **nein** (im Registry-Chat kurz „skip, still 4.14.76“ notieren)


## Ablauf
1. Deploy gemäß `wi-staging-infra.mdc` / `cloudways-deploy.mdc`.
2. `wp cache flush` + ggf. Varnish purge.
3. Alle 8 Fälle im Inkognito durchklicken (neue Leads je Lauf).
4. Screenshots je Fall speichern.
5. Erst nach „working ✓“ vom Human committen (`wi-git-clean-between-tasks.mdc`).

## Kurz-Checkliste
- [ ] Default-Reg → TY
- [ ] SC Custom → TY
- [ ] GB Reg → TY
- [ ] TY mit gültiger lid zeigt TY
- [ ] TY ohne/invalid lid → Reg
- [ ] TY → Raum bei Master „Live“ vor Startzeit
- [ ] Direkter Live-Link → Raum
- [ ] Direkter Live-Link invalid lid → Reg

---

<!-- source: .cursor/rules/wi-git-clean-between-tasks.mdc -->

# Git / SCM — sauber zwischen Tasks

- **Maßstab:** Im Projektroot `git status`. Keine `modified`/`staged` Einträge = getrackte Dateien sind sauber.
- **Cursor-Chat „1 File“ / Review-Leiste:** Oft der **Vorschlags-Diff dieser Konversation**, nicht zwingend der aktuelle Git-Stand. Nach erfolgreichem `git commit` ggf. Review schließen oder neuen Chat — bei Unsicherheit immer `git status` prüfen.
- **Untracked** (z. B. neue `.cursor/rules/*.mdc`): Erscheinen in der SCM-Sidebar, bis `git add` + `commit` — oder bewusst in `.gitignore`, wenn sie nie ins Repo sollen (Team-Regeln in `.cursor/rules/` gehören meist ins Repo).
- **Nach Agent-Tasks:** Wo möglich mit **clean working tree** abschließen (außer absichtlich lokale/ignorierte Dateien wie `STAGING-ACCESS.local.md`).

## Clean commits (hart — vor jedem `git add`)

**Ziel:** Bitbucket + Freemius = **nur Plugin ship-set** ([wi-commit-scope.mdc](wi-commit-scope.mdc)). Lokale Tooling-Diffs dürfen im Working Tree bleiben — **nicht** stagen.

### Nie stagen (Default)

- `scripts/` — Deploy/Smoke/Loco/Freemius-Runner (lokal nutzen; Freemius-ZIP excluded anyway)
- `docs/`, `wi-dev/`, `phpcs.xml*`, `.cursor/` (Rules nur bei explizitem „commit rules“)
- `*ACCESS*.md`, `*.local.env`, Screenshots, Root-Scratch (`_wi-*.mjs`)

### Immer stagen (Release)

- `webinarignition.php`, `inc/`, `UI/`, `admin/`, `assets/`, `templates/`, `css/`, `languages/`, `images/`, `font/`

### Agent-Pflicht vor Commit

1. `git status --short` — jede Zeile prüfen; nur Allowlist-Pfade `git add`.
2. `git diff --cached --name-only` — wenn ein Pfad außerhalb Allowlist: **unstage** (`git restore --staged <path>`).
3. Non-Plugin-Scratch **neu** nur unter `wi-dev/local/` anlegen ([wi-dev/local/README.md](../wi-dev/local/README.md)).
4. Nach Commit: getrackte Allowlist-Dateien sauber; `scripts/`/`docs/`/`phpcs` modified lokal ok — **nicht** `git restore`, außer User verlangt es.

### User-Signal

- **`working ✓`** / **`#Axxx working`** → Task in Plan-Status **abgeschlossen** — **kein** Auto-Commit (Ship: Freemius / explizit `sync bitbucket` + `wi-ship-preflight.sh`).
- Breiterer Commit (`scripts/`, `docs/`, Rules) → nur bei explizitem Auftrag („commit scripts“, „commit rules“).

