Added @cspnonce to all inline <script> and <style> tags in layout, explorer, and register views. Enabled nonce generation in headers config. unsafe-inline kept as fallback. Nonces will activate after container restart when the Headers Boot registers the Blade directive. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
122 lines
6 KiB
PHP
122 lines
6 KiB
PHP
@extends('lethean::layout')
|
|
|
|
@section('title', 'Explorer')
|
|
|
|
@section('content')
|
|
<div class="section">
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
|
<h2 style="margin: 0;">Block Explorer</h2>
|
|
<span style="font-size: 0.8rem; color: var(--muted);">Live Chain <span id="feed-dot" style="color: var(--green);">●</span></span>
|
|
</div>
|
|
|
|
<div id="feed-entries" style="font-family: monospace; font-size: 0.8rem; color: var(--muted); max-height: 180px; overflow: hidden; background: var(--surface); border-radius: 0.5rem; padding: 0.75rem; border: 1px solid var(--border); margin-bottom: 1.5rem;"></div>
|
|
|
|
<form method="get" action="/explorer/search" style="margin-bottom: 1.5rem; display: flex; gap: 0.5rem;">
|
|
<input type="text" name="q" placeholder="Search by block height, hash, or tx hash..."
|
|
style="flex: 1; padding: 0.5rem 1rem; background: var(--surface); border: 1px solid var(--border); border-radius: 0.5rem; color: var(--text); font-size: 0.9rem;">
|
|
<button type="submit" class="api-link" style="margin-top: 0;">Search</button>
|
|
</form>
|
|
|
|
<div class="stats">
|
|
<div class="stat">
|
|
<a href="/explorer/block/{{ ($info['height'] ?? 1) - 1 }}" style="text-decoration: none;">
|
|
<div class="value">{{ number_format($info['height'] ?? 0) }}</div>
|
|
<div class="label">Height</div>
|
|
</a>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="value">{{ number_format($info['tx_count'] ?? 0) }}</div>
|
|
<div class="label">Transactions</div>
|
|
</div>
|
|
<div class="stat">
|
|
<a href="/explorer/aliases" style="text-decoration: none;">
|
|
<div class="value">{{ number_format($info['alias_count'] ?? 0) }}</div>
|
|
<div class="label">Names</div>
|
|
</a>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="value">{{ number_format(($info['height'] ?? 0) + 10000000) }}</div>
|
|
<div class="label">Supply (LTHN)</div>
|
|
</div>
|
|
</div>
|
|
|
|
<h3 style="margin-top: 2rem; margin-bottom: 1rem;">Recent Blocks</h3>
|
|
<table style="width: 100%; border-collapse: collapse;">
|
|
<tr style="border-bottom: 1px solid var(--border); text-align: left;">
|
|
<th style="padding: 0.75rem; color: var(--muted); font-size: 0.8rem; text-transform: uppercase;">Height</th>
|
|
<th style="padding: 0.75rem; color: var(--muted); font-size: 0.8rem; text-transform: uppercase;">Type</th>
|
|
<th style="padding: 0.75rem; color: var(--muted); font-size: 0.8rem; text-transform: uppercase;">Time</th>
|
|
<th style="padding: 0.75rem; color: var(--muted); font-size: 0.8rem; text-transform: uppercase;">Difficulty</th>
|
|
<th style="padding: 0.75rem; color: var(--muted); font-size: 0.8rem; text-transform: uppercase;">Hash</th>
|
|
</tr>
|
|
@foreach($blocks as $block)
|
|
@php
|
|
$isPos = $block['is_pos_block'] ?? false;
|
|
$h = $block['height'] ?? 0;
|
|
@endphp
|
|
<tr style="border-bottom: 1px solid var(--border);">
|
|
<td style="padding: 0.75rem;">
|
|
<a href="/explorer/block/{{ $h }}" style="font-weight: 600;">{{ number_format($h) }}</a>
|
|
</td>
|
|
<td style="padding: 0.75rem;">
|
|
<span class="badge {{ $isPos ? 'badge-amber' : 'badge-green' }}">{{ $isPos ? 'PoS' : 'PoW' }}</span>
|
|
</td>
|
|
<td style="padding: 0.75rem; color: var(--muted); font-size: 0.85rem;">
|
|
{{ isset($block['timestamp']) ? date('H:i:s', $block['timestamp']) : '' }}
|
|
</td>
|
|
<td style="padding: 0.75rem;">{{ number_format($block['difficulty'] ?? 0) }}</td>
|
|
<td style="padding: 0.75rem;">
|
|
<a href="/explorer/block/{{ $block['hash'] ?? '' }}" style="font-family: monospace; font-size: 0.75rem;">{{ substr($block['hash'] ?? '', 0, 16) }}...</a>
|
|
</td>
|
|
</tr>
|
|
@endforeach
|
|
</table>
|
|
|
|
<div style="margin-top: 1.5rem; display: flex; gap: 1.5rem;">
|
|
<a href="/explorer/aliases">View all names →</a>
|
|
<a href="/names">Name directory →</a>
|
|
</div>
|
|
|
|
<script @cspnonce>
|
|
(function() {
|
|
var feed = document.getElementById('feed-entries');
|
|
var dot = document.getElementById('feed-dot');
|
|
var lastHeight = 0;
|
|
|
|
function addEntry(text, color) {
|
|
var line = document.createElement('div');
|
|
line.style.padding = '2px 0';
|
|
line.style.borderBottom = '1px solid rgba(255,255,255,0.05)';
|
|
line.style.color = color || 'var(--muted)';
|
|
line.textContent = new Date().toLocaleTimeString() + ' ' + text;
|
|
feed.insertBefore(line, feed.firstChild);
|
|
while (feed.children.length > 8) feed.removeChild(feed.lastChild);
|
|
}
|
|
|
|
function poll() {
|
|
fetch((window.LTHN_API || '') + '/v1/explorer/info', {headers: {'Accept': 'application/json'}})
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
var h = data.height || 0;
|
|
if (lastHeight > 0 && h > lastHeight) {
|
|
for (var i = lastHeight; i < h; i++) {
|
|
addEntry('Block #' + i + ' mined', '#34d399');
|
|
}
|
|
dot.style.color = '#34d399';
|
|
setTimeout(function() { dot.style.color = 'var(--muted)'; }, 2000);
|
|
}
|
|
if (lastHeight === 0) {
|
|
addEntry('Connected to chain at height ' + h, '#818cf8');
|
|
addEntry(data.alias_count + ' names registered, ' + data.tx_count + ' transactions', 'var(--muted)');
|
|
}
|
|
lastHeight = h;
|
|
})
|
|
.catch(function() { addEntry('Connection lost — retrying...', '#fbbf24'); });
|
|
}
|
|
|
|
poll();
|
|
setInterval(poll, 10000);
|
|
})();
|
|
</script>
|
|
</div>
|
|
@endsection
|