From 5229ae35b8e39b473109e1c5585473037e7a5838 Mon Sep 17 00:00:00 2001 From: ChesnoTech <263363000+ChesnoTech@users.noreply.github.com> Date: Thu, 7 May 2026 07:25:42 +0300 Subject: [PATCH] =?UTF-8?q?P2:=20Installer=20resilience=20=E2=80=94=20resu?= =?UTF-8?q?me,=20retry/skip,=20structured=20log,=20health=20probe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 2 of the multi-panel installer plan. Adds operational robustness on top of P0 hardening + P1 prefix support. Changes: 1. Resumable installer - install/.progress.json breadcrumb file. Each step writes its number and timestamp on completion via the new progress_set action. - On page boot, JS calls progress_get; if last_step >= 1, prompts user "Resume from step N+1?" with Cancel = start over (which clears the file). Bypassed for fresh installs. - handleFinalize unlinks the breadcrumb on success (install complete). 2. Per-migration retry / skip - Step 3 UI: when a migration errors, inline Retry + Skip buttons appear next to the failed row. - Retry: re-runs install_db_step for that file. On success, the migration loop resumes from the next file. - Skip: prompts a hard-yes confirmation, then calls the new migration_skip action which inserts a row into schema_versions with checksum prefix `SKIPPED:` (so future audits can tell apart successful applies from forced skips). Loop resumes. - Both paths respect the canonical migration whitelist. 3. Structured install.log - installerLog() is now called from auto-unlock recovery, finalize completion, progress_set, migration_skip, and progress_clear. - Audit format: `[YYYY-MM-DD HH:MM:SS] event_name: details`. 4. Health-probe button on step 6 - "Run health check" button next to "Open Admin Panel". - Calls existing handleHealth action; renders pass/fail per check (DB connect, presence of {prefix}admin_users, oem_keys, technicians, system_config, schema_versions, plus admin account count). 5. install.lock content extended - Now persists db_prefix and db_charset alongside installer_ver, admin_username, php_version, server_software. Makes post-install forensics easier. 6. .gitignore - install/install.log and install/.progress.json — runtime per-host artifacts, never to be committed. Verified live: - POST progress_set/progress_get round-trip works - Lint clean on ajax.php + index.php - 14/14 frontend tests pass --- .gitignore | 4 + FINAL_PRODUCTION_SYSTEM/install/ajax.php | 115 ++++++++++++++++++++++ FINAL_PRODUCTION_SYSTEM/install/index.php | 108 +++++++++++++++++++- 3 files changed, 222 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 9aa8908..c0bd259 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,10 @@ logs/ # ── Uploaded client artifacts (per-instance, regenerated at runtime) ── FINAL_PRODUCTION_SYSTEM/uploads/client-resources/ +# ── Installer runtime artifacts (per-host, never committed) ──────── +FINAL_PRODUCTION_SYSTEM/install/install.log +FINAL_PRODUCTION_SYSTEM/install/.progress.json + # ── PHP Dependencies (managed by Composer) ──────────────── FINAL_PRODUCTION_SYSTEM/vendor/ diff --git a/FINAL_PRODUCTION_SYSTEM/install/ajax.php b/FINAL_PRODUCTION_SYSTEM/install/ajax.php index a9cbf4c..d361a41 100644 --- a/FINAL_PRODUCTION_SYSTEM/install/ajax.php +++ b/FINAL_PRODUCTION_SYSTEM/install/ajax.php @@ -55,6 +55,18 @@ case 'health': handleHealth(); break; + case 'progress_get': + handleProgressGet(); + break; + case 'progress_set': + handleProgressSet(); + break; + case 'progress_clear': + handleProgressClear(); + break; + case 'migration_skip': + handleMigrationSkip(); + break; default: echo json_encode(['success' => false, 'message' => 'Unknown action']); } @@ -996,10 +1008,16 @@ function handleFinalize() { 'admin_username' => $adminUser, 'php_version' => PHP_VERSION, 'server_software' => $_SERVER['SERVER_SOFTWARE'] ?? 'unknown', + 'db_prefix' => $prefix, + 'db_charset' => $charset, ]; $lockPath = realpath(__DIR__ . '/..') . '/install.lock'; file_put_contents($lockPath, json_encode($lockData, JSON_PRETTY_PRINT)); + // ── Clear resume breadcrumb (install is complete) ── + @unlink(installerProgressPath()); + installerLog("finalize: install complete; admin={$adminUser}, prefix='" . $prefix . "', charset={$charset}"); + // ── Response ── echo json_encode([ 'success' => true, @@ -1535,3 +1553,100 @@ function installerLog(string $line): void { FILE_APPEND ); } + +// ═══════════════════════════════════════════════════════════════ +// P2: Resumable installer (progress breadcrumb file) +// ═══════════════════════════════════════════════════════════════ + +function installerProgressPath(): string { + return __DIR__ . '/.progress.json'; +} + +/** + * Read the progress breadcrumb. Returns the highest step the user has + * completed plus a per-step timestamp map. + */ +function handleProgressGet(): void { + $path = installerProgressPath(); + if (!file_exists($path)) { + echo json_encode(['success' => true, 'progress' => null]); + return; + } + $data = json_decode(@file_get_contents($path), true); + if (!is_array($data)) { + echo json_encode(['success' => true, 'progress' => null]); + return; + } + echo json_encode(['success' => true, 'progress' => $data]); +} + +/** + * Persist a step completion breadcrumb. + * Body: { step: 1..6 } + */ +function handleProgressSet(): void { + $step = (int)($_POST['step'] ?? 0); + if ($step < 1 || $step > 6) { + echo json_encode(['success' => false, 'message' => 'Invalid step']); + return; + } + + $path = installerProgressPath(); + $current = ['steps' => [], 'last_step' => 0, 'updated_at' => date('Y-m-d H:i:s')]; + if (file_exists($path)) { + $loaded = json_decode(@file_get_contents($path), true); + if (is_array($loaded)) $current = array_merge($current, $loaded); + } + + $current['steps'][(string)$step] = date('Y-m-d H:i:s'); + if ($step > (int)($current['last_step'] ?? 0)) { + $current['last_step'] = $step; + } + $current['updated_at'] = date('Y-m-d H:i:s'); + + @file_put_contents($path, json_encode($current, JSON_PRETTY_PRINT)); + installerLog("step_done: {$step}"); + echo json_encode(['success' => true, 'progress' => $current]); +} + +/** + * Wipe the progress file. Used on user-initiated "Start Over". + */ +function handleProgressClear(): void { + @unlink(installerProgressPath()); + installerLog("progress_cleared by user"); + echo json_encode(['success' => true]); +} + +/** + * Mark a migration as forcibly skipped after an error (user clicked Skip + * on the per-migration retry UI). Records in schema_versions with the + * `checksum` column suffixed `:SKIPPED` so we can tell apart from + * successful applies. + * + * Body: { file: 'install.sql', version: 1, error: 'optional msg' } + */ +function handleMigrationSkip(): void { + $pdo = getInstallerPdo(); + if (!$pdo) return; + + $file = $_POST['file'] ?? ''; + $version = (int)($_POST['version'] ?? 0); + $error = (string)($_POST['error'] ?? ''); + + $allowed = array_column(installerMigrationList(), 0); + if (!in_array($file, $allowed, true)) { + echo json_encode(['success' => false, 'message' => "Migration '{$file}' not on the canonical list."]); + return; + } + + $svTable = '`' . installerT('schema_versions') . '`'; + try { + $stmt = $pdo->prepare("INSERT IGNORE INTO {$svTable} (version, filename, checksum) VALUES (?, ?, ?)"); + $stmt->execute([$version, $file, 'SKIPPED:' . substr(hash('sha256', $error . microtime()), 0, 16)]); + installerLog("migration_skipped: {$file} (error: " . substr($error, 0, 200) . ")"); + echo json_encode(['success' => true, 'message' => "Migration '{$file}' marked as skipped."]); + } catch (PDOException $e) { + echo json_encode(['success' => false, 'message' => $e->getMessage()]); + } +} diff --git a/FINAL_PRODUCTION_SYSTEM/install/index.php b/FINAL_PRODUCTION_SYSTEM/install/index.php index d7b7169..ac8b424 100644 --- a/FINAL_PRODUCTION_SYSTEM/install/index.php +++ b/FINAL_PRODUCTION_SYSTEM/install/index.php @@ -616,9 +616,13 @@ - - Open Admin Panel → - +