Files
peardata/index.html
T
Raven Scott 3fc0245b32
CI / test (push) Successful in 1m3s
Release rolling / release (push) Has been cancelled
Updates
2026-07-18 23:47:54 -04:00

500 lines
24 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PearData — P2P Fleet Monitoring</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;800&family=IBM+Plex+Mono:wght@400;500&display=swap"
rel="stylesheet"
/>
<script>
try {
var raw = localStorage.getItem('peardata.settings.v1') || localStorage.getItem('peardata-ui-boot') || '{}'
var s = JSON.parse(raw)
var theme = s.theme || (s.settings && s.settings.theme)
if (theme) document.documentElement.dataset.theme = theme
} catch (e) {}
</script>
<link rel="stylesheet" href="./ui/styles.css" />
</head>
<body data-accent="teal" data-density="comfortable" data-reduce-motion="0">
<div id="titlebar" role="banner">
<div class="titlebar-left">
<pear-ctrl></pear-ctrl>
<div class="app-brand" aria-hidden="true">
<div class="app-brand-mark"></div>
<div class="app-brand-text">
PearData <span>P2P observability</span>
</div>
</div>
</div>
<div class="titlebar-right">
<span id="active-peer-chip" class="chip chip-muted">no agent</span>
<span id="role-badge" class="badge"></span>
<div id="status-chip" class="chip offline">offline</div>
</div>
</div>
<aside id="sidebar">
<nav id="main-nav" aria-label="Main">
<div class="nav-group-label">Monitor</div>
<button type="button" class="nav-link active" data-view="overview">
<span class="nav-ico"></span><span class="nav-label">Overview</span>
</button>
<button type="button" class="nav-link" data-view="charts">
<span class="nav-ico"></span><span class="nav-label">Charts</span>
</button>
<button type="button" class="nav-link" data-view="alerts">
<span class="nav-ico"></span><span class="nav-label">Alerts</span>
</button>
<button type="button" class="nav-link" data-view="logs">
<span class="nav-ico"></span><span class="nav-label">Logs</span>
</button>
<div class="nav-group-label">Fleet</div>
<button type="button" class="nav-link" data-view="fleet">
<span class="nav-ico"></span><span class="nav-label">Fleet</span>
</button>
<button type="button" class="nav-link" data-view="connect">
<span class="nav-ico"></span><span class="nav-label">Connect</span>
</button>
<div class="nav-group-label">App</div>
<button type="button" class="nav-link" data-view="settings">
<span class="nav-ico"></span><span class="nav-label">Settings</span>
</button>
</nav>
<div class="sidebar-footer">
<button type="button" id="collapse-sidebar-btn" class="ghost compact" title="Collapse sidebar">
</button>
<div id="conn-meta" class="meta muted">Ready</div>
</div>
</aside>
<div id="content">
<div id="offline-banner" class="offline-banner hidden" role="status">
Agent offline — showing last-known samples. Reconnecting…
</div>
<div id="restoring-banner" class="offline-banner restoring-banner hidden" role="status">
Restoring saved agents…
</div>
<!-- Overview -->
<section id="overview-view" class="view">
<header class="page-header overview-header">
<div>
<p class="dash-kicker">Live</p>
<h1 class="dash-title">Overview</h1>
<p class="page-subtitle">Active agent at a glance</p>
</div>
<div class="page-actions">
<button type="button" id="btn-refresh-meta" class="ghost">Refresh</button>
</div>
</header>
<section class="dash-kpis">
<div class="dash-kpi" data-stat="cpu">
<span class="stat-label">CPU</span>
<strong id="stat-cpu"></strong>
</div>
<div class="dash-kpi" data-stat="ram">
<span class="stat-label">RAM used</span>
<strong id="stat-ram"></strong>
</div>
<div class="dash-kpi" data-stat="load">
<span class="stat-label">Load 1m</span>
<strong id="stat-load"></strong>
</div>
<div class="dash-kpi" data-stat="net">
<span class="stat-label">Net RX</span>
<strong id="stat-net"></strong>
</div>
<div class="dash-kpi" data-stat="health" id="kpi-health">
<span class="stat-label">Health</span>
<strong id="stat-health"></strong>
</div>
</section>
<section id="fleet-strip" class="fleet-strip dash-card hidden" aria-live="polite">
<header>
<h3>Fleet</h3>
<span id="fleet-summary" class="muted"></span>
</header>
<ul id="fleet-children" class="fleet-children"></ul>
</section>
<section class="charts-grid overview-charts">
<article class="dash-card chart-panel" data-chart="system.cpu" id="panel-cpu">
<header>
<h3>CPU</h3>
<span class="muted" id="chart-cpu-label">system.cpu</span>
</header>
<canvas id="chart-cpu" height="84"></canvas>
<div class="chart-legend" id="legend-cpu"></div>
</article>
<article class="dash-card chart-panel" data-chart="mem.available" id="panel-ram">
<header>
<h3>Memory</h3>
<span class="muted">system.ram</span>
</header>
<canvas id="chart-ram" height="84"></canvas>
</article>
<article class="dash-card chart-panel" data-chart="system.net" id="panel-net">
<header>
<h3>Network</h3>
<span class="muted">system.net</span>
</header>
<canvas id="chart-net" height="84"></canvas>
</article>
<article class="dash-card chart-panel" data-chart="system.io" id="panel-io">
<header>
<h3>Disk I/O</h3>
<span class="muted">system.io</span>
</header>
<canvas id="chart-io" height="84"></canvas>
</article>
<article class="dash-card chart-panel" data-chart="system.load" id="panel-load">
<header>
<h3>Load</h3>
<span class="muted">system.load</span>
</header>
<canvas id="chart-load" height="84"></canvas>
</article>
<article class="dash-card chart-panel" data-chart="explore" id="panel-explore">
<header>
<h3>Spotlight</h3>
<label class="chart-picker">
<select id="explore-chart" aria-label="Spotlight chart">
<option value="system.io">system.io</option>
</select>
</label>
</header>
<canvas id="chart-explore" height="84"></canvas>
</article>
</section>
</section>
<!-- Master metrics wall -->
<section id="charts-view" class="view hidden">
<header class="page-header metrics-header">
<div>
<p class="dash-kicker">Metrics</p>
<h1 class="dash-title">Charts</h1>
<p class="page-subtitle">Scroll freely · <kbd></kbd>/<kbd></kbd>+scroll zooms · <kbd>Space</kbd> · <kbd>/</kbd> · <kbd>15</kbd></p>
</div>
</header>
<div class="metrics-toolbar" id="metrics-timebar">
<div class="metrics-toolbar-left">
<span id="metrics-live" class="metrics-live is-live" aria-live="polite">
<i class="metrics-live-dot" aria-hidden="true"></i>
<span id="metrics-live-label">Live</span>
</span>
<button type="button" id="metrics-play" class="btn btn-ghost" aria-pressed="true">Pause</button>
<button type="button" id="metrics-force-play" class="btn btn-ghost" aria-pressed="false" title="Keep live (wallboard)">Force</button>
<button type="button" id="metrics-board" class="btn btn-ghost" aria-pressed="false" title="Pinned board only">Board</button>
<button type="button" id="metrics-correlate" class="btn btn-ghost" aria-pressed="false" title="Metric Correlations — brush a highlight, then find changed metrics">Correlate</button>
<button type="button" id="metrics-reset" class="btn btn-ghost" title="Reset to live 5m">Reset</button>
</div>
<div id="metrics-presets" class="metrics-presets" role="group" aria-label="Time window">
<button type="button" class="metrics-tf" data-preset="1m" title="Last 1 minute">1m</button>
<button type="button" class="metrics-tf active" data-preset="5m" title="Last 5 minutes">5m</button>
<button type="button" class="metrics-tf" data-preset="15m" title="Last 15 minutes">15m</button>
<button type="button" class="metrics-tf" data-preset="1h" title="Last hour">1h</button>
<button type="button" class="metrics-tf" data-preset="6h" title="Last 6 hours">6h</button>
</div>
<div class="metrics-toolbar-right">
<button type="button" id="metrics-filters-btn" class="btn btn-ghost" aria-expanded="false" aria-controls="metrics-filters-panel" title="Search, sections, group, sort">Filters</button>
<span id="metrics-filter-chip" class="metrics-filter-chip hidden" role="status"></span>
<span id="metrics-meta" class="muted metrics-meta"></span>
<span id="metrics-hover" class="muted metrics-hover"></span>
</div>
</div>
<div id="metrics-mc-bar" class="metrics-mc-bar hidden" aria-label="Metric Correlations">
<label class="metrics-group-label muted">
Method
<select id="metrics-mc-method" aria-label="Correlation method">
<option value="volume">volume</option>
<option value="ks2">ks2</option>
<option value="anomaly-rate">anomaly-rate</option>
<option value="value">value (cv)</option>
</select>
</label>
<span id="metrics-mc-hint" class="muted metrics-mc-hint">Brush a chart (≥15s), then Find Correlations</span>
<button type="button" id="metrics-mc-run" class="btn btn-primary" disabled>Find Correlations</button>
<button type="button" id="metrics-mc-clear" class="btn btn-ghost">Clear</button>
</div>
<p id="metrics-retention-hint" class="metrics-retention-hint muted hidden" role="status"></p>
<div id="metrics-related" class="related-panel hidden"></div>
<div id="metrics-mc-results" class="related-panel mc-results-panel hidden"></div>
<div class="metrics-shell">
<aside id="metrics-filters-panel" class="metrics-toc metrics-filters-panel" hidden>
<div class="metrics-filters-controls">
<label class="metrics-group-label muted">
Group
<select id="metrics-group" aria-label="Downsample aggregation">
<option value="average">average</option>
<option value="min">min</option>
<option value="max">max</option>
<option value="sum">sum</option>
</select>
</label>
<button type="button" id="metrics-dim-sort" class="btn btn-ghost">Sort: name</button>
</div>
<input id="chart-search" type="search" placeholder="Filter charts…" autocomplete="off" />
<nav id="metrics-toc" class="metrics-toc-nav" aria-label="Chart sections"></nav>
</aside>
<div id="metrics-wall" class="metrics-wall" tabindex="0"></div>
</div>
</section>
<!-- Alerts -->
<section id="alerts-view" class="view hidden">
<header class="page-header">
<div>
<p class="dash-kicker">Signals</p>
<h1 class="dash-title">Alerts</h1>
<p class="page-subtitle">Anomalies and threshold events from the agent</p>
</div>
<label class="check-row">
<input type="checkbox" id="notify-desktop" checked />
Desktop notifications
</label>
</header>
<div class="dash-card">
<ul id="anomaly-list" class="event-list"></ul>
</div>
</section>
<!-- Logs -->
<section id="logs-view" class="view hidden">
<header class="page-header logs-header">
<div>
<p class="dash-kicker">Investigate</p>
<h1 class="dash-title">Logs</h1>
<p class="page-subtitle">Host journal · anomalies · audit · <kbd>/</kbd> search · Follow for live</p>
</div>
<div class="page-actions logs-header-actions">
<button type="button" id="logs-follow" class="btn btn-ghost" aria-pressed="false" title="Auto-refresh every 5s">Follow</button>
<button type="button" id="logs-refresh" class="btn btn-ghost" title="Refresh now">Refresh</button>
</div>
</header>
<div class="logs-toolbar" id="logs-toolbar">
<div class="logs-toolbar-top">
<div id="logs-sources" class="logs-sources" role="tablist" aria-label="Log source">
<button type="button" class="metrics-tf active" data-log-source="journal" role="tab" aria-selected="true" title="Admin · host journal">Journal</button>
<button type="button" class="metrics-tf" data-log-source="anomaly" role="tab" aria-selected="false">Anomalies</button>
<button type="button" class="metrics-tf" data-log-source="audit" role="tab" aria-selected="false" title="Admin · agent audit trail">Audit</button>
</div>
<div id="logs-presets" class="metrics-presets" role="group" aria-label="Time range">
<button type="button" class="metrics-tf" data-log-range="15m">15m</button>
<button type="button" class="metrics-tf active" data-log-range="1h">1h</button>
<button type="button" class="metrics-tf" data-log-range="6h">6h</button>
<button type="button" class="metrics-tf" data-log-range="24h">24h</button>
</div>
</div>
<div class="logs-search-row">
<input id="logs-q" type="search" placeholder="Search messages…" autocomplete="off" spellcheck="false" />
<label class="metrics-group-label muted logs-journal-only">
Max priority
<select id="logs-priority" aria-label="Journal max priority (and above)">
<option value="">any</option>
<option value="3">err+</option>
<option value="4">warning+</option>
<option value="5">notice+</option>
<option value="6">info+</option>
<option value="2">crit+</option>
<option value="0">emerg only</option>
</select>
</label>
<label class="metrics-group-label muted logs-journal-only">
Unit
<input id="logs-unit" type="text" placeholder="peardata.service" autocomplete="off" spellcheck="false" />
</label>
<button type="button" id="logs-search-btn" class="btn btn-primary">Search</button>
<button type="button" id="logs-clear-btn" class="btn btn-ghost" title="Clear search filters">Clear</button>
</div>
<div class="logs-status-row">
<p id="logs-status" class="muted logs-status" role="status"></p>
<button type="button" id="logs-copy" class="btn btn-ghost logs-copy-btn" hidden title="Copy visible lines">Copy</button>
</div>
</div>
<div class="logs-stream" id="logs-stream">
<div class="logs-cols" aria-hidden="true">
<span>Time</span>
<span>Sev</span>
<span>Unit</span>
<span>Message</span>
<span></span>
</div>
<ul id="logs-list" class="logs-list" aria-live="polite"></ul>
<div id="logs-empty" class="logs-empty hidden" role="status"></div>
<div class="logs-footer">
<button type="button" id="logs-more" class="btn btn-ghost hidden">Load more</button>
</div>
</div>
</section>
<!-- Fleet -->
<section id="fleet-view" class="view hidden">
<header class="page-header">
<div>
<p class="dash-kicker">Multi-host</p>
<h1 class="dash-title">Fleet</h1>
<p class="page-subtitle">Concurrent agents — set active, reconnect, open charts</p>
</div>
<div class="page-actions">
<label class="check-row">
<input type="checkbox" id="compare-toggle" />
Compare CPU overlay
</label>
<button type="button" id="fleet-refresh-btn" class="btn btn-ghost">Refresh</button>
<button type="button" id="btn-invite" class="btn btn-ghost" disabled>Mint invite</button>
</div>
</header>
<section id="fleet-summary-bar" class="fleet-summary-bar" aria-live="polite">
<div class="fleet-stat" data-fleet-stat="live"><span class="fleet-stat-val" id="fleet-stat-live">0</span><span class="muted">Live</span></div>
<div class="fleet-stat" data-fleet-stat="retry"><span class="fleet-stat-val" id="fleet-stat-retry">0</span><span class="muted">Retrying</span></div>
<div class="fleet-stat" data-fleet-stat="offline"><span class="fleet-stat-val" id="fleet-stat-offline">0</span><span class="muted">Offline</span></div>
<div class="fleet-stat" data-fleet-stat="failed"><span class="fleet-stat-val" id="fleet-stat-failed">0</span><span class="muted">Failed</span></div>
</section>
<div id="fleet-cards" class="fleet-cards"></div>
<pre id="invite-out" class="invite-out hidden"></pre>
</section>
<!-- Connect -->
<section id="connect-view" class="view hidden">
<header class="page-header">
<div>
<p class="dash-kicker">Dial</p>
<h1 class="dash-title">Connect</h1>
<p class="page-subtitle">Public key or <code>pd1.</code> invite</p>
</div>
</header>
<div class="dash-card connect-card">
<label>
Agent / invite
<textarea id="connect-input" rows="3" placeholder="64-hex key or pd1.…"></textarea>
</label>
<label>
Alias (optional)
<input id="peer-alias" type="text" maxlength="64" placeholder="homelab-1" autocomplete="off" />
</label>
<label>
Admin seed (optional)
<input id="admin-seed" type="password" autocomplete="off" placeholder="SERVER_SEED" />
</label>
<div class="row">
<button id="btn-connect" class="primary">Connect</button>
<button id="btn-disconnect" class="ghost" disabled>Disconnect</button>
</div>
</div>
</section>
<!-- Settings -->
<section id="settings-view" class="view hidden">
<header class="page-header">
<div>
<p class="dash-kicker">Preferences</p>
<h1 class="dash-title">Settings</h1>
<p class="page-subtitle">Appearance, charts, and desktop behaviour</p>
</div>
</header>
<div class="settings-subtabs" id="settings-tabs" role="tablist">
<button type="button" class="settings-tab active" data-settings-tab="appearance">Appearance</button>
<button type="button" class="settings-tab" data-settings-tab="charts">Charts</button>
<button type="button" class="settings-tab" data-settings-tab="connections">Connections</button>
<button type="button" class="settings-tab" data-settings-tab="notifications">Notifications</button>
<button type="button" class="settings-tab" data-settings-tab="about">About</button>
</div>
<div class="settings-panel active" data-settings-panel="appearance">
<div class="dash-card settings-section">
<h3>Theme</h3>
<div class="seg">
<button type="button" class="seg-btn" data-theme-set="dark">Dark</button>
<button type="button" class="seg-btn" data-theme-set="light">Light</button>
</div>
<h3>Accent</h3>
<div class="accent-swatches" id="accent-swatches">
<button type="button" data-accent="teal" class="swatch" title="Teal"></button>
<button type="button" data-accent="emerald" class="swatch" title="Emerald"></button>
<button type="button" data-accent="sky" class="swatch" title="Sky"></button>
<button type="button" data-accent="amber" class="swatch" title="Amber"></button>
<button type="button" data-accent="rose" class="swatch" title="Rose"></button>
</div>
<h3>Density</h3>
<div class="seg">
<button type="button" class="seg-btn" data-density-set="compact">Compact</button>
<button type="button" class="seg-btn" data-density-set="comfortable">Comfortable</button>
<button type="button" class="seg-btn" data-density-set="spacious">Spacious</button>
</div>
<label class="check-row">
<input type="checkbox" id="setting-reduce-motion" />
Reduce motion
</label>
</div>
</div>
<div class="settings-panel" data-settings-panel="charts">
<div class="dash-card settings-section">
<h3>History window</h3>
<label>
Overview spark depth
<input id="setting-chart-points" type="number" min="30" max="300" step="10" value="90" />
</label>
<p class="muted settings-hint">Fallback ring size when Charts has no active window. While Charts is open, Overview follows the Charts timeframe (capped at 300).</p>
<label>
Default spotlight chart
<input id="setting-default-explore" type="text" value="system.io" />
</label>
</div>
</div>
<div class="settings-panel" data-settings-panel="connections">
<div class="dash-card settings-section">
<h3>Multi-agent</h3>
<label class="check-row">
<input type="checkbox" id="setting-auto-restore" checked />
Restore saved agents on launch
</label>
<label>
Max reconnect attempts
<input id="setting-reconnect-max" type="number" min="0" max="100" step="1" value="20" />
</label>
<p class="hint">
Saved agents live in <code>~/.config/peardata/cache/peers.json</code>.
Last active agent is restored when online. Reconnect never steals the active host.
</p>
<button type="button" id="btn-reset-peers" class="ghost">Forget all saved agents</button>
</div>
</div>
<div class="settings-panel" data-settings-panel="notifications">
<div class="dash-card settings-section">
<h3>Desktop</h3>
<label class="check-row">
<input type="checkbox" id="setting-notify" checked />
Enable desktop notifications for alerts
</label>
<p class="hint">Agent webhooks are configured on the server via environment variables.</p>
</div>
</div>
<div class="settings-panel" data-settings-panel="about">
<div class="dash-card settings-section">
<h3>PearData</h3>
<p class="hint">Decentralized P2P real-time host monitoring over HyperDHT.</p>
<pre id="server-info" class="server-info muted">Not connected</pre>
<h3>Log</h3>
<pre id="log" class="log"></pre>
</div>
</div>
</section>
</div>
<script type="module" src="./app.js"></script>
</body>
</html>