migrations.php

main 188 lines · 5.8 KB Raw
Alex Alex Commit Initial commit 01/10/2026 09:20
1<?php
2/**
3 * Title: Migrations
4 * Icon: fa-database
5 * Group: Operations
6 * Order: 30
7 * Description: Apply SQL updates from SQL/migrations without phpMyAdmin.
8 */
9
10if (!defined('ACP_ROOT')) {
11 http_response_code(403);
12 die('Direct access denied.');
13}
14
15function acp_migration_state_label(string $state): string {
16 switch ($state) {
17 case 'applied': return 'Applied';
18 case 'pending': return 'Pending';
19 case 'changed': return 'Changed after apply';
20 case 'missing': return 'Applied file missing';
21 default: return ucfirst($state);
22 }
23}
24
25function acp_migration_state_class(string $state): string {
26 switch ($state) {
27 case 'applied': return 'green';
28 case 'pending': return 'amber';
29 case 'changed':
30 case 'missing':
31 return 'red';
32 default: return 'grey';
33 }
34}
35
36if ($_SERVER['REQUEST_METHOD'] === 'POST') {
37 $action = (string)($_POST['do'] ?? '');
38
39 if ($action === 'run_pending') {
40 $results = znote_migrations_run_pending();
41 if (!$results) {
42 acp_flash_info('No pending migrations.');
43 } else {
44 $ok = 0;
45 $failed = '';
46 foreach ($results as $name => $result) {
47 if (!empty($result['ok'])) {
48 $ok++;
49 } else {
50 $failed = $name . ': ' . ($result['message'] ?? 'failed');
51 break;
52 }
53 }
54
55 if ($failed !== '') {
56 acp_flash_error(h($failed));
57 } else {
58 acp_log('system.migrations', '', ['applied' => array_keys($results)]);
59 acp_flash_success('Applied ' . $ok . ' migration' . ($ok === 1 ? '' : 's') . '.');
60 }
61 }
62 acp_redirect('migrations');
63 }
64
65 if ($action === 'run_one') {
66 $name = basename((string)($_POST['migration'] ?? ''));
67 $result = znote_migration_run($name);
68 if (!empty($result['ok'])) {
69 acp_log('system.migration', $name, ['statements' => $result['statements'] ?? 0]);
70 acp_flash_success(h($name) . ': ' . h((string)$result['message']));
71 } else {
72 acp_flash_error(h($name) . ': ' . h((string)$result['message']));
73 }
74 acp_redirect('migrations');
75 }
76
77 acp_redirect('migrations');
78}
79
80$status = znote_migrations_status();
81$pending = array_filter($status, static fn(array $row): bool => $row['state'] === 'pending');
82$applied = array_filter($status, static fn(array $row): bool => $row['state'] === 'applied');
83$warnings = array_filter($status, static fn(array $row): bool => in_array($row['state'], array('changed', 'missing'), true));
84?>
85
86<div class="acp-stats">
87 <?php
88 acp_stat('Pending', count($pending), 'fa-clock-o', null, count($pending) ? 'amber' : 'green');
89 acp_stat('Applied', count($applied), 'fa-check', null, 'blue');
90 acp_stat('Warnings', count($warnings), 'fa-exclamation-triangle', null, count($warnings) ? 'red' : 'grey');
91 ?>
92</div>
93
94<section class="acp-card">
95 <header class="acp-card-head">
96 <h2>Database migrations</h2>
97 <p>Runs SQL files from <code>SQL/migrations</code> and records successful executions in <code>znote_migrations</code>.</p>
98 </header>
99 <div class="acp-card-body">
100 <?php if (!$status): ?>
101 <?php acp_empty('No migration files found.', 'fa-database'); ?>
102 <?php else: ?>
103 <div class="acp-actions" style="margin-bottom:14px;">
104 <form method="post" data-confirm="Run all pending migrations now?">
105 <?= acp_csrf_field() ?>
106 <input type="hidden" name="do" value="run_pending">
107 <button class="acp-btn" type="submit" <?= !$pending ? 'disabled' : '' ?>>
108 <i class="fa fa-play"></i> Run pending migrations
109 </button>
110 </form>
111 </div>
112
113 <div class="acp-table-wrap">
114 <table class="acp-table">
115 <thead>
116 <tr>
117 <th>Migration</th>
118 <th>Status</th>
119 <th>Checksum</th>
120 <th>Executed</th>
121 <th>Time</th>
122 <th style="width:1%">Action</th>
123 </tr>
124 </thead>
125 <tbody>
126 <?php foreach ($status as $row):
127 $state = (string)$row['state'];
128 $appliedRow = is_array($row['applied']) ? $row['applied'] : array();
129 ?>
130 <tr>
131 <td>
132 <strong><?= h($row['name']) ?></strong>
133 <?php if (!empty($row['size'])): ?>
134 <br><small><?= h(number_format((int)$row['size'])) ?> bytes</small>
135 <?php endif; ?>
136 </td>
137 <td>
138 <span class="acp-pill acp-pill--<?= h(acp_migration_state_class($state)) ?>">
139 <?= h(acp_migration_state_label($state)) ?>
140 </span>
141 </td>
142 <td><code title="<?= h($row['checksum']) ?>"><?= h(substr((string)$row['checksum'], 0, 12)) ?></code></td>
143 <td>
144 <?php if (!empty($appliedRow['executed_at'])): ?>
145 <?= h(date('Y-m-d H:i:s', (int)$appliedRow['executed_at'])) ?>
146 <?php else: ?>
147 <span class="is-muted">Never</span>
148 <?php endif; ?>
149 </td>
150 <td>
151 <?php if (isset($appliedRow['execution_time_ms'])): ?>
152 <?= h((string)(int)$appliedRow['execution_time_ms']) ?> ms
153 <?php else: ?>
154 <span class="is-muted">-</span>
155 <?php endif; ?>
156 </td>
157 <td>
158 <?php if ($state === 'pending'): ?>
159 <form method="post" data-confirm="Run this migration now?">
160 <?= acp_csrf_field() ?>
161 <input type="hidden" name="do" value="run_one">
162 <input type="hidden" name="migration" value="<?= h($row['name']) ?>">
163 <button class="acp-btn acp-btn--sm" type="submit">Run</button>
164 </form>
165 <?php elseif ($state === 'changed'): ?>
166 <span class="is-muted">File changed</span>
167 <?php elseif ($state === 'missing'): ?>
168 <span class="is-muted">Missing</span>
169 <?php else: ?>
170 <span class="is-muted">Done</span>
171 <?php endif; ?>
172 </td>
173 </tr>
174 <?php endforeach; ?>
175 </tbody>
176 </table>
177 </div>
178 <?php endif; ?>
179 </div>
180</section>
181
182<div class="acp-flash acp-flash--info">
183 <i class="fa fa-info-circle"></i>
184 <span>
185 Use this page after pulling/updating ZnoteX. Payment IPNs and normal visitors never run migrations; only admins can apply them here.
186 </span>
187</div>
188
Top