diff --git a/docs/plans/2026-02-28-update-channels.md b/docs/plans/2026-02-28-update-channels.md
new file mode 100644
index 0000000..3ea3673
--- /dev/null
+++ b/docs/plans/2026-02-28-update-channels.md
@@ -0,0 +1,766 @@
+# Releases Module Implementation Plan
+
+> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
+
+**Goal:** Wdrożyć dwukanałowy system aktualizacji (beta/stable) z zarządzaniem licencjami w panelu admina.
+
+**Architecture:** Dwie nowe tabele MySQL (`pp_update_licenses`, `pp_update_versions`) dostępne tylko na serwerze dewelopera. `updates/versions.php` czyta z DB przez Medoo. Nowy moduł `Releases` w panelu admina zarządza wersjami i licencjami. Całość wykluczona z paczek klientów przez `.updateignore`.
+
+**Tech Stack:** PHP 8.x, Medoo 1.x (`$mdb` global), jQuery UI (dialog), Bootstrap 3 (klasy CSS), `\Tpl` (admin templates z `admin/templates/`), `\S::get()` (POST→GET params).
+
+---
+
+### Task 1: Dodaj wykluczenia do `.updateignore`
+
+**Files:**
+- Modify: `.updateignore`
+
+**Step 1: Dopisz nowe wykluczenia**
+
+Otwórz `.updateignore` i dopisz na końcu:
+
+```
+# Moduł zarządzania releaseami (tylko serwer dewelopera)
+autoload/admin/controls/class.Releases.php
+autoload/admin/factory/class.Releases.php
+autoload/admin/view/class.Releases.php
+admin/templates/releases/
+
+# Menu dewelopera
+templates/additional-menu.php
+```
+
+**Step 2: Zweryfikuj manualnie**
+
+Uruchom build w trybie dry-run i sprawdź, że powyższe pliki NIE pojawiają się na liście:
+```powershell
+./build-update.ps1 -ToTag v9.999 -DryRun
+```
+
+**Step 3: Commit**
+
+```bash
+git add .updateignore
+git commit -m "chore: wyklucz modul Releases i menu dewelopera z paczek klientow"
+```
+
+---
+
+### Task 2: Utwórz tabele DB (jednorazowo na serwerze)
+
+**Files:**
+- Create: `_db_releases_setup.sql` (uruchom raz w phpMyAdmin, nie commituj)
+
+**Step 1: Utwórz plik SQL**
+
+```sql
+CREATE TABLE pp_update_licenses (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ `key` VARCHAR(64) NOT NULL UNIQUE,
+ domain VARCHAR(255) NOT NULL,
+ valid_to_date DATE NULL,
+ valid_to_version VARCHAR(10) NULL,
+ beta TINYINT(1) NOT NULL DEFAULT 0,
+ note TEXT NULL,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE pp_update_versions (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ version VARCHAR(10) NOT NULL UNIQUE,
+ channel ENUM('beta','stable') NOT NULL DEFAULT 'beta',
+ created_at DATETIME NULL,
+ promoted_at DATETIME NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+```
+
+**Step 2: Uruchom w phpMyAdmin na serwerze `cmspro.project-dc.pl`**
+
+Wklej SQL do phpMyAdmin → Execute. Obie tabele muszą być widoczne.
+
+**Step 3: Nie commituj** — ten plik jest tylko pomocniczy, możesz go usunąć po wykonaniu.
+
+---
+
+### Task 3: Utwórz `admin\factory\Releases`
+
+**Files:**
+- Create: `autoload/admin/factory/class.Releases.php`
+
+**Step 1: Napisz klasę**
+
+```php
+select('pp_update_versions', '*', ['ORDER' => ['version' => 'DESC']]);
+ if (!$rows) return [];
+ foreach ($rows as &$row)
+ $row['zip_exists'] = file_exists('../updates/' . self::zip_dir($row['version']) . '/ver_' . $row['version'] . '.zip');
+ return $rows;
+ }
+
+ public static function promote(string $version): void
+ {
+ global $mdb;
+ $mdb->update('pp_update_versions',
+ ['channel' => 'stable', 'promoted_at' => date('Y-m-d H:i:s')],
+ ['version' => $version]
+ );
+ }
+
+ public static function demote(string $version): void
+ {
+ global $mdb;
+ $mdb->update('pp_update_versions',
+ ['channel' => 'beta', 'promoted_at' => null],
+ ['version' => $version]
+ );
+ }
+
+ public static function get_licenses(): array
+ {
+ global $mdb;
+ return $mdb->select('pp_update_licenses', '*', ['ORDER' => ['domain' => 'ASC']]) ?: [];
+ }
+
+ public static function get_license(int $id): array
+ {
+ global $mdb;
+ return $mdb->get('pp_update_licenses', '*', ['id' => $id]) ?: [];
+ }
+
+ public static function save_license(array $data): void
+ {
+ global $mdb;
+ $row = [
+ 'key' => trim($data['key'] ?? ''),
+ 'domain' => trim($data['domain'] ?? ''),
+ 'valid_to_date' => $data['valid_to_date'] ?: null,
+ 'valid_to_version' => $data['valid_to_version'] ?: null,
+ 'beta' => (int)(bool)($data['beta'] ?? 0),
+ 'note' => trim($data['note'] ?? ''),
+ ];
+ if (!empty($data['id']))
+ $mdb->update('pp_update_licenses', $row, ['id' => (int)$data['id']]);
+ else
+ $mdb->insert('pp_update_licenses', $row);
+ }
+
+ public static function delete_license(int $id): void
+ {
+ global $mdb;
+ $mdb->delete('pp_update_licenses', ['id' => $id]);
+ }
+
+ public static function toggle_beta(int $id): void
+ {
+ global $mdb;
+ $license = $mdb->get('pp_update_licenses', ['id', 'beta'], ['id' => $id]);
+ if ($license)
+ $mdb->update('pp_update_licenses', ['beta' => $license['beta'] ? 0 : 1], ['id' => $id]);
+ }
+
+ private static function zip_dir(string $version): string
+ {
+ return substr($version, 0, strlen($version) - (strlen($version) == 5 ? 2 : 1)) . '0';
+ }
+}
+```
+
+**Step 2: Weryfikacja składni**
+
+```bash
+php -l autoload/admin/factory/class.Releases.php
+```
+Oczekiwane: `No syntax errors detected`
+
+---
+
+### Task 4: Utwórz `admin\controls\Releases`
+
+**Files:**
+- Create: `autoload/admin/controls/class.Releases.php`
+
+**Step 1: Napisz klasę**
+
+```php
+versions = \admin\factory\Releases::get_versions();
+ $tpl->licenses = \admin\factory\Releases::get_licenses();
+ return $tpl->render('releases/main-view');
+ }
+}
+```
+
+**Step 2: Weryfikacja składni**
+
+```bash
+php -l autoload/admin/view/class.Releases.php
+```
+
+---
+
+### Task 6: Utwórz szablon `admin/templates/releases/main-view.php`
+
+**Files:**
+- Create: `admin/templates/releases/main-view.php`
+
+**Step 1: Utwórz katalog i plik**
+
+```php
+
+
+
+
+
+
+
+
+
+
+
+
Wersje
+
+
+
+
+ Wersja Kanał Dodana
+ Promocja do stable ZIP Akcje
+
+
+
+ foreach ($this->versions as $v): ?>
+
+ = htmlspecialchars($v['version']) ?>
+
+ if ($v['channel'] == 'stable'): ?>
+ stable
+ else: ?>
+ beta
+ endif; ?>
+
+ = $v['created_at'] ? substr($v['created_at'], 0, 10) : '-' ?>
+ = $v['promoted_at'] ? substr($v['promoted_at'], 0, 10) : '-' ?>
+ = $v['zip_exists'] ? '✓ ' : '✗ ' ?>
+
+ if ($v['channel'] == 'beta'): ?>
+
+ Promuj →stable
+
+ else: ?>
+
+ Cofnij →beta
+
+ endif; ?>
+
+
+ endforeach; ?>
+ if (!$this->versions): ?>
+ Brak wersji w bazie. Wersje będą rejestrowane automatycznie przy pierwszym odpytaniu versions.php.
+ endif; ?>
+
+
+
+
+
+
+
+
+
+
+
Licencje
+ + Dodaj licencję
+
+
+
+
+
+
+
+
+
+ Domena Klucz Do daty
+ Do wersji Beta Notatka Akcje
+
+
+
+ foreach ($this->licenses as $lic): ?>
+
+ = htmlspecialchars($lic['domain']) ?>
+
+ = $lic['key'] === '' ? '(domyślny) ' : htmlspecialchars(substr($lic['key'], 0, 8)) . '…' ?>
+
+ = $lic['valid_to_date'] ?: '∞ ' ?>
+ = $lic['valid_to_version'] ?: '∞ ' ?>
+
+
+ = $lic['beta'] ? 'beta' : 'stable' ?>
+
+
+ = htmlspecialchars($lic['note'] ?? '') ?>
+
+ Edytuj
+ Usuń
+
+
+ endforeach; ?>
+ if (!$this->licenses): ?>
+ Brak licencji. Dodaj pierwszą lub uruchom skrypt migracji.
+ endif; ?>
+
+
+
+
+
+
+
+
+
+
+
+```
+
+**Step 2: Zweryfikuj, że szablon jest poprawny**
+
+Otwórz w przeglądarce: `https://cmspro.project-dc.pl/admin/releases/main_view/`
+
+Oczekiwane: strona ładuje się bez błędów PHP, widoczne dwa taby, tabela wersji pusta lub z danymi.
+
+---
+
+### Task 7: Dodaj pozycję menu dla dewelopera
+
+**Files:**
+- Create: `templates/additional-menu.php` (ten plik nie trafia do klientów — dodany do `.updateignore` w Task 1)
+
+**Step 1: Utwórz plik**
+
+```php
+
+Developer
+
+```
+
+**Step 2: Sprawdź w przeglądarce**
+
+Po odświeżeniu panelu admina powinna pojawić się sekcja "Developer" z linkiem "Releases & Licencje" w menu bocznym.
+
+---
+
+### Task 8: Migracja danych licencji do DB
+
+Licencje z hardkodowanej tablicy `$license` w `updates/versions.php` muszą trafić do `pp_update_licenses` **przed** przejściem na nową wersję `versions.php`.
+
+**Files:**
+- Create: `_migrate_licenses.php` (tymczasowy, uruchom raz przez przeglądarkę, potem usuń)
+
+**Step 1: Utwórz skrypt migracji**
+
+```php
+ 'mysql',
+ 'database_name' => $database['name'],
+ 'server' => $database['host'],
+ 'username' => $database['user'],
+ 'password' => $database['password'],
+ 'charset' => 'utf8'
+]);
+
+// Wyodrębnij $license z pliku versions.php bez uruchamiania die()
+$source = file_get_contents('updates/versions.php');
+
+// Wyciągnij wszystkie wpisy $license['key']['domain'] itd.
+preg_match_all('/\$license\[\'([^\']*)\'\]\[\'domain\'\]\s*=\s*\'([^\']*)\';/', $source, $m_domain);
+preg_match_all('/\$license\[\'([^\']*)\'\]\[\'valid_to_date\'\]\s*=\s*\'([^\']*)\';/', $source, $m_date);
+preg_match_all('/\$license\[\'([^\']*)\'\]\[\'valid_to_version\'\]\s*=\s*\'([^\']*)\';/', $source, $m_ver);
+
+// Zbuduj mapę po kluczu
+$dates = array_combine($m_date[1], $m_date[2]);
+$vers = array_combine($m_ver[1], $m_ver[2]);
+
+$count = 0;
+foreach ($m_domain[1] as $i => $key) {
+ $domain = $m_domain[2][$i];
+ $row = [
+ 'key' => $key,
+ 'domain' => $domain,
+ 'valid_to_date' => ($dates[$key] ?? '') ?: null,
+ 'valid_to_version' => ($vers[$key] ?? '') ?: null,
+ 'beta' => 0,
+ ];
+ // Pomiń jeśli już istnieje
+ if ($mdb->has('pp_update_licenses', ['key' => $key])) {
+ echo "SKIP (już istnieje): $domain ($key) ";
+ continue;
+ }
+ $mdb->insert('pp_update_licenses', $row);
+ echo "OK: $domain ($key) ";
+ $count++;
+}
+echo "Zmigrowano $count licencji. ";
+echo "USUŃ ten plik z serwera! ";
+```
+
+**Step 2: Wgraj na serwer i uruchom**
+
+```
+https://cmspro.project-dc.pl/_migrate_licenses.php
+```
+
+Oczekiwane: lista "OK: domena (klucz)" dla każdej licencji, na końcu podsumowanie.
+
+**Step 3: Usuń skrypt z serwera**
+
+Skasuj `_migrate_licenses.php` — nie commituj go do repozytorium.
+
+**Step 4: Ustaw flagę beta dla swoich testowych stron**
+
+W panelu admina `/admin/releases/main_view/` → zakładka Licencje → przy swoich testowych domenach kliknij "stable" aby przełączyć na "beta".
+
+---
+
+### Task 9: Przebuduj `updates/versions.php`
+
+**Files:**
+- Modify: `updates/versions.php`
+
+> **WAŻNE:** Wykonaj ten krok dopiero po Task 8 (migracja danych do DB).
+
+**Step 1: Zastąp całą zawartość pliku**
+
+```php
+
+require_once '../config.php';
+require_once '../libraries/medoo/medoo.php';
+
+$mdb = new medoo( [
+ 'database_type' => 'mysql',
+ 'database_name' => $database['name'],
+ 'server' => $database['host'],
+ 'username' => $database['user'],
+ 'password' => $database['password'],
+ 'charset' => 'utf8'
+] );
+
+$current_ver = 1691; // aktualizowane automatycznie przez build-update.ps1
+
+// 1. Skan filesystem — lista istniejących ZIPów
+$versions = [];
+for ( $i = 1; $i <= $current_ver; $i++ )
+{
+ $dir = substr( number_format( $i / 1000, 3 ), 0, strlen( number_format( $i / 1000, 3 ) ) - 2 ) . '0';
+ $version_old = number_format( $i / 1000, 2 );
+ $version_new = number_format( $i / 1000, 3 );
+
+ if ( file_exists( '../updates/' . $dir . '/ver_' . $version_old . '.zip' ) )
+ $versions[] = $version_old;
+
+ if ( file_exists( '../updates/' . $dir . '/ver_' . $version_new . '.zip' ) )
+ $versions[] = $version_new;
+}
+$versions = array_unique( $versions );
+
+// 2. Walidacja klucza licencji
+$license = $mdb->get( 'pp_update_licenses', '*', [ 'key' => ( $_GET['key'] ?? '' ) ] );
+if ( !$license )
+ die();
+
+// 3. Sprawdź ważność daty
+if ( $license['valid_to_date'] && $license['valid_to_date'] < date( 'Y-m-d' ) )
+ die();
+
+// 4. Auto-discovery: rejestruj nowe ZIPy jako beta
+$known = array_flip( $mdb->select( 'pp_update_versions', 'version', [] ) ?: [] );
+foreach ( $versions as $ver )
+{
+ if ( !isset( $known[$ver] ) )
+ {
+ @$mdb->insert( 'pp_update_versions', [
+ 'version' => $ver,
+ 'channel' => 'beta',
+ 'created_at' => date( 'Y-m-d H:i:s' )
+ ] );
+ $known[$ver] = true;
+ }
+}
+
+// 5. Filtruj wersje wg kanału (beta widzi beta+stable, reszta tylko stable)
+$channels = $license['beta'] ? [ 'beta', 'stable' ] : [ 'stable' ];
+$allowed = array_flip( $mdb->select( 'pp_update_versions', 'version', [ 'channel' => $channels ] ) ?: [] );
+
+// 6. Wypisz dostępne wersje
+$valid_to_version = $license['valid_to_version'];
+foreach ( $versions as $ver )
+{
+ if ( !isset( $allowed[$ver] ) )
+ continue;
+
+ if ( $valid_to_version && $ver > $valid_to_version )
+ continue;
+
+ echo $ver . PHP_EOL;
+}
+```
+
+**Step 2: Weryfikacja składni**
+
+```bash
+php -l updates/versions.php
+```
+
+**Step 3: Test ręczny**
+
+Otwórz w przeglądarce (podaj klucz jednej z migrowanych licencji):
+```
+https://cmspro.project-dc.pl/updates/versions.php?key=TWOJ_KLUCZ
+```
+
+Oczekiwane:
+- Dla klucza z `beta=0`: zwraca TYLKO wersje `channel='stable'` (pusta lista jeśli żadna jeszcze niepromowana)
+- Dla klucza z `beta=1`: zwraca wersje `channel='beta'` i `'stable'`
+- Dla nieprawidłowego klucza: brak odpowiedzi (die())
+
+**Step 4: Sprawdź panel aktualizacji u klienta**
+
+Na swojej testowej stronie (beta=1) otwórz `/admin/update/main_view/` — powinien widzieć dostępne wersje.
+
+---
+
+### Task 10: Commit końcowy
+
+**Step 1: Sprawdź status**
+
+```bash
+git status
+git diff updates/versions.php
+```
+
+**Step 2: Commit**
+
+```bash
+git add \
+ .updateignore \
+ autoload/admin/factory/class.Releases.php \
+ autoload/admin/controls/class.Releases.php \
+ autoload/admin/view/class.Releases.php \
+ admin/templates/releases/main-view.php \
+ templates/additional-menu.php \
+ updates/versions.php
+
+git commit -m "$(cat <<'EOF'
+feat: dwukanalowy system aktualizacji (beta/stable) + zarzadzanie licencjami
+
+- Nowy modul admin\Releases: lista wersji z promocja beta→stable,
+ CRUD licencji z flaga beta
+- versions.php czyta z DB (pp_update_licenses, pp_update_versions)
+ zamiast hardkodowanej tablicy $license
+- Auto-discovery: nowe ZIPy automatycznie rejestrowane jako 'beta'
+- Calosc wykluczona z paczek klientow przez .updateignore
+
+Co-Authored-By: Claude Sonnet 4.6
+EOF
+)"
+```
+
+---
+
+## Weryfikacja end-to-end
+
+Po wdrożeniu sprawdź ręcznie:
+
+1. **Nowy ZIP (beta):** Wrzuć nowy ZIP na serwer → odpytaj `versions.php` kluczem beta → pojawia się nowa wersja → w panelu admina widoczna jako `beta`
+2. **Promocja:** Kliknij "Promuj →stable" → odpytaj kluczem `beta=0` → wersja pojawia się na liście
+3. **Cofnięcie:** Kliknij "Cofnij →beta" → klient `beta=0` nie widzi wersji
+4. **Licencja:** Dodaj nową licencję przez formularz → weryfikuj w phpMyAdmin
+5. **Toggle beta:** Kliknij "stable" przy licencji → zmienia się na "beta"
+6. **Zabezpieczenie:** Wgraj nową wersję CMS do klienta → sprawdź czy `admin/templates/releases/` i `autoload/admin/*/class.Releases.php` NIE znalazły się w ZIPie (build dry-run)