Initial commit: Pireon Admin — headless admin panel and public API

Laravel 13 + Filament 5 admin panel and REST API for a non-profit developer
collective, backed by the Pireon OAuth identity server. The public site is a
separate, server-rendered application consuming /api/public/v1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 17:55:27 +02:00
commit 3897113313
534 changed files with 64050 additions and 0 deletions

View File

@@ -0,0 +1,142 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api\PublicApi\V1;
use App\Http\Requests\PublicApi\ReportMissingUrlRequest;
use App\Http\Requests\PublicApi\ReportViewsRequest;
use App\Models\ContentView;
use App\Models\MissingUrl;
use App\Models\Redirect;
use App\Support\ContentTypes;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
/**
* The frontend reporting back.
*
* Everything else this API does is answer questions. These two endpoints are
* the direction nothing else travels in: the site telling the panel what
* happened out there, which is the half of every editorial decision that has
* been missing.
*
* Two things are worth knowing and neither can be discovered from here: which
* addresses failed, because a broken link only ever fails in front of a
* visitor, and which records were read, because the panel can say what is
* published and never whether anybody opened it.
*
* What is deliberately not collected in either: anything about a person. No
* address, no session, no user agent, no time of day. A failed URL and a daily
* tally per record answer the questions; a visitor log would answer different
* ones nobody here asked.
*/
class TelemetryController extends PublicController
{
/**
* POST /api/public/v1/telemetry/not-found
*
* Answers 202 rather than 201: nothing here is a resource the caller owns,
* and reports that were merged into an existing count or dropped as noise
* are still perfectly successful. The count of what was actually recorded
* comes back so the frontend can tell whether its filter matches ours.
*/
public function notFound(ReportMissingUrlRequest $request): JsonResponse
{
$recorded = 0;
foreach ($request->hits() as $hit) {
// Normalised exactly as a redirect is, because that is what these
// are eventually compared against: "/x/" and "/x?ref=1" are the
// same broken address and must not become two rows.
$path = Redirect::normalise((string) $hit['path']);
if ($this->isNoise($path)) {
continue;
}
MissingUrl::report(
$path,
// The frontend usually knows which language it was rendering.
// When it does not, the negotiated locale is the same answer:
// it comes from the Accept-Language the frontend forwarded.
filled($hit['locale'] ?? null) ? (string) $hit['locale'] : app()->getLocale(),
filled($hit['referrer'] ?? null) ? (string) $hit['referrer'] : null,
);
$recorded++;
}
return response()->json([
'data' => [
'received' => count($request->hits()),
'recorded' => $recorded,
],
], Response::HTTP_ACCEPTED)
->header('Cache-Control', 'no-store, private');
}
/**
* POST /api/public/v1/telemetry/views
*
* What was read. The panel has always known what exists and what is
* published, and never whether any of it gets opened — which is the fact
* behind most editorial decisions: what to translate next, whether the
* featured project is one anybody looks at, whether a page earns the work
* of keeping it accurate.
*
* There is no visitor in any of this. A report says a record was rendered
* in a language; what is stored is a daily total per record. No address,
* no session, no user agent, no time of day — nothing that could be
* assembled back into a person.
*/
public function views(ReportViewsRequest $request): JsonResponse
{
$recorded = 0;
foreach ($request->views() as $view) {
$locale = filled($view['locale'] ?? null)
? (string) $view['locale']
: app()->getLocale();
$record = ContentTypes::resolve((string) $view['type'], (string) $view['slug'], $locale);
// A slug that no longer resolves is a 404, and the endpoint next
// door exists to hear about those. Counting it here would attach
// somebody's reading to nothing.
if ($record === null) {
continue;
}
ContentView::record($record, $locale, (int) ($view['count'] ?? 1));
$recorded++;
}
return response()->json([
'data' => [
'received' => count($request->views()),
'recorded' => $recorded,
],
], Response::HTTP_ACCEPTED)
->header('Cache-Control', 'no-store, private');
}
/**
* A scan for something that was never here.
*
* Dropped rather than stored, and dropped on this side rather than left to
* the frontend: the list is only useful while a person is willing to read
* it, and one afternoon of a bot looking for WordPress would end that.
*/
protected function isNoise(string $path): bool
{
foreach ((array) config('site.telemetry.ignore', []) as $pattern) {
if (fnmatch((string) $pattern, $path, FNM_CASEFOLD)) {
return true;
}
}
return false;
}
}