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

154
app/Models/MissingUrl.php Normal file
View File

@@ -0,0 +1,154 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Enums\MissingUrlStatus;
use Database\Factories\MissingUrlFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
/**
* An address the frontend was asked for and could not resolve.
*
* Nobody creates one of these by hand: they arrive from the frontend, which is
* the only place that finds out. What the panel does with them is decide which
* ones deserve a redirect.
*/
#[Fillable(['path', 'locale', 'referrer', 'status'])]
class MissingUrl extends Model
{
/** @use HasFactory<MissingUrlFactory> */
use HasFactory;
protected $attributes = [
'status' => MissingUrlStatus::Pending->value,
];
protected function casts(): array
{
return [
'status' => MissingUrlStatus::class,
'hits' => 'integer',
'first_seen_at' => 'datetime',
'last_seen_at' => 'datetime',
'resolved_at' => 'datetime',
];
}
/**
* Record one sighting of a path.
*
* The count is an upsert rather than a read-then-write because these
* arrive in bursts — a crawler walking a dead section produces dozens in a
* second — and two of them racing on the same path would otherwise violate
* the unique index or lose a count. What an upsert cannot express portably
* follows it as two narrow updates on the same indexed row.
*
* A *resolved* row comes back to `pending` when it is seen again: the
* status was a claim that the address had a destination, and a fresh 404
* disproves it — a redirect written and then deleted is exactly this.
*
* An *ignored* row does not. That status is a decision about attention
* rather than a claim about the world, and the addresses it is used on are
* the ones being requested constantly. Reviving them on the next sighting
* would mean the button never worked twice.
*/
public static function report(string $path, string $locale, ?string $referrer = null): void
{
$now = now();
static::query()->upsert(
[[
'path' => $path,
'locale' => $locale,
'referrer' => $referrer,
'status' => MissingUrlStatus::Pending->value,
'hits' => 1,
'first_seen_at' => $now,
'last_seen_at' => $now,
'resolved_at' => null,
'created_at' => $now,
'updated_at' => $now,
]],
['path', 'locale'],
[
'hits' => DB::raw('missing_urls.hits + 1'),
'last_seen_at' => $now,
'updated_at' => $now,
],
);
// Kept out of the upsert because it must apply to one status and not
// the other, which no portable upsert can express.
static::query()
->where('path', $path)
->where('locale', $locale)
->where('status', MissingUrlStatus::Resolved)
->update([
'status' => MissingUrlStatus::Pending,
'resolved_at' => null,
]);
// Kept out of the upsert on purpose. Overwriting unconditionally would
// erase the referrer of a known bad link the moment the same address
// is next requested without one — which is what a bot does. So the
// newest referrer wins, but only against another referrer.
if (filled($referrer)) {
static::query()
->where('path', $path)
->where('locale', $locale)
->update(['referrer' => $referrer]);
}
}
/**
* Mark whatever this redirect now answers as dealt with.
*
* Called when a redirect is written, from either end — the panel's action
* on this list, a rule typed by hand, or a slug rename — so the list never
* shows an address that has already been given a destination.
*/
public static function resolvedBy(Redirect $redirect): void
{
static::query()
->where('path', $redirect->from)
->where('status', '!=', MissingUrlStatus::Resolved)
// A redirect with no locale applies in every language, so it
// answers the sightings of that path in all of them.
->when($redirect->locale !== null, fn (Builder $query) => $query
->where('locale', $redirect->locale))
->update([
'status' => MissingUrlStatus::Resolved,
'resolved_at' => now(),
]);
}
/**
* Whether the link that produced this came from our own site.
*
* The distinction that decides how urgent it is: an external referrer is
* somebody else's stale link, and a redirect is a courtesy. Our own domain
* means a page we publish is pointing at an address that does not exist.
*/
public function isInternalReferrer(): bool
{
$site = (string) config('site.url');
return filled($this->referrer) && filled($site) && Str::startsWith($this->referrer, $site);
}
/**
* @param Builder<$this> $query
* @return Builder<$this>
*/
public function scopePending(Builder $query): Builder
{
return $query->where('status', MissingUrlStatus::Pending);
}
}