500 lines
24 KiB
HTML
500 lines
24 KiB
HTML
<!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>1–5</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>
|