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:
parent
1f31444171
commit
774e9be207
4 changed files with 164 additions and 0 deletions
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
88
app/Website/Lethean/Views/status.blade.php
Normal file
88
app/Website/Lethean/Views/status.blade.php
Normal 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 }} · 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' }} · {{ $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>
|
||||
· 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
|
||||
Loading…
Add table
Reference in a new issue