lthn.io/app/Mod/Explorer/Views/index.blade.php
Claude 8a21996add
security: add CSP nonce attributes to inline scripts and styles
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>
2026-04-04 10:22:38 +01:00

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 &rarr;</a>
<a href="/names">Name directory &rarr;</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