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

73
app/Support/Presence.php Normal file
View File

@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace App\Support;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
/**
* Who else has a record's edit screen open right now.
*
* Advisory, not authoritative — there is no signal at all for "closed the
* tab", so presence really means "opened this within the last few minutes",
* which fades out on its own once nobody touches the screen again. That
* heuristic is the right shape for the problem: knowing that two people are
* mid-edit matters; an exact headcount does not.
*/
final class Presence
{
/**
* How long somebody stays "here" after they were last seen. Long enough
* to survive reading a long field without typing; short enough that a
* closed tab does not haunt a record for the rest of the day.
*/
protected const TTL_MINUTES = 10;
/**
* Record this user as here now, and answer who else already was.
*
* Reading and writing happen in one call because that is the one moment
* presence is ever needed: opening the screen. There is no separate
* "who is here" query anywhere else, so there is nothing for the two to
* disagree about.
*
* @return Collection<int, User>
*/
public static function touch(Model $record, User $user): Collection
{
$key = self::key($record);
$cutoff = now()->subMinutes(self::TTL_MINUTES)->timestamp;
$present = collect(self::entries($key))
->filter(fn (int $seenAt) => $seenAt >= $cutoff);
// Answered before this visit is recorded: nobody is ever told they
// are keeping themselves company.
$others = $present->keys()->reject(fn (int $id) => $id === $user->getKey());
$present[$user->getKey()] = now()->timestamp;
Cache::put($key, $present->all(), now()->addMinutes(self::TTL_MINUTES));
return $others->isEmpty()
? collect()
: User::query()->whereKey($others)->get();
}
protected static function key(Model $record): string
{
return 'presence:'.$record->getMorphClass().':'.$record->getKey();
}
/**
* @return array<int, int>
*/
protected static function entries(string $key): array
{
return (array) Cache::get($key, []);
}
}