@extends('layouts.admin')
@use('App\Models\UpdateLog')
@use('App\Models\UpdateVersion')
@use('App\Repositories\VersionRepository')
@section('title', 'Versions')
@section('heading', 'CMS Versions & Updates')
@section('subheading', 'Manage releases, update packages and deployment channels.')
@php
$canManage = auth()->user()?->isOwner() ?? false;
// allForAdmin() counts logs but does not load the author, which the drawer
// shows. One query here beats one per release.
$versions->loadMissing('creator');
/*
| Update activity, in ONE grouped query, keyed by version then status.
|
| withCount('logs') on the collection already counts EVERY log row, which is
| mostly routine update checks — using that as "downloads" would overstate
| uptake by an order of magnitude. This breakdown is what lets the page say
| downloaded, installed and failed separately, and mean them.
*/
$activity = UpdateLog::query()
->selectRaw('update_version_id, status, COUNT(*) as total')
->whereNotNull('update_version_id')
->groupBy('update_version_id', 'status')
->get()
->groupBy('update_version_id')
->map(fn ($rows) => $rows->pluck('total', 'status'));
$countsFor = fn ($version) => $activity->get($version->id, collect());
// The release the update API would actually serve a stable CMS install —
// the same call the API makes, so the "Latest" badge cannot disagree with it.
$latest = app(VersionRepository::class)->latestForChannel(UpdateVersion::CHANNEL_STABLE, UpdateVersion::PRODUCT_CMS);
$latestId = $latest?->id;
$maxDownloads = (int) $versions->max(fn ($v) => (int) ($countsFor($v)['downloaded'] ?? 0));
// The single model the drawer, the filters, the sort and the CSV export all
// read from, so none of them has to scrape the table out of the DOM.
$rows = $versions->map(function ($version) use ($countsFor, $canManage) {
$counts = $countsFor($version);
return [
'id' => $version->id,
'version' => $version->version,
'name' => $version->release_name,
'product' => $version->product ?: UpdateVersion::PRODUCT_CMS,
'channel' => $version->channel,
'status' => $version->status,
'type' => $version->package_type,
'notes' => $version->release_notes,
'minSupported' => $version->min_supported_version,
'security' => (bool) $version->security_update,
'force' => (bool) $version->force_update,
'hasPackage' => $version->hasPackage(),
'signed' => filled($version->signature),
'fileName' => $version->hasPackage() ? $version->downloadName() : null,
'size' => (int) $version->file_size,
'checksum' => $version->checksum_sha256,
'author' => $version->creator?->name,
'publishedAt' => $version->published_at?->toIso8601String(),
'checks' => (int) ($counts['checked'] ?? 0),
'downloads' => (int) ($counts['downloaded'] ?? 0),
'installed' => (int) ($counts['installed'] ?? 0),
'failed' => (int) ($counts['failed'] ?? 0),
'showUrl' => route('versions.show', $version),
'editUrl' => $canManage ? route('versions.edit', $version) : null,
// Only the moments this record actually stores. No invented steps.
'timeline' => [
['label' => 'Created', 'at' => $version->created_at?->format('d M Y H:i'), 'done' => (bool) $version->created_at],
['label' => 'Release date', 'at' => $version->release_date?->format('d M Y'), 'done' => (bool) $version->release_date],
['label' => 'Package attached', 'at' => $version->hasPackage() ? $version->updated_at?->format('d M Y H:i') : null, 'done' => $version->hasPackage()],
['label' => 'Signed', 'at' => filled($version->signature) ? 'Signature attached' : null, 'done' => filled($version->signature)],
['label' => 'Published', 'at' => $version->published_at?->format('d M Y H:i'), 'done' => (bool) $version->published_at],
],
];
})->values();
$blank = new UpdateVersion([
'product' => UpdateVersion::PRODUCT_CMS,
'channel' => UpdateVersion::CHANNEL_STABLE,
'package_type' => 'full',
]);
@endphp
@section('header-actions')
@if ($canManage)
{{-- The layout renders this outside the page component, so the click
dispatches an event that bubbles to window instead of calling the
method directly. Still a real link with JavaScript off. --}}
| {{-- indeterminate exists only as a DOM property, so it has to be assigned, not bound. --}} | Version | Release Notes | Channel | Package | Flags | Status | Published | Downloads | Actions |
|---|
No releases match these filters
Nothing in the catalogue fits. Widen the search, or clear the filters to see all {{ $versions->count() }}.