Report site health and pending translations through the API

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 08:32:45 +02:00
parent 5b46436641
commit 68c5bd4274
5 changed files with 316 additions and 0 deletions

View File

@@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Models\Call;
use App\Models\Page;
use App\Models\Project;
use App\Models\Service;
use App\Support\AdminContent;
use App\Support\ContentTypes;
use App\Support\Health\Finding;
use App\Support\Health\HealthReport;
use App\Support\TranslationFreshness;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
/**
* What is wrong with the site, and what is left to translate — the panel's
* health screen, for a client that cannot open it.
*/
class HealthController extends Controller
{
/**
* The kinds of content written in several languages and checked for it.
*
* @var array<int, class-string<Model>>
*/
protected const TRANSLATED = [Page::class, Project::class, Service::class, Call::class];
/**
* Every check the caller may see, problems first. A check behind a
* permission they lack is left out, as on the panel.
*/
public function index(Request $request, HealthReport $report): JsonResponse
{
// Who may open the panel's screen; each check guards itself further.
$this->authorize('panel.access');
$findings = $report->runFor($request->user());
return response()->json([
'data' => $findings->map(fn (Finding $finding) => [
'key' => $finding->key,
'severity' => $finding->severity->value,
'count' => $finding->count,
'title' => $finding->title(),
'description' => $finding->description(),
'samples' => $finding->samples,
'panel_url' => $finding->url,
])->values(),
'meta' => [
'problems' => $findings->filter(fn (Finding $f) => $f->severity->isProblem())->count(),
'checked_at' => now()->toIso8601String(),
],
]);
}
/**
* Records with a language still to write (`missing`) or left behind by the
* one it was translated from (`stale`, with since when).
*
* Filters: locale (only what that language lacks), type, published (only
* what is on the site — open, for calls).
*/
public function translations(Request $request): JsonResponse
{
$locales = array_keys((array) config('locales.supported'));
$request->validate([
'locale' => ['sometimes', Rule::in($locales)],
'type' => ['sometimes', Rule::in(['pages', 'projects', 'services', 'calls'])],
'published' => ['sometimes', 'boolean'],
]);
$wanted = $request->input('locale');
$items = collect();
foreach (self::TRANSLATED as $model) {
$type = AdminContent::nameOf(new $model);
if ($request->filled('type') && $request->input('type') !== $type) {
continue;
}
if ($request->user()->cannot('viewAny', $model)) {
continue;
}
$model::query()
->when($request->boolean('published'), fn ($q) => $model === Call::class ? $q->open() : $q->published())
->with('revisions')
->get()
->each(function (Model $record) use ($locales, $wanted, $type, $items): void {
$missing = array_values(array_filter(
$locales,
fn (string $locale) => blank($record->getTranslation('title', $locale, false)),
));
$stale = collect(TranslationFreshness::for($record))
->map(fn ($since) => $since->toIso8601String())
->all();
if ($wanted !== null) {
$missing = array_values(array_intersect($missing, [$wanted]));
$stale = array_intersect_key($stale, [$wanted => true]);
}
if ($missing === [] && $stale === []) {
return;
}
$items->push([
'type' => $type,
'id' => $record->getKey(),
'title' => ContentTypes::title($record),
'missing' => $missing,
'stale' => (object) $stale,
]);
});
}
return response()->json(['data' => $items->values()]);
}
}