feat: HealthCheckable interface on DaemonRpc and WalletRpc

Services implement healthCheck() returning {status, detail, stale?}.
Status page refactored to use healthCheck() instead of ad-hoc checks.
Statuses: healthy, degraded (stale data), unhealthy (unreachable).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude 2026-04-04 11:43:01 +01:00
parent 924e8e223f
commit b4e4766e01
No known key found for this signature in database
GPG key ID: AF404715446AEB41
4 changed files with 65 additions and 16 deletions

View file

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Mod\Chain\Contracts;
/**
* Service health check interface.
*
* $result = $service->healthCheck();
* if ($result['status'] === 'healthy') { ... }
*/
interface HealthCheckable
{
/**
* @return array{status: string, detail: string, stale?: bool}
*/
public function healthCheck(): array;
}

View file

@ -14,7 +14,7 @@ use Illuminate\Support\Facades\Http;
* $info = $rpc->getInfo();
* $block = $rpc->getBlockByHeight(12345);
*/
class DaemonRpc implements \Mod\Chain\Contracts\ChainDaemon
class DaemonRpc implements \Mod\Chain\Contracts\ChainDaemon, \Mod\Chain\Contracts\HealthCheckable
{
private string $endpoint;
private int $cacheTtl;
@ -154,4 +154,26 @@ class DaemonRpc implements \Mod\Chain\Contracts\ChainDaemon
return ['error' => 'Daemon unreachable', '_offline' => true];
}
}
public function healthCheck(): array
{
$info = $this->getInfo();
if (isset($info['_offline'])) {
return ['status' => 'unhealthy', 'detail' => 'Unreachable'];
}
if (isset($info['_stale'])) {
return [
'status' => 'degraded',
'detail' => 'Stale data — height ' . number_format($info['height'] ?? 0),
'stale' => true,
];
}
return [
'status' => 'healthy',
'detail' => 'Height ' . number_format($info['height'] ?? 0) . ' — ' . ($info['status'] ?? 'OK'),
];
}
}

View file

@ -10,7 +10,7 @@ use Illuminate\Support\Facades\Http;
* $wallet = app(WalletRpc::class);
* $wallet->registerAlias('myname', $address, 'v=lthn1;type=user');
*/
class WalletRpc implements \Mod\Chain\Contracts\ChainWallet
class WalletRpc implements \Mod\Chain\Contracts\ChainWallet, \Mod\Chain\Contracts\HealthCheckable
{
private string $endpoint;
@ -99,4 +99,19 @@ class WalletRpc implements \Mod\Chain\Contracts\ChainWallet
return $data['result'] ?? $data['error'] ?? [];
}
public function healthCheck(): array
{
try {
$balance = $this->getBalance();
if (isset($balance['error'])) {
return ['status' => 'unhealthy', 'detail' => 'Unreachable'];
}
return ['status' => 'healthy', 'detail' => 'Connected'];
} catch (\Throwable $e) {
return ['status' => 'unhealthy', 'detail' => 'Unreachable'];
}
}
}

View file

@ -159,29 +159,22 @@ class HomeController extends Controller
public function status(): \Illuminate\View\View
{
$chainInfo = $this->rpc->getInfo();
try {
$walletInfo = $this->wallet->getBalance();
} catch (\Throwable $e) {
$walletInfo = ['error' => 'Wallet unreachable'];
}
$chainHealth = $this->rpc->healthCheck();
$walletHealth = $this->wallet->healthCheck();
$liveGateways = $this->registry->liveGateways();
$pendingClaims = \Mod\Names\Models\NameClaim::pending()->count();
$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'),
'ok' => $chainHealth['status'] !== 'unhealthy',
'stale' => $chainHealth['stale'] ?? false,
'detail' => $chainHealth['detail'],
],
'wallet' => [
'label' => 'Registry Wallet',
'ok' => ! isset($walletInfo['error']),
'detail' => isset($walletInfo['error'])
? 'Unreachable'
: 'Connected',
'ok' => $walletHealth['status'] === 'healthy',
'detail' => $walletHealth['detail'],
],
'gateways' => [
'label' => 'Paired Gateways',