scheduler.php

main 145 lines · 5.3 KB Raw
Alex Alex Commit Initial commit 01/10/2026 09:20
1<?php
2/**
3 * Title: Task Scheduler
4 * Icon: fa-clock-o
5 * Group: Operations
6 * Order: 40
7 * Description: Recurring maintenance without a real cron - backups, admin log pruning, health checks.
8 */
9
10if (!defined('ACP_ROOT')) {
11 http_response_code(403);
12 die('Direct access denied.');
13}
14
15$tasks = scheduler_tasks();
16
17if ($_SERVER['REQUEST_METHOD'] === 'POST') {
18 $do = (string) ($_POST['do'] ?? '');
19
20 if ($do === 'save') {
21 foreach ($tasks as $id => $task) {
22 $enabled = !empty($_POST['enabled'][$id]) ? '1' : '0';
23 $interval = max(1, intv($_POST['interval_hours'][$id] ?? $task['default_interval_hours']));
24
25 setting_set('scheduler:' . $id . ':enabled', $enabled);
26 setting_set('scheduler:' . $id . ':interval_hours', (string) $interval);
27
28 if ($id === 'purge_admin_log') {
29 $keepDays = max(1, intv($_POST['keep_days'] ?? 90));
30 setting_set('scheduler:purge_admin_log:keep_days', (string) $keepDays);
31 }
32 }
33
34 acp_log('scheduler.settings_save');
35 acp_flash_success(t_default('acp.sched.saved', 'Scheduler settings saved.'));
36 acp_redirect('scheduler');
37 }
38
39 if ($do === 'run_now') {
40 $taskId = (string) ($_POST['task'] ?? '');
41 if (!isset($tasks[$taskId])) {
42 acp_flash_error(t_default('acp.sched.unknown_task', 'Unknown task.'));
43 acp_redirect('scheduler');
44 }
45
46 try {
47 $summary = (string) call_user_func($tasks[$taskId]['run']);
48 } catch (Throwable $e) {
49 $summary = 'error: ' . $e->getMessage();
50 }
51
52 scheduler_mark_run($taskId, $summary);
53 acp_log('scheduler.' . $taskId, '', array('summary' => $summary, 'manual' => true));
54 acp_flash_success(t_default('acp.sched.run_done', '{task} ran: {summary}', ['task' => $tasks[$taskId]['label'], 'summary' => $summary]));
55 acp_redirect('scheduler');
56 }
57}
58?>
59
60<section class="acp-card">
61 <header class="acp-card-head">
62 <h2><?= t_default('acp.sched.title', 'Task Scheduler') ?></h2>
63 <p><?= t_default('acp.sched.sub', 'No real cron here - each task runs opportunistically on the next page load once its interval has passed (public pages, and any admin panel page).') ?></p>
64 </header>
65 <div class="acp-card-body is-flush">
66 <form method="post">
67 <?= acp_csrf_field() ?>
68 <input type="hidden" name="do" value="save">
69 <div class="acp-table-wrap">
70 <table class="acp-table">
71 <thead>
72 <tr>
73 <th><?= t_default('acp.sched.col_task', 'Task') ?></th>
74 <th><?= t_default('acp.sched.col_enabled', 'Enabled') ?></th>
75 <th><?= t_default('acp.sched.col_interval', 'Every (hours)') ?></th>
76 <th><?= t_default('acp.sched.col_last_run', 'Last run') ?></th>
77 <th><?= t_default('acp.sched.col_last_result', 'Last result') ?></th>
78 <th class="is-num"><?= t_default('acp.sched.col_actions', 'Actions') ?></th>
79 </tr>
80 </thead>
81 <tbody>
82 <?php foreach ($tasks as $id => $task): ?>
83 <?php
84 $lastRun = scheduler_last_run($id);
85 $lastResult = scheduler_last_result($id);
86 $resultTone = 'grey';
87 if ($lastResult !== '') {
88 if (str_starts_with($lastResult, 'error')) {
89 $resultTone = 'red';
90 } elseif (str_contains($lastResult, 'issue')) {
91 $resultTone = 'amber';
92 } else {
93 $resultTone = 'green';
94 }
95 }
96 ?>
97 <tr>
98 <td>
99 <strong><?= h(t_default('acp.sched.task.' . $id, $task['label'])) ?></strong>
100 <?php if ($id === 'purge_admin_log'): ?>
101 <div class="acp-field" style="margin-top:6px;">
102 <label class="acp-label" for="sched_keep_days" style="font-weight:400;"><?= t_default('acp.sched.keep_days_label', 'Keep entries for (days)') ?></label>
103 <input class="acp-input" id="sched_keep_days" name="keep_days" type="number" min="1" style="width:100px;" value="<?= (int) setting('scheduler:purge_admin_log:keep_days', '90') ?>">
104 </div>
105 <?php endif; ?>
106 </td>
107 <td>
108 <input type="checkbox" name="enabled[<?= h($id) ?>]" value="1" <?= scheduler_enabled($id) ? 'checked' : '' ?>>
109 </td>
110 <td>
111 <input class="acp-input" type="number" min="1" style="width:90px;" name="interval_hours[<?= h($id) ?>]" value="<?= scheduler_interval_hours($id, (int) $task['default_interval_hours']) ?>">
112 </td>
113 <td class="is-muted"><?= $lastRun > 0 ? h(getClock($lastRun, true)) : t_default('acp.sched.never', 'Never') ?></td>
114 <td>
115 <?php if ($lastResult !== ''): ?>
116 <span class="acp-pill acp-pill--<?= $resultTone ?>"><?= h($lastResult) ?></span>
117 <?php else: ?>
118 <span class="is-muted">-</span>
119 <?php endif; ?>
120 </td>
121 <td class="is-nowrap is-num">
122 <button class="acp-btn acp-btn--ghost acp-btn--sm" type="submit" form="sched_run_<?= h($id) ?>">
123 <i class="fa fa-play"></i> <?= t_default('acp.sched.run_now', 'Run now') ?>
124 </button>
125 </td>
126 </tr>
127 <?php endforeach; ?>
128 </tbody>
129 </table>
130 </div>
131 <div class="acp-actions">
132 <button class="acp-btn acp-btn--green" type="submit"><i class="fa fa-check"></i> <?= t_default('acp.sched.save_btn', 'Save') ?></button>
133 </div>
134 </form>
135
136 <?php foreach ($tasks as $id => $task): ?>
137 <form method="post" id="sched_run_<?= h($id) ?>">
138 <?= acp_csrf_field() ?>
139 <input type="hidden" name="do" value="run_now">
140 <input type="hidden" name="task" value="<?= h($id) ?>">
141 </form>
142 <?php endforeach; ?>
143 </div>
144</section>
145
Top