Skip to content

Commit c5027f9

Browse files
committed
feat: Add JSON API, Home Assistant support, and connect-info box
1 parent 8506cab commit c5027f9

2 files changed

Lines changed: 139 additions & 1 deletion

File tree

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ A lightweight, self-hosted dashboard that displays information from your Bitcoin
1313
- Light and dark mode with several built-in themes (Dark, Light, Nord, Solarized, Dracula), remembered across visits
1414
- Configurable default theme, with the option to lock it and hide the switcher for fixed/branded deployments
1515
- Tooltip explanations for every stat (hover over a stat label)
16+
- JSON API (`?format=json`) for Home Assistant, Grafana, scripts, and other tools
1617

1718

1819
## Installation
@@ -69,6 +70,10 @@ Supported environment variables (all optional; the defaults match the configurat
6970
| `THEME` | `dark` | Default theme: `dark`, `light`, `nord`, `solarized`, or `dracula` |
7071
| `SHOW_THEME_SWITCHER` | `true` | Set `false` to lock the theme and hide the switcher |
7172
| `REFRESH_SECONDS` | `60` | Auto-refresh interval in seconds (`0` disables auto-refresh) |
73+
| `ENABLE_JSON` | `true` | Set `false` to disable the `?format=json` API |
74+
| `SHOW_CONNECT` | `false` | Show a "Connect to this Node" box (for advertising a public node) |
75+
| `CONNECT_ADDRESS` | _(empty)_ | Public node address shown in that box, e.g. `node.example.com:18089` |
76+
| `CONNECT_NOTE` | _(empty)_ | Optional note in that box (e.g. a Tor address or usage notes) |
7277

7378

7479
## Custom Networks
@@ -120,6 +125,45 @@ docker run -d -p 80:80 \
120125
Monero is handled as a special case and cannot be used as a template for custom coins.
121126

122127

128+
## JSON API & Home Assistant
129+
130+
Append `?format=json` to the dashboard URL for a machine-readable snapshot of the current
131+
stats, e.g. `http://your-host/index.php?format=json`. Values are raw and unformatted, which
132+
makes them easy to consume from Home Assistant, Grafana, scripts, or anything else. It exposes
133+
the same public node data already shown on the dashboard, nothing more.
134+
135+
Example Home Assistant REST sensor (`configuration.yaml`):
136+
137+
```yaml
138+
sensor:
139+
- platform: rest
140+
name: Bitcoin Node
141+
resource: http://192.168.1.10/index.php?format=json
142+
scan_interval: 60
143+
value_template: "{{ value_json.block_height }}"
144+
json_attributes:
145+
- connections
146+
- difficulty
147+
- network_hashps
148+
- mempool_txns
149+
- mempool_bytes
150+
- verification_progress
151+
```
152+
153+
The response always includes `network`, `coin`, `unit`, `family`, and `updated` (Unix time).
154+
The remaining fields depend on the coin family (e.g. `block_height`, `connections`,
155+
`difficulty`, `network_hashps`, `mempool_txns`, `mempool_bytes`, `mempool_total_fee`,
156+
`total_transactions`, and fee estimates). Disabled sections or failed RPC calls report `null`.
157+
158+
Monetary amounts (`mempool_total_fee`, Monero `supply`) are in whole coins; fee rates are in
159+
native units (sat/vB for Bitcoin, piconero/kB for Monero). Very large estimates such as
160+
`network_hashps` may be rendered in scientific notation, which is still valid JSON.
161+
162+
The endpoint serves only the same public data already shown on the dashboard. To turn it off
163+
entirely (for example on a public host), set `ENABLE_JSON=false`; `?format=json` then just
164+
returns the normal dashboard page.
165+
166+
123167
## Security Notes
124168

125169
- Make sure to protect the dashboard if you’re displaying sensitive node info.

index.php

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ function env_bool($name, $default) {
2323
'theme' => getenv('THEME') ?: 'dark', // Default theme: dark, light, nord, solarized, or dracula
2424
'show_theme_switcher' => env_bool('SHOW_THEME_SWITCHER', true), // Set false to lock the theme above and hide the dropdown
2525
'refresh_seconds' => getenv('REFRESH_SECONDS') !== false ? (int) getenv('REFRESH_SECONDS') : 60, // Auto-refresh interval in seconds (0 disables)
26+
'enable_json' => env_bool('ENABLE_JSON', true), // Set false to disable the ?format=json API (e.g. for public hosts)
27+
// "Connect to this node" box, for advertising a public node. These are operator-provided
28+
// public details (not the internal RPC settings above), and only show when an address is set.
29+
'show_connect' => env_bool('SHOW_CONNECT', false), // Show the "Connect to this Node" box
30+
'connect_address' => getenv('CONNECT_ADDRESS') ?: '', // Public address to advertise, e.g. node.example.com:18089
31+
'connect_note' => getenv('CONNECT_NOTE') ?: '', // Optional note shown in the box (e.g. "Restricted RPC" or a Tor address)
2632
];
2733

2834
$network = strtoupper($dashboard_config['network']);
@@ -31,6 +37,10 @@ function env_bool($name, $default) {
3137
$theme = $dashboard_config['theme'] ?? 'dark';
3238
$show_theme_switcher = $dashboard_config['show_theme_switcher'] ?? true;
3339
$refresh_seconds = max(0, (int) ($dashboard_config['refresh_seconds'] ?? 60));
40+
$enable_json = $dashboard_config['enable_json'] ?? true;
41+
$show_connect = ($dashboard_config['show_connect'] ?? false) && ($dashboard_config['connect_address'] ?? '') !== '';
42+
$connect_address = $dashboard_config['connect_address'] ?? '';
43+
$connect_note = $dashboard_config['connect_note'] ?? '';
3444

3545
$errors = [];
3646

@@ -422,7 +432,68 @@ function format_hashrate($hashps) {
422432
$errors[] = 'Failed to fetch Monero fee estimates.';
423433
}
424434
}
425-
435+
436+
}
437+
438+
439+
// Machine-readable snapshot for JSON consumers (Home Assistant, Grafana, scripts, etc.).
440+
// Request it with ?format=json. Values are raw/unformatted for easy parsing. The `?? null`
441+
// guards keep it warning-free when a section is disabled or its RPC call failed.
442+
$stats = [
443+
'network' => $network,
444+
'coin' => $coin,
445+
'unit' => $profile['unit'],
446+
'family' => $family,
447+
'updated' => time(),
448+
];
449+
450+
if ($family === 'bitcoin') {
451+
$stats += [
452+
'version' => $netinfo['subversion'] ?? null,
453+
'chain' => $blockinfo['chain'] ?? null,
454+
'pruned' => $blockinfo['pruned'] ?? null,
455+
'connections' => $netinfo['connections'] ?? null,
456+
'block_height' => $blockinfo['blocks'] ?? null,
457+
'headers' => $blockinfo['headers'] ?? null,
458+
'verification_progress' => $blockinfo['verificationprogress'] ?? null,
459+
'size_on_disk' => $blockinfo['size_on_disk'] ?? null,
460+
'mempool_txns' => $mempoolinfo['size'] ?? null,
461+
'mempool_bytes' => $mempoolinfo['bytes'] ?? null,
462+
'mempool_total_fee' => $mempoolinfo['total_fee'] ?? null,
463+
'difficulty' => $blockinfo['difficulty'] ?? null,
464+
'network_hashps' => $mininginfo['networkhashps'] ?? null,
465+
'total_transactions' => $chaintxstats['txcount'] ?? null,
466+
'tx_rate' => $chaintxstats['txrate'] ?? null,
467+
'window_tx_count' => $chaintxstats['window_tx_count'] ?? null,
468+
'fees_sat_vb' => ['fast' => $fastfees ?? null, 'medium' => $mediumfees ?? null, 'slow' => $slowfees ?? null],
469+
];
470+
} elseif ($family === 'monero') {
471+
$stats += [
472+
'version' => $getinfo['version'] ?? null,
473+
'chain' => $getinfo['nettype'] ?? null,
474+
'connections' => isset($getinfo['incoming_connections_count'], $getinfo['outgoing_connections_count'])
475+
? $getinfo['incoming_connections_count'] + $getinfo['outgoing_connections_count'] : null,
476+
'block_height' => $getinfo['height'] ?? ($block_count['count'] ?? null),
477+
'synchronized' => $getinfo['synchronized'] ?? null,
478+
'database_size' => $getinfo['database_size'] ?? null,
479+
'mempool_txns' => $mempoolinfo['pool_stats']['txs_total'] ?? null,
480+
'mempool_bytes' => $mempoolinfo['pool_stats']['bytes_total'] ?? null,
481+
'mempool_total_fee' => isset($mempoolinfo['pool_stats']['fee_total']) ? $mempoolinfo['pool_stats']['fee_total'] / 1e12 : null,
482+
'difficulty' => $getinfo['difficulty'] ?? null,
483+
'total_transactions' => $getinfo['tx_count'] ?? null,
484+
'supply' => isset($mininginfo['already_generated_coins']) ? $mininginfo['already_generated_coins'] / 1e12 : null,
485+
'fees_per_kb' => $getfees['fees'] ?? null,
486+
];
487+
}
488+
489+
if (!empty($errors)) {
490+
$stats['errors'] = $errors;
491+
}
492+
493+
if ($enable_json && ($_GET['format'] ?? '') === 'json') {
494+
header('Content-Type: application/json');
495+
echo json_encode($stats, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
496+
exit;
426497
}
427498

428499
?>
@@ -582,6 +653,19 @@ function format_hashrate($hashps) {
582653
text-underline-offset: 3px;
583654
}
584655

656+
/* The "Connect to this Node" box spans the full width as a banner above the grid */
657+
.section.connect-box {
658+
flex: 0 0 100%;
659+
max-width: 100%;
660+
text-align: center;
661+
}
662+
663+
.section.connect-box .stat-row {
664+
justify-content: center;
665+
gap: 0.5rem;
666+
border-bottom: none;
667+
}
668+
585669
.dashboard-header {
586670
position: fixed;
587671
top: 0;
@@ -689,6 +773,16 @@ function setTheme(theme) {
689773
<div id="dashboard-content">
690774
<div class="compact-dashboard<?= $show_theme_switcher ? '' : ' no-switcher' ?>">
691775

776+
<?php if ($show_connect): ?>
777+
<div class="section connect-box">
778+
<div class="section-title">Connect to this Node</div>
779+
<div class="stat-row"><span title="The public address to point your wallet or client at.">Address</span><span><?= htmlspecialchars($connect_address) ?></span></div>
780+
<?php if ($connect_note !== ''): ?>
781+
<div class="stat-row"><span title="Additional connection details from the node operator.">Note</span><span><?= htmlspecialchars($connect_note) ?></span></div>
782+
<?php endif; ?>
783+
</div>
784+
<?php endif; ?>
785+
692786
<?php if ($dashboard_config['show_node_info']): ?>
693787
<div class="section">
694788
<div class="section-title">Node Info</div>

0 commit comments

Comments
 (0)