feat(logs): structured cron run history + read endpoint
Adds unifi_cron_runs table (one row per scheduled-task execution) and
UnifiCronRun::record() wrapper that captures start/finish/status and
exceptions. The three scheduled commands now write through it:
- reboot-all-aps → rebooted/failed AP names per run
- rotate-passwords → rotated SSIDs + PPSKs, failures (when actually
rotating; the "is it due" early-return is silent
so we don't flood the log with no-op rows every
minute)
- sync-ppsk-schedules → enabled/disabled PPSKs (silent when there's
no work)
UnifiCronLogsController returns the most-recent 200 runs as JSON,
filterable by command + status. Behind permission:unifi.settings; no
super-admin required — read-only history is fine for any operator
who can see settings.
v1.5.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,61 +2,68 @@
|
||||
|
||||
namespace Dashboard\Unifi\Console;
|
||||
|
||||
use Dashboard\Unifi\Models\UnifiCronRun;
|
||||
use Dashboard\Unifi\Services\UnifiApiClient;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class RebootAllAps extends Command
|
||||
{
|
||||
protected $signature = 'unifi:reboot-all-aps {--delay=5 : Seconds to wait between each reboot}';
|
||||
protected $signature = 'unifi:reboot-all-aps {--delay=5 : Seconds to wait between each reboot} {--triggered-by=schedule}';
|
||||
protected $description = 'Planned reboot of all access points — suppresses webhook offline/online alerts';
|
||||
|
||||
public function handle(UnifiApiClient $unifi): int
|
||||
{
|
||||
try {
|
||||
$aps = $unifi->getAccessPoints();
|
||||
} catch (\Throwable $e) {
|
||||
$this->error('Failed to fetch APs: ' . $e->getMessage());
|
||||
return self::FAILURE;
|
||||
}
|
||||
$run = UnifiCronRun::record(
|
||||
'reboot-all-aps',
|
||||
$this->option('triggered-by') ?: 'schedule',
|
||||
null,
|
||||
function () use ($unifi) {
|
||||
$aps = $unifi->getAccessPoints();
|
||||
|
||||
if (empty($aps)) {
|
||||
$this->warn('No access points found.');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
if (empty($aps)) {
|
||||
$this->warn('No access points found.');
|
||||
return ['status' => 'skipped', 'reason' => 'no APs found'];
|
||||
}
|
||||
|
||||
$delay = max(0, (int) $this->option('delay'));
|
||||
$delay = max(0, (int) $this->option('delay'));
|
||||
$rebooted = [];
|
||||
$failed = [];
|
||||
|
||||
// Pre-mark all APs as planned reboots before sending any commands
|
||||
foreach ($aps as $ap) {
|
||||
$mac = strtolower($ap['mac']);
|
||||
Cache::put("unifi:planned_reboot:{$mac}", true, now()->addMinutes(20));
|
||||
$this->line("Marked planned reboot: {$ap['name']} ({$mac})");
|
||||
}
|
||||
foreach ($aps as $ap) {
|
||||
$mac = strtolower($ap['mac']);
|
||||
Cache::put("unifi:planned_reboot:{$mac}", true, now()->addMinutes(20));
|
||||
$this->line("Marked planned reboot: {$ap['name']} ({$mac})");
|
||||
}
|
||||
$this->newLine();
|
||||
|
||||
$this->newLine();
|
||||
$ok = 0;
|
||||
$fail = 0;
|
||||
foreach ($aps as $ap) {
|
||||
$mac = strtolower($ap['mac']);
|
||||
$name = $ap['name'] ?? $mac;
|
||||
try {
|
||||
$unifi->rebootDevice($mac);
|
||||
$this->info("Rebooted: {$name} ({$mac})");
|
||||
$rebooted[] = $name;
|
||||
} catch (\Throwable $e) {
|
||||
$this->error("Failed to reboot {$name}: {$e->getMessage()}");
|
||||
$failed[] = ['name' => $name, 'error' => $e->getMessage()];
|
||||
}
|
||||
|
||||
foreach ($aps as $ap) {
|
||||
$mac = strtolower($ap['mac']);
|
||||
$name = $ap['name'] ?? $mac;
|
||||
try {
|
||||
$unifi->rebootDevice($mac);
|
||||
$this->info("Rebooted: {$name} ({$mac})");
|
||||
$ok++;
|
||||
} catch (\Throwable $e) {
|
||||
$this->error("Failed to reboot {$name}: {$e->getMessage()}");
|
||||
$fail++;
|
||||
if ($delay > 0 && count($rebooted) + count($failed) < count($aps)) {
|
||||
sleep($delay);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => count($failed) === 0 ? 'succeeded' : (count($rebooted) > 0 ? 'partial' : 'failed'),
|
||||
'rebooted' => $rebooted,
|
||||
'failed' => $failed,
|
||||
'total' => count($aps),
|
||||
];
|
||||
}
|
||||
);
|
||||
|
||||
if ($delay > 0 && $ok + $fail < count($aps)) {
|
||||
sleep($delay);
|
||||
}
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->info("Done. {$ok} rebooted, {$fail} failed.");
|
||||
return $fail > 0 ? self::FAILURE : self::SUCCESS;
|
||||
$this->info("Done. Status: {$run->status}.");
|
||||
return $run->status === 'failed' ? self::FAILURE : self::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user