feat: /status page with live system health checks

Shows blockchain daemon, wallet, gateways, name registry, and consensus
status with green/amber indicators. Chain stats, hardfork status, and
gateway details. Footer link added.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude 2026-04-04 08:33:14 +01:00
parent 1f31444171
commit 774e9be207
No known key found for this signature in database
GPG key ID: AF404715446AEB41
4 changed files with 164 additions and 0 deletions

View file

@ -5,7 +5,9 @@ declare(strict_types=1);
namespace Website\Lethean\Controllers;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Cache;
use Mod\Chain\Services\DaemonRpc;
use Mod\Chain\Services\WalletRpc;
use Mod\Gateway\Services\GatewayRegistry;
/**
@ -18,6 +20,7 @@ class HomeController extends Controller
public function __construct(
private readonly DaemonRpc $rpc,
private readonly GatewayRegistry $registry,
private readonly WalletRpc $wallet,
) {}
public function index(): \Illuminate\View\View
@ -153,6 +156,77 @@ class HomeController extends Controller
return view('lethean::services-seo', ['nodes' => array_values($nodes)]);
}
public function status(): \Illuminate\View\View
{
$chainInfo = $this->rpc->getInfo();
$walletInfo = $this->wallet->getBalance();
$liveGateways = $this->registry->liveGateways();
$claims = Cache::get('name_claims', []);
$pendingClaims = count(array_filter($claims, fn ($c) => $c['status'] === 'pending'));
$checks = [
'chain' => [
'label' => 'Blockchain Daemon',
'ok' => ! isset($chainInfo['_offline']),
'stale' => isset($chainInfo['_stale']),
'detail' => isset($chainInfo['_offline'])
? 'Unreachable'
: 'Height ' . number_format($chainInfo['height'] ?? 0) . ' — ' . ($chainInfo['status'] ?? 'unknown'),
],
'wallet' => [
'label' => 'Registry Wallet',
'ok' => ! isset($walletInfo['error']),
'detail' => isset($walletInfo['error'])
? 'Unreachable'
: 'Connected',
],
'gateways' => [
'label' => 'Paired Gateways',
'ok' => true,
'detail' => count($liveGateways) . ' live',
],
'names' => [
'label' => 'Name Registry',
'ok' => ! isset($chainInfo['_offline']),
'detail' => ($chainInfo['alias_count'] ?? 0) . ' registered, ' . $pendingClaims . ' claims pending',
],
'consensus' => [
'label' => 'Consensus',
'ok' => ! isset($chainInfo['_offline']),
'detail' => isset($chainInfo['_offline'])
? 'Unknown'
: (($chainInfo['pos_allowed'] ?? false) ? 'PoW + PoS (hybrid)' : 'PoW only'),
],
];
$allOk = ! in_array(false, array_column($checks, 'ok'));
return view('lethean::status', [
'checks' => $checks,
'allOk' => $allOk,
'info' => $chainInfo,
'liveGateways' => $liveGateways,
'uptime' => $this->uptimeString(),
]);
}
private function uptimeString(): string
{
$started = Cache::get('app.started_at');
if (! $started) {
Cache::put('app.started_at', now()->toIso8601String(), 86400 * 365);
return 'Just started';
}
$diff = now()->diff(\Carbon\Carbon::parse($started));
$parts = [];
if ($diff->d > 0) $parts[] = $diff->d . 'd';
if ($diff->h > 0) $parts[] = $diff->h . 'h';
$parts[] = $diff->i . 'm';
return implode(' ', $parts);
}
public function services(): \Illuminate\View\View
{
$result = $this->rpc->getAllAliases();

View file

@ -11,6 +11,7 @@ Route::get('/network', [HomeController::class, 'network'])->name('network');
Route::get('/sunrise', [HomeController::class, 'sunrise'])->name('sunrise');
Route::get('/pricing', [HomeController::class, 'pricing'])->name('pricing');
Route::get('/docs', [HomeController::class, 'apiDocs'])->name('api');
Route::get('/status', [HomeController::class, 'status'])->name('status');
Route::get('/services', [HomeController::class, 'servicesLanding'])->name('services');
Route::get('/services/ssl', [HomeController::class, 'sslCertificates'])->name('services.ssl');
Route::get('/services/dns-hosting', [HomeController::class, 'dnsHosting'])->name('services.dns');

View file

@ -127,6 +127,7 @@
<a href="/docs">API</a>
<a href="/sunrise">Sunrise</a>
<a href="https://order.lthn.ai">Order</a>
<a href="/status">Status</a>
<a href="https://forge.lthn.ai">Source</a>
<a href="mailto:developers@lethean.io">Contact</a>
</div>

View file

@ -0,0 +1,88 @@
@extends('lethean::layout')
@section('title', 'System Status')
@section('meta_description', 'Live system status for the Lethean .lthn TLD registrar — blockchain, wallet, gateways, DNS.')
@section('content')
<div class="section" style="max-width: 800px; margin: 0 auto;">
<div style="text-align: center; margin-bottom: 2rem;">
<h2>System Status</h2>
<div style="margin-top: 1rem;">
@if($allOk)
<span style="display: inline-block; padding: 0.5rem 1.5rem; background: rgba(52, 211, 153, 0.15); border: 1px solid rgba(52, 211, 153, 0.3); border-radius: 2rem; color: #34d399; font-weight: 600;">All Systems Operational</span>
@else
<span style="display: inline-block; padding: 0.5rem 1.5rem; background: rgba(251, 191, 36, 0.15); border: 1px solid rgba(251, 191, 36, 0.3); border-radius: 2rem; color: #fbbf24; font-weight: 600;">Degraded Performance</span>
@endif
</div>
<p style="color: var(--muted); font-size: 0.85rem; margin-top: 0.75rem;">Uptime: {{ $uptime }} &middot; Last checked: now</p>
</div>
<div class="card" style="padding: 0; overflow: hidden;">
@foreach($checks as $key => $check)
<div style="display: flex; align-items: center; justify-content: space-between; padding: 1rem 1.25rem; border-bottom: 1px solid var(--border);">
<div style="display: flex; align-items: center; gap: 0.75rem;">
<span style="display: inline-block; width: 10px; height: 10px; border-radius: 50%; background: {{ $check['ok'] ? '#34d399' : '#fbbf24' }};{{ $check['ok'] ? '' : ' animation: pulse 2s infinite;' }}"></span>
<span style="font-weight: 500;">{{ $check['label'] }}</span>
</div>
<div style="display: flex; align-items: center; gap: 0.5rem;">
@if(isset($check['stale']) && $check['stale'])
<span style="font-size: 0.75rem; padding: 0.15rem 0.5rem; background: rgba(251, 191, 36, 0.15); border-radius: 0.25rem; color: #fbbf24;">stale</span>
@endif
<span style="color: var(--muted); font-size: 0.85rem;">{{ $check['detail'] }}</span>
</div>
</div>
@endforeach
</div>
<div class="card-grid" style="margin-top: 2rem;">
<div class="card">
<h3>Chain</h3>
<div style="color: var(--muted); font-size: 0.85rem; line-height: 2;">
<div style="display: flex; justify-content: space-between;"><span>Block Height</span><span style="color: var(--text);">{{ number_format($info['height'] ?? 0) }}</span></div>
<div style="display: flex; justify-content: space-between;"><span>PoW Difficulty</span><span style="color: var(--text);">{{ number_format($info['pow_difficulty'] ?? 0) }}</span></div>
<div style="display: flex; justify-content: space-between;"><span>PoS</span><span style="color: var(--text);">{{ ($info['pos_allowed'] ?? false) ? 'Active' : 'Inactive' }}</span></div>
<div style="display: flex; justify-content: space-between;"><span>TX Pool</span><span style="color: var(--text);">{{ $info['tx_pool_size'] ?? 0 }} pending</span></div>
<div style="display: flex; justify-content: space-between;"><span>Total TXs</span><span style="color: var(--text);">{{ number_format($info['tx_count'] ?? 0) }}</span></div>
</div>
</div>
<div class="card">
<h3>Gateways</h3>
<div style="color: var(--muted); font-size: 0.85rem; line-height: 2;">
<div style="display: flex; justify-content: space-between;"><span>Paired</span><span style="color: var(--text);">{{ count($liveGateways) }}</span></div>
@foreach($liveGateways as $gw)
<div style="display: flex; justify-content: space-between;">
<span>{{ $gw['name'] }}</span>
<span style="color: #34d399;">{{ $gw['region'] ?? 'unknown' }} &middot; {{ $gw['current_load'] ?? 0 }}% load</span>
</div>
@endforeach
@if(empty($liveGateways))
<div style="color: var(--muted);">No gateways currently paired</div>
@endif
</div>
</div>
</div>
<div class="card" style="margin-top: 1rem;">
<h3>Hardforks</h3>
<div style="display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.5rem;">
@foreach(($info['is_hardfok_active'] ?? []) as $i => $active)
<span class="badge {{ $active ? 'badge-green' : 'badge-amber' }}">HF{{ $i + 1 }}: {{ $active ? 'Active' : 'Pending' }}</span>
@endforeach
</div>
</div>
<div style="text-align: center; margin-top: 2rem;">
<p style="color: var(--muted); font-size: 0.8rem;">
API health: <a href="/v1/names/health" style="color: var(--link);">/v1/names/health</a>
&middot; Chain data: <a href="/v1/chain/info" style="color: var(--link);">/v1/chain/info</a>
</p>
</div>
</div>
<style>
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
</style>
@endsection