6 - Add admin instances page and PollAlertService failure tracking
This commit is contained in:
parent
257dbfcf5f
commit
f9cebe5bae
7 changed files with 150 additions and 10 deletions
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Enums\PageStatusEnum;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\View\View;
|
||||
use Lvl0\FediDiscover\Models\Instance;
|
||||
|
|
@ -11,7 +12,10 @@ class InstancesController extends Controller
|
|||
{
|
||||
public function index(): View
|
||||
{
|
||||
$instances = Instance::orderBy('url', 'asc')->get();
|
||||
$instances = Instance::withCount([
|
||||
'pages',
|
||||
'pages as failed_pages_count' => fn ($q) => $q->where('status', PageStatusEnum::Failed),
|
||||
])->orderBy('url', 'asc')->get();
|
||||
|
||||
return view('admin.index', ['instances' => $instances]);
|
||||
}
|
||||
|
|
|
|||
14
app/Services/PollAlertService.php
Normal file
14
app/Services/PollAlertService.php
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Lvl0\FediDiscover\Models\Instance;
|
||||
|
||||
class PollAlertService
|
||||
{
|
||||
public function recordFailure(Instance $instance): void
|
||||
{
|
||||
$instance->increment('consecutive_poll_failures');
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ public function up(): void
|
|||
$table->boolean('enabled')->default(true);
|
||||
$table->unsignedInteger('interval_seconds')->default(300);
|
||||
$table->json('extras')->default('{}');
|
||||
$table->unsignedInteger('consecutive_poll_failures')->default(0);
|
||||
$table->timestampTz('last_polled_at')->nullable();
|
||||
$table->string('last_seen_id')->nullable();
|
||||
$table->timestamps();
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@
|
|||
|
||||
namespace Lvl0\FediDiscover\Models;
|
||||
|
||||
use App\Models\Page;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Lvl0\FediDiscover\Config\InstanceType;
|
||||
use Lvl0\FediDiscover\Database\Factories\InstanceFactory;
|
||||
|
|
@ -20,6 +22,7 @@
|
|||
* @property int $interval_seconds
|
||||
* @property array<string, mixed> $extras
|
||||
* @property string|null $last_seen_id
|
||||
* @property int $consecutive_poll_failures
|
||||
* @property Carbon|null $last_polled_at
|
||||
* @property Carbon $created_at
|
||||
* @property Carbon $updated_at
|
||||
|
|
@ -31,7 +34,7 @@ class Instance extends Model
|
|||
|
||||
protected $table = 'fedi_discover_instances';
|
||||
|
||||
protected $fillable = ['type', 'url', 'enabled', 'interval_seconds', 'extras', 'last_seen_id', 'last_polled_at'];
|
||||
protected $fillable = ['type', 'url', 'enabled', 'interval_seconds', 'extras', 'last_seen_id', 'last_polled_at', 'consecutive_poll_failures'];
|
||||
|
||||
protected $casts = [
|
||||
'type' => InstanceType::class,
|
||||
|
|
@ -53,4 +56,10 @@ protected static function newFactory(): Factory
|
|||
{
|
||||
return InstanceFactory::new();
|
||||
}
|
||||
|
||||
public function pages(): HasMany
|
||||
{
|
||||
return $this->hasMany(Page::class);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@
|
|||
<tr>
|
||||
<th>Instance</th>
|
||||
<th>Last polled at</th>
|
||||
<th>URLs</th>
|
||||
<th>Errors</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -14,6 +16,8 @@
|
|||
<tr>
|
||||
<td>{{ $instance->url }}</td>
|
||||
<td>{{ $instance->last_polled_at }}</td>
|
||||
<td>{{ $instance->pages_count }} URLs</td>
|
||||
<td>{{ $instance->failed_pages_count }} errors</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
namespace Tests\Feature\Admin;
|
||||
|
||||
use App\Models\Page;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Lvl0\FediDiscover\Config\InstanceType;
|
||||
use Lvl0\FediDiscover\Models\Instance;
|
||||
|
|
@ -13,10 +14,6 @@ class InstancesAdminPageTest extends TestCase
|
|||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Test 3 — admin instances page is accessible
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function test_admin_instances_page_is_accessible(): void
|
||||
{
|
||||
$response = $this->get('/admin/instances');
|
||||
|
|
@ -24,10 +21,6 @@ public function test_admin_instances_page_is_accessible(): void
|
|||
$response->assertStatus(200);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Test 4 — admin instances page lists each instance's URL and last_polled_at
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function test_admin_instances_page_shows_each_instance_url_and_last_polled_at(): void
|
||||
{
|
||||
$mastodon = Instance::factory()
|
||||
|
|
@ -53,4 +46,87 @@ public function test_admin_instances_page_shows_each_instance_url_and_last_polle
|
|||
$response->assertSee($mastodon->last_polled_at->toDateString());
|
||||
$response->assertSee($lemmy->last_polled_at->toDateString());
|
||||
}
|
||||
|
||||
public function test_admin_instances_page_shows_error_count_per_instance(): void
|
||||
{
|
||||
$first = Instance::factory()
|
||||
->type(InstanceType::Mastodon)
|
||||
->enabled()
|
||||
->create(['url' => 'https://aardvark.example']);
|
||||
|
||||
$second = Instance::factory()
|
||||
->type(InstanceType::Lemmy)
|
||||
->enabled()
|
||||
->create(['url' => 'https://zebra.example']);
|
||||
|
||||
// First instance: 3 failed + 2 non-failed pages
|
||||
Page::factory()
|
||||
->count(3)
|
||||
->sequence(fn ($s) => ['url' => "https://aardvark.example/fail-{$s->index}"])
|
||||
->createQuietly(['instance_id' => $first->id, 'status' => \App\Enums\PageStatusEnum::Failed]);
|
||||
|
||||
Page::factory()
|
||||
->count(2)
|
||||
->sequence(fn ($s) => ['url' => "https://aardvark.example/ok-{$s->index}"])
|
||||
->createQuietly(['instance_id' => $first->id, 'status' => \App\Enums\PageStatusEnum::Fetched]);
|
||||
|
||||
// Second instance: 1 failed + 4 non-failed pages
|
||||
Page::factory()
|
||||
->count(1)
|
||||
->sequence(fn ($s) => ['url' => "https://zebra.example/fail-{$s->index}"])
|
||||
->createQuietly(['instance_id' => $second->id, 'status' => \App\Enums\PageStatusEnum::Failed]);
|
||||
|
||||
Page::factory()
|
||||
->count(4)
|
||||
->sequence(fn ($s) => ['url' => "https://zebra.example/ok-{$s->index}"])
|
||||
->createQuietly(['instance_id' => $second->id, 'status' => \App\Enums\PageStatusEnum::Fetched]);
|
||||
|
||||
$response = $this->get('/admin/instances');
|
||||
|
||||
// Each error-count cell must render as "{n} errors" — this string cannot
|
||||
// collide with dates, IDs, or the "URLs" column. The counts (3 and 1)
|
||||
// are distinct and non-equal so the assertion proves per-row mapping,
|
||||
// not a leaked total.
|
||||
$response->assertSeeInOrder([
|
||||
$first->url,
|
||||
'3 errors',
|
||||
$second->url,
|
||||
'1 errors',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_admin_instances_page_shows_url_count_per_instance(): void
|
||||
{
|
||||
$first = Instance::factory()
|
||||
->type(InstanceType::Mastodon)
|
||||
->enabled()
|
||||
->create(['url' => 'https://aardvark.example']);
|
||||
|
||||
$second = Instance::factory()
|
||||
->type(InstanceType::Lemmy)
|
||||
->enabled()
|
||||
->create(['url' => 'https://zebra.example']);
|
||||
|
||||
Page::factory()
|
||||
->count(7)
|
||||
->sequence(fn ($s) => ['url' => "https://aardvark.example/page-{$s->index}"])
|
||||
->createQuietly(['instance_id' => $first->id]);
|
||||
|
||||
Page::factory()
|
||||
->count(2)
|
||||
->sequence(fn ($s) => ['url' => "https://zebra.example/page-{$s->index}"])
|
||||
->createQuietly(['instance_id' => $second->id]);
|
||||
|
||||
$response = $this->get('/admin/instances');
|
||||
|
||||
// Each count cell must render as "{n} URLs" — this string cannot
|
||||
// collide with dates, IDs, or any other incidental numeric content,
|
||||
// so the assertion only passes when a real count column is wired in.
|
||||
$response->assertSeeInOrder([
|
||||
$first->url,
|
||||
'7 URLs',
|
||||
$second->url,
|
||||
'2 URLs',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
32
tests/Feature/Services/PollAlertServiceTest.php
Normal file
32
tests/Feature/Services/PollAlertServiceTest.php
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Services;
|
||||
|
||||
use App\Services\PollAlertService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Lvl0\FediDiscover\Config\InstanceType;
|
||||
use Lvl0\FediDiscover\Models\Instance;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PollAlertServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_recordFailure_increments_consecutive_poll_failures_on_the_instance(): void
|
||||
{
|
||||
$instance = Instance::factory()
|
||||
->type(InstanceType::Mastodon)
|
||||
->enabled()
|
||||
->create(['consecutive_poll_failures' => 0]);
|
||||
|
||||
$service = new PollAlertService();
|
||||
$service->recordFailure($instance);
|
||||
|
||||
$this->assertDatabaseHas('fedi_discover_instances', [
|
||||
'id' => $instance->id,
|
||||
'consecutive_poll_failures' => 1,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue