lthn.io/app/Website/Lethean/Controllers/HomeController.php
Claude b4e4766e01
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>
2026-04-04 11:43:01 +01:00

252 lines
8.1 KiB
PHP

<?php
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;
/**
* lthn.io homepage.
*
* GET / — TLD landing with live chain stats
*/
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
{
$info = $this->rpc->getInfo();
$aliases = $this->rpc->getAllAliases();
return view('lethean::home', [
'height' => number_format($info['height'] ?? 0),
'aliases' => count($aliases['aliases'] ?? []),
'transactions' => number_format($info['tx_count'] ?? 0),
'difficulty' => [
'pow' => number_format($info['pow_difficulty'] ?? 0),
'pos' => $info['pos_difficulty'] ?? '0',
],
]);
}
public function apiDocs(): \Illuminate\View\View
{
return view('lethean::api');
}
public function pricing(): \Illuminate\View\View
{
return view('lethean::pricing');
}
public function sunrise(): \Illuminate\View\View
{
$result = $this->rpc->getAllAliases();
$aliases = $result['aliases'] ?? [];
$reserved = array_filter($aliases, fn ($a) => str_contains($a['comment'] ?? '', 'type=reserved'));
usort($reserved, fn ($a, $b) => ($a['alias'] ?? '') <=> ($b['alias'] ?? ''));
return view('lethean::sunrise', [
'names' => array_values($reserved),
'total' => count($reserved),
]);
}
public function about(): \Illuminate\View\View
{
return view('lethean::about');
}
public function why(): \Illuminate\View\View
{
return view('lethean::why');
}
public function network(): \Illuminate\View\View
{
$info = $this->rpc->getInfo();
return view('lethean::network', [
'info' => $info,
]);
}
public function servicesLanding(): \Illuminate\View\View
{
$result = $this->rpc->getAllAliases();
$aliases = $result['aliases'] ?? [];
$gateways = array_filter($aliases, fn ($a) => str_contains($a['comment'] ?? '', 'type=gateway'));
$proxyNodes = array_filter($aliases, fn ($a) =>
str_contains($a['comment'] ?? '', 'cap=') &&
(str_contains($a['comment'] ?? '', 'proxy') || str_contains($a['comment'] ?? '', 'vpn'))
);
// Enrich with live pairing data where available
$liveGateways = $this->registry->liveGateways();
$liveNames = array_column($liveGateways, 'name');
$enriched = array_map(function ($node) use ($liveNames, $liveGateways) {
$node['live'] = in_array($node['alias'], $liveNames);
if ($node['live']) {
$live = $liveGateways[array_search($node['alias'], $liveNames)];
$node['region'] = $live['region'] ?? 'unknown';
$node['load'] = $live['current_load'] ?? 0;
$node['connections'] = $live['current_connections'] ?? 0;
}
return $node;
}, array_values($proxyNodes));
return view('lethean::services-landing', [
'gatewayCount' => count($gateways),
'proxyNodes' => $enriched,
'liveCount' => count($liveGateways),
'totalNames' => count($aliases),
]);
}
public function sslCertificates(): \Illuminate\View\View
{
return view('lethean::services-ssl');
}
public function dnsHosting(): \Illuminate\View\View
{
return view('lethean::services-dns');
}
public function residentialProxy(): \Illuminate\View\View
{
$result = $this->rpc->getAllAliases();
$nodes = array_filter($result['aliases'] ?? [], fn ($a) =>
str_contains($a['comment'] ?? '', 'proxy') || str_contains($a['comment'] ?? '', 'vpn')
);
return view('lethean::services-residential', ['nodes' => array_values($nodes)]);
}
public function mobileProxy(): \Illuminate\View\View
{
$result = $this->rpc->getAllAliases();
$nodes = array_filter($result['aliases'] ?? [], fn ($a) =>
str_contains($a['comment'] ?? '', 'proxy') || str_contains($a['comment'] ?? '', 'vpn')
);
return view('lethean::services-mobile', ['nodes' => array_values($nodes)]);
}
public function seoTraffic(): \Illuminate\View\View
{
$result = $this->rpc->getAllAliases();
$nodes = array_filter($result['aliases'] ?? [], fn ($a) =>
str_contains($a['comment'] ?? '', 'proxy') || str_contains($a['comment'] ?? '', 'exit')
);
return view('lethean::services-seo', ['nodes' => array_values($nodes)]);
}
public function status(): \Illuminate\View\View
{
$chainInfo = $this->rpc->getInfo();
$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' => $chainHealth['status'] !== 'unhealthy',
'stale' => $chainHealth['stale'] ?? false,
'detail' => $chainHealth['detail'],
],
'wallet' => [
'label' => 'Registry Wallet',
'ok' => $walletHealth['status'] === 'healthy',
'detail' => $walletHealth['detail'],
],
'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();
$aliases = $result['aliases'] ?? [];
$gateways = [];
$services = [];
$exits = [];
foreach ($aliases as $alias) {
$comment = $alias['comment'] ?? '';
if (str_contains($comment, 'type=gateway')) {
$gateways[] = $alias;
} elseif (str_contains($comment, 'type=exit')) {
$exits[] = $alias;
} elseif (str_contains($comment, 'type=service')) {
$services[] = $alias;
}
}
return view('lethean::services', [
'gateways' => $gateways,
'services' => $services,
'exits' => $exits,
]);
}
}