v0.29.10 — Admin UI refonte (modals + onglets), emails de release, 4-digit versions
Admin backoffice — UI redesign : - Pages versions.php + licenses.php : remplace les <details> inline qui débordaient horizontalement par UN bouton « ✎ Modifier » par row qui ouvre une modal <dialog> avec onglets. 5 tabs versions (Méta, Notes, BÊTA, Channels, Avancé) + 6 tabs licenses (Prolonger, Slots, Channel, BÊTA, Lock, Contacts, Machines). Délégation JS unique pour les onglets. - Bouton 📋 Copier la clé license dans chaque row (Clipboard API + feedback visuel ✓ vert 1.5s). Évite le détour par phpMyAdmin pour transmettre la clé aux clients. - Overlay « hashing en cours » plein écran sur tous les boutons de hash (3-5 min sur OVH pour 13 Go ZIP). Spinner CSS + message contextualisé par scope (bulk vs single). - Date de release passée de datetime-local à date (l'heure n'a pas de sens UX), avec défaut = aujourd'hui pour release_at et aujourd'hui-1an pour min_license_date (= license standard couvre les releases sur 1 an). Emails de notification release : - Migration 004 : colonne contact_emails TEXT NULL sur licenses (CSV) - Onglet « Contacts » sur la modal licenses pour saisir les emails par license (parsing tolérant : CSV, ligne par ligne, point-virgule) - Bouton « ✉ Notifier » par version : POST notify_release filtre les licenses éligibles (channel match + min_license_date + can_see_betas pour BÊTA) et envoie un email HTML à chaque contact (dédup global) - Template email table-based + bgcolor (compat Outlook/Word engine), navy foncé #0F172A, logo Asterion en CID embed (= affichage direct sans demande de permission Outlook), bouton download installer centré (align="center" + margin auto), release notes en <pre> - Mailer.php helper : parse emails, multipart/related avec attachments inline, fallback execCommand pour clipboard 4-digit version support (X.Y.Z.B) : - SemVer Parse/CompareTo/ToString gèrent 3 ou 4 digits ; Build absent = 0 implicite (1.5.4 == 1.5.4.0 < 1.5.4.13). HasExplicitBuild préserve le format d'origine au round-trip. - Regex InstallationRegistry étendue avec (?:\.\d+)? → reconnaît « PROSERVE v1.5.4.13 » côte-à-côte avec « PROSERVE v1.5.4 » sur disque - Server-side : versions.php, launcher.php, DownloadUrl.php, api/index.php, Releasenotes.php — toutes les regex de validation acceptent le 4ᵉ digit - Use case : dev/test iterations cohabitant avec leur release stable Bugs fixes : - migrate.php : strip ligne par ligne les commentaires SQL avant le check is-empty. Sans ça, le PREMIER chunk d'un fichier migration (= header + premier ALTER) commençait par `--` et était silencieusement skip → ALTER jamais appliqué. Affectait migrations 003, 004. - SignManifest::getOrComputeSha256 ignorait son cache interne quand force demandé par le caller, retournant l'ancien hash en 0 ms même après re-upload SFTP (avec mtime préservé). Propage maintenant le flag $force. Bouton « 🔁 Hash » per-row force maintenant un re-calcul systématique. - DownloadManager : 416 (Range Not Satisfiable) ajouté aux URL-refresh triggers, avec HEAD probe pour comparer taille serveur vs manifest → message d'erreur explicite si ZIP tronqué. Bps display lissé sur une fenêtre glissante de 12 samples (3 s) → plus de clignotement quand un segment finit / Polly retry. SHA mismatch popup enrichi avec les deux SHAs (attendu vs calculé) extraits via regex de l'exception. - DownloadUrl.php : signature de l'URL utilisait un template hardcodé /builds/proserve-{version}.zip, ignorant tout rename serveur. Lit maintenant download.url du manifest et signe le filename réel. Strings i18n (5 langues) : - ~15 nouveaux : SHA mismatch enrichi avec sources, 416 size mismatch, stale manifest, manifest refreshed auto-retry, force fresh menu Bumps : 0.29.7 → 0.29.10 (4-digit support + accumulated UI fixes). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -56,16 +56,26 @@ final class SignManifest
|
||||
* Le cache est invalidé dès que la taille OU mtime du fichier a changé,
|
||||
* ce qui permet de garder le cache à jour sans intervention manuelle.
|
||||
*
|
||||
* IMPORTANT : si <code>$force</code> est true, on IGNORE le cache et on
|
||||
* recalcule (puis on rafraîchit le cache). Sans ça, le cache interne
|
||||
* supplantait silencieusement le `--force` du caller — bug subtil parce
|
||||
* que la couche outer de `run()` avait déjà skip le cache (sha REPLACE
|
||||
* ou force=true), mais ce helper retombait sur sa propre cache lookup
|
||||
* (clé = realpath) → retournait instantanément l'ancien hash sans
|
||||
* recalculer, même après que l'opérateur ait re-uploadé le ZIP via
|
||||
* SFTP avec mtime préservé.
|
||||
*
|
||||
* @param array<string, array{size:int,mtime:int,sha256:string}> $cache
|
||||
* @return array{sha256:string, fromCache:bool, durationMs:int}
|
||||
*/
|
||||
private function getOrComputeSha256(string $path, array &$cache): array
|
||||
private function getOrComputeSha256(string $path, array &$cache, bool $force = false): array
|
||||
{
|
||||
$size = filesize($path) ?: 0;
|
||||
$mtime = filemtime($path) ?: 0;
|
||||
$key = realpath($path) ?: $path;
|
||||
|
||||
if (isset($cache[$key])
|
||||
if (!$force
|
||||
&& isset($cache[$key])
|
||||
&& ($cache[$key]['size'] ?? -1) === $size
|
||||
&& ($cache[$key]['mtime'] ?? -1) === $mtime
|
||||
&& !empty($cache[$key]['sha256'])) {
|
||||
@@ -192,7 +202,7 @@ final class SignManifest
|
||||
}
|
||||
}
|
||||
|
||||
$r = $this->getOrComputeSha256($zip, $cache);
|
||||
$r = $this->getOrComputeSha256($zip, $cache, $force);
|
||||
$cacheChanged = true;
|
||||
$note = $r['fromCache'] ? '(cache)' : "(calculé en {$r['durationMs']} ms)";
|
||||
$this->out(" [hash] $version : " . basename($zip) . " ($size octets) sha256={$r['sha256']} $note");
|
||||
@@ -231,7 +241,7 @@ final class SignManifest
|
||||
&& ($cache[$lkey]['sha256'] ?? '') === $existingLsha) {
|
||||
$this->out(" [launcher] v{$lver} : " . basename($lpath) . " ({$lsize} octets) — cache hit");
|
||||
} else {
|
||||
$r = $this->getOrComputeSha256($lpath, $cache);
|
||||
$r = $this->getOrComputeSha256($lpath, $cache, $force);
|
||||
$cacheChanged = true;
|
||||
$launcher['download']['sha256'] = $r['sha256'];
|
||||
$note = $r['fromCache'] ? '(cache)' : "(calculé en {$r['durationMs']} ms)";
|
||||
|
||||
@@ -70,7 +70,23 @@ foreach ($files as $f) {
|
||||
// Découpe naïve sur les ';' suivis d'un saut de ligne (suffit pour notre schéma)
|
||||
$statements = array_filter(array_map('trim', preg_split('/;\s*\n/', $sql) ?: []));
|
||||
foreach ($statements as $stmt) {
|
||||
if ($stmt === '' || str_starts_with($stmt, '--')) continue;
|
||||
// Strip les lignes de commentaire SQL (--) du début / intercalées avant
|
||||
// de checker si le statement est vide. Sinon : un fichier migration qui
|
||||
// commence par un header de commentaires se retrouve dans le PREMIER
|
||||
// chunk avec son premier ALTER (séparés par \n pas par ;\n) ; le chunk
|
||||
// commence donc par `--`, le check str_starts_with le déclare vide,
|
||||
// et l'ALTER est silencieusement skip. Bug rapporté : migrations 003+
|
||||
// qui n'ont qu'un seul ALTER après un bloc de commentaires en tête
|
||||
// n'étaient jamais appliquées (output vide entre "--- 003 ---" et "===").
|
||||
$lines = preg_split('/\r?\n/', $stmt) ?: [];
|
||||
$kept = [];
|
||||
foreach ($lines as $line) {
|
||||
$t = ltrim($line);
|
||||
if ($t === '' || str_starts_with($t, '--')) continue;
|
||||
$kept[] = $line;
|
||||
}
|
||||
$stmt = trim(implode("\n", $kept));
|
||||
if ($stmt === '') continue;
|
||||
try {
|
||||
$pdo->exec($stmt);
|
||||
$first40 = substr(preg_replace('/\s+/', ' ', $stmt), 0, 60);
|
||||
|
||||
Reference in New Issue
Block a user