Files
cmsPRO/docs/plans/2026-02-28-update-channels.md

24 KiB

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:

./build-update.ps1 -ToTag v9.999 -DryRun

Step 3: Commit

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

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
namespace admin\factory;

class Releases
{
  public static function get_versions(): array
  {
    global $mdb;
    $rows = $mdb->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

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
namespace admin\controls;

class Releases
{
  public static function main_view(): string
  {
    return \admin\view\Releases::main_view();
  }

  public static function promote(): void
  {
    $version = trim(\S::get('version'));
    if ($version)
      \admin\factory\Releases::promote($version);
    header('Location: /admin/releases/main_view/');
    exit;
  }

  public static function demote(): void
  {
    $version = trim(\S::get('version'));
    if ($version)
      \admin\factory\Releases::demote($version);
    header('Location: /admin/releases/main_view/');
    exit;
  }

  public static function save_license(): void
  {
    \admin\factory\Releases::save_license($_POST);
    \S::set_message('Licencja została zapisana.');
    header('Location: /admin/releases/main_view/');
    exit;
  }

  public static function delete_license(): void
  {
    $id = (int)\S::get('id');
    if ($id)
      \admin\factory\Releases::delete_license($id);
    header('Location: /admin/releases/main_view/');
    exit;
  }

  public static function toggle_beta(): void
  {
    $id = (int)\S::get('id');
    if ($id)
      \admin\factory\Releases::toggle_beta($id);
    header('Location: /admin/releases/main_view/');
    exit;
  }
}

Step 2: Weryfikacja składni

php -l autoload/admin/controls/class.Releases.php

Task 5: Utwórz admin\view\Releases

Files:

  • Create: autoload/admin/view/class.Releases.php

Step 1: Napisz klasę

<?php
namespace admin\view;

class Releases
{
  public static function main_view(): string
  {
    $tpl            = new \Tpl;
    $tpl->versions  = \admin\factory\Releases::get_versions();
    $tpl->licenses  = \admin\factory\Releases::get_licenses();
    return $tpl->render('releases/main-view');
  }
}

Step 2: Weryfikacja składni

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 global $user; ?>

<div class="row">
  <div class="col-lg-12">

    <!-- TABS NAV -->
    <ul class="nav nav-tabs releases-tabs" style="margin-bottom:20px">
      <li class="active"><a href="#" data-tab="versions">Wersje</a></li>
      <li><a href="#" data-tab="licenses">Licencje</a></li>
    </ul>

    <!-- TAB: WERSJE -->
    <div id="tab-versions">
      <div class="panel panel-default">
        <div class="panel-heading"><h3 class="panel-title">Wersje</h3></div>
        <div class="panel-body">
          <table class="table table-striped table-condensed">
            <thead>
              <tr>
                <th>Wersja</th><th>Kanał</th><th>Dodana</th>
                <th>Promocja do stable</th><th>ZIP</th><th>Akcje</th>
              </tr>
            </thead>
            <tbody>
            <? foreach ($this->versions as $v): ?>
              <tr>
                <td><strong><?= htmlspecialchars($v['version']) ?></strong></td>
                <td>
                  <? if ($v['channel'] == 'stable'): ?>
                    <span class="label label-success">stable</span>
                  <? else: ?>
                    <span class="label label-warning">beta</span>
                  <? endif; ?>
                </td>
                <td><?= $v['created_at'] ? substr($v['created_at'], 0, 10) : '-' ?></td>
                <td><?= $v['promoted_at'] ? substr($v['promoted_at'], 0, 10) : '-' ?></td>
                <td><?= $v['zip_exists'] ? '<span class="text-success">✓</span>' : '<span class="text-danger">✗</span>' ?></td>
                <td>
                  <? if ($v['channel'] == 'beta'): ?>
                    <a href="/admin/releases/promote/?version=<?= urlencode($v['version']) ?>"
                       class="btn btn-xs btn-success"
                       onclick="return confirm('Promować <?= $v['version'] ?> do stable?')">
                      Promuj →stable
                    </a>
                  <? else: ?>
                    <a href="/admin/releases/demote/?version=<?= urlencode($v['version']) ?>"
                       class="btn btn-xs btn-default"
                       onclick="return confirm('Cofnąć <?= $v['version'] ?> do beta?')">
                      Cofnij →beta
                    </a>
                  <? endif; ?>
                </td>
              </tr>
            <? endforeach; ?>
            <? if (!$this->versions): ?>
              <tr><td colspan="6" class="text-center text-muted">Brak wersji w bazie. Wersje będą rejestrowane automatycznie przy pierwszym odpytaniu versions.php.</td></tr>
            <? endif; ?>
            </tbody>
          </table>
        </div>
      </div>
    </div><!-- /tab-versions -->

    <!-- TAB: LICENCJE -->
    <div id="tab-licenses" style="display:none">
      <div class="panel panel-default">
        <div class="panel-heading" style="display:flex;justify-content:space-between;align-items:center">
          <h3 class="panel-title">Licencje</h3>
          <button class="btn btn-sm btn-system" id="btn-add-license">+ Dodaj licencję</button>
        </div>
        <div class="panel-body">

          <!-- Formularz dodawania/edycji (ukryty) -->
          <div id="license-form-box" style="display:none;border:1px solid #ddd;padding:15px;margin-bottom:20px;background:#f9f9f9">
            <h4 id="license-form-title">Nowa licencja</h4>
            <form method="post" action="/admin/releases/save_license/">
              <input type="hidden" name="id" id="f-id" value="">
              <div class="row">
                <div class="form-group col-lg-3">
                  <label>Klucz (hash)</label>
                  <input type="text" name="key" id="f-key" class="form-control" placeholder="md5/hash lub pusty dla domyślnego">
                </div>
                <div class="form-group col-lg-3">
                  <label>Domena</label>
                  <input type="text" name="domain" id="f-domain" class="form-control" required>
                </div>
                <div class="form-group col-lg-2">
                  <label>Ważna do daty</label>
                  <input type="date" name="valid_to_date" id="f-valid-date" class="form-control">
                </div>
                <div class="form-group col-lg-2">
                  <label>Ważna do wersji</label>
                  <input type="text" name="valid_to_version" id="f-valid-ver" class="form-control" placeholder="np. 1.618">
                </div>
                <div class="form-group col-lg-1">
                  <label>Beta</label><br>
                  <input type="checkbox" name="beta" id="f-beta" value="1">
                </div>
                <div class="form-group col-lg-2">
                  <label>Notatka</label>
                  <input type="text" name="note" id="f-note" class="form-control">
                </div>
              </div>
              <button type="submit" class="btn btn-system btn-sm">Zapisz</button>
              <button type="button" class="btn btn-default btn-sm" id="btn-cancel-license">Anuluj</button>
            </form>
          </div><!-- /license-form-box -->

          <table class="table table-striped table-condensed">
            <thead>
              <tr>
                <th>Domena</th><th>Klucz</th><th>Do daty</th>
                <th>Do wersji</th><th>Beta</th><th>Notatka</th><th>Akcje</th>
              </tr>
            </thead>
            <tbody>
            <? foreach ($this->licenses as $lic): ?>
              <tr>
                <td><?= htmlspecialchars($lic['domain']) ?></td>
                <td><code title="<?= htmlspecialchars($lic['key']) ?>">
                  <?= $lic['key'] === '' ? '<em>(domyślny)</em>' : htmlspecialchars(substr($lic['key'], 0, 8)) . '…' ?>
                </code></td>
                <td><?= $lic['valid_to_date'] ?: '<em>∞</em>' ?></td>
                <td><?= $lic['valid_to_version'] ?: '<em>∞</em>' ?></td>
                <td>
                  <a href="/admin/releases/toggle_beta/?id=<?= $lic['id'] ?>"
                     class="label <?= $lic['beta'] ? 'label-info' : 'label-default' ?>"
                     title="Kliknij aby przełączyć">
                    <?= $lic['beta'] ? 'beta' : 'stable' ?>
                  </a>
                </td>
                <td><?= htmlspecialchars($lic['note'] ?? '') ?></td>
                <td>
                  <button class="btn btn-xs btn-default btn-edit-license"
                    data-id="<?= $lic['id'] ?>"
                    data-key="<?= htmlspecialchars($lic['key']) ?>"
                    data-domain="<?= htmlspecialchars($lic['domain']) ?>"
                    data-valid-date="<?= $lic['valid_to_date'] ?? '' ?>"
                    data-valid-ver="<?= $lic['valid_to_version'] ?? '' ?>"
                    data-beta="<?= $lic['beta'] ?>"
                    data-note="<?= htmlspecialchars($lic['note'] ?? '') ?>">Edytuj</button>
                  <a href="/admin/releases/delete_license/?id=<?= $lic['id'] ?>"
                     class="btn btn-xs btn-danger"
                     onclick="return confirm('Usunąć licencję <?= htmlspecialchars($lic['domain']) ?>?')">Usuń</a>
                </td>
              </tr>
            <? endforeach; ?>
            <? if (!$this->licenses): ?>
              <tr><td colspan="7" class="text-center text-muted">Brak licencji. Dodaj pierwszą lub uruchom skrypt migracji.</td></tr>
            <? endif; ?>
            </tbody>
          </table>

        </div>
      </div>
    </div><!-- /tab-licenses -->

  </div>
</div>

<script>
// Przełączanie tabów
$('.releases-tabs a').on('click', function(e) {
  e.preventDefault();
  $('.releases-tabs li').removeClass('active');
  $(this).parent().addClass('active');
  $('[id^="tab-"]').hide();
  $('#tab-' + $(this).data('tab')).show();
});

// Formularz: Dodaj nową licencję
$('#btn-add-license').on('click', function() {
  $('#license-form-title').text('Nowa licencja');
  $('#f-id').val('');
  $('#f-key, #f-domain, #f-valid-date, #f-valid-ver, #f-note').val('');
  $('#f-beta').prop('checked', false);
  $('#license-form-box').slideDown();
});

// Formularz: Edytuj istniejącą licencję
$('.btn-edit-license').on('click', function() {
  var d = $(this).data();
  $('#license-form-title').text('Edytuj licencję: ' + d.domain);
  $('#f-id').val(d.id);
  $('#f-key').val(d.key);
  $('#f-domain').val(d.domain);
  $('#f-valid-date').val(d.validDate);
  $('#f-valid-ver').val(d.validVer);
  $('#f-beta').prop('checked', d.beta == 1);
  $('#f-note').val(d.note);
  $('#license-form-box').slideDown();
  $('html, body').animate({ scrollTop: $('#license-form-box').offset().top - 20 }, 300);
});

// Anuluj formularz
$('#btn-cancel-license').on('click', function() {
  $('#license-form-box').slideUp();
});
</script>

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
// Menu tylko na serwerze dewelopera — wykluczone z .updateignore
?>
<div class="title">Developer</div>
<ul>
  <li>
    <a href="/admin/releases/main_view/">
      <img src="/admin/css/icons/settings-20-filled.svg">Releases &amp; Licencje
    </a>
  </li>
</ul>

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
// UWAGA: Uruchom JEDEN raz przez przeglądarkę, potem natychmiast usuń!
// URL: https://cmspro.project-dc.pl/_migrate_licenses.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'
]);

// 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)<br>";
    continue;
  }
  $mdb->insert('pp_update_licenses', $row);
  echo "OK: $domain ($key)<br>";
  $count++;
}
echo "<hr><strong>Zmigrowano $count licencji.</strong>";
echo "<br><strong style='color:red'>USUŃ ten plik z serwera!</strong>";

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

<?
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

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

git status
git diff updates/versions.php

Step 2: Commit

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 <noreply@anthropic.com>
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)