@yuuki824/kanshi 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +34 -0
- package/Dockerfile +53 -0
- package/LICENSE +661 -0
- package/README.md +219 -0
- package/bin/kanshi.js +63 -0
- package/docker-compose.yml +84 -0
- package/go.mod +3 -0
- package/internal/access/access.go +275 -0
- package/internal/access/listeners.go +229 -0
- package/internal/config/config.go +256 -0
- package/internal/config/defaults_other.go +5 -0
- package/internal/config/defaults_windows.go +4 -0
- package/internal/config/envfile.go +147 -0
- package/internal/dockerstats/dial.go +32 -0
- package/internal/dockerstats/dial_other.go +13 -0
- package/internal/dockerstats/dial_windows.go +62 -0
- package/internal/dockerstats/dockerstats.go +626 -0
- package/internal/roots/roots.go +175 -0
- package/internal/roots/roots_linux.go +132 -0
- package/internal/roots/roots_windows.go +74 -0
- package/internal/server/access.go +155 -0
- package/internal/server/server.go +523 -0
- package/internal/server/static.go +165 -0
- package/internal/storage/cache.go +283 -0
- package/internal/storage/dir_linux.go +177 -0
- package/internal/storage/dir_windows.go +244 -0
- package/internal/storage/statat_fast.go +21 -0
- package/internal/storage/statat_other.go +11 -0
- package/internal/storage/storage.go +598 -0
- package/internal/storage/sysnum_amd64.go +7 -0
- package/internal/storage/sysnum_generic.go +7 -0
- package/internal/storage/walk.go +342 -0
- package/internal/vitals/vitals.go +246 -0
- package/internal/vitals/vitals_linux.go +548 -0
- package/internal/vitals/vitals_windows.go +286 -0
- package/main.go +129 -0
- package/main_other.go +11 -0
- package/main_windows.go +29 -0
- package/package.json +49 -0
- package/web/app.js +835 -0
- package/web/apple-touch-icon.png +0 -0
- package/web/icon.svg +6 -0
- package/web/index.html +151 -0
- package/web/style.css +356 -0
- package/web/treemap.js +217 -0
|
Binary file
|
package/web/icon.svg
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
2
|
+
<rect width="32" height="32" rx="7" fill="#1c5cab"/>
|
|
3
|
+
<rect x="6" y="6" width="11" height="20" rx="1.5" fill="#fff"/>
|
|
4
|
+
<rect x="19" y="6" width="7" height="11" rx="1.5" fill="#fff" fill-opacity=".75"/>
|
|
5
|
+
<rect x="19" y="19" width="7" height="7" rx="1.5" fill="#fab219"/>
|
|
6
|
+
</svg>
|
package/web/index.html
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en" data-theme="auto">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
6
|
+
<meta name="color-scheme" content="light dark">
|
|
7
|
+
<meta name="theme-color" content="#f9f9f7" media="(prefers-color-scheme: light)">
|
|
8
|
+
<meta name="theme-color" content="#0d0d0d" media="(prefers-color-scheme: dark)">
|
|
9
|
+
<title>kanshi</title>
|
|
10
|
+
<link rel="stylesheet" href="/static/style.css">
|
|
11
|
+
<link rel="icon" href="/static/icon.svg" type="image/svg+xml">
|
|
12
|
+
<link rel="apple-touch-icon" href="/static/apple-touch-icon.png">
|
|
13
|
+
</head>
|
|
14
|
+
<body class="viz-root">
|
|
15
|
+
|
|
16
|
+
<header class="topbar">
|
|
17
|
+
<h1>kanshi</h1>
|
|
18
|
+
<div class="topbar-meta">
|
|
19
|
+
<span id="uptime" class="muted">—</span>
|
|
20
|
+
<span id="conn" class="conn" title="Live connection status"><i></i><span>connecting</span></span>
|
|
21
|
+
</div>
|
|
22
|
+
</header>
|
|
23
|
+
|
|
24
|
+
<main id="app">
|
|
25
|
+
|
|
26
|
+
<!-- ───────── CPU: the hero figure, exactly one per view ───────── -->
|
|
27
|
+
<section class="card" aria-labelledby="cpu-h">
|
|
28
|
+
<div class="card-head">
|
|
29
|
+
<h2 id="cpu-h">Processor</h2>
|
|
30
|
+
<span id="cpu-meta" class="muted small">—</span>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="hero-row">
|
|
33
|
+
<div>
|
|
34
|
+
<div id="cpu-hero" class="hero">—<span class="hero-unit">%</span></div>
|
|
35
|
+
<div class="hero-label muted small">utilisation across <span id="cpu-count">—</span> cores</div>
|
|
36
|
+
</div>
|
|
37
|
+
<dl class="minitable" id="cpu-aux"></dl>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="cores" id="cores" role="img" aria-label="Per-core utilisation"></div>
|
|
40
|
+
</section>
|
|
41
|
+
|
|
42
|
+
<!-- ───────── Memory + filesystem meters ───────── -->
|
|
43
|
+
<section class="card" aria-labelledby="mem-h">
|
|
44
|
+
<div class="card-head"><h2 id="mem-h">Memory & volumes</h2></div>
|
|
45
|
+
<div id="meters" class="meters"></div>
|
|
46
|
+
</section>
|
|
47
|
+
|
|
48
|
+
<!-- ───────── Storage treemap ───────── -->
|
|
49
|
+
<section class="card" aria-labelledby="stor-h">
|
|
50
|
+
<div class="card-head">
|
|
51
|
+
<h2 id="stor-h">Storage map</h2>
|
|
52
|
+
<button id="rescan" class="btn" type="button" title="Measure the folders shown here again">Rescan</button>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="scan-progress" id="scan-progress" hidden aria-live="polite">
|
|
55
|
+
<div class="meter-track"><div class="meter-fill" id="scan-progress-fill" style="width:0%"></div></div>
|
|
56
|
+
<p class="muted small" id="scan-progress-label"></p>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="rootbar" id="rootbar" role="tablist" aria-label="Storage root"></div>
|
|
59
|
+
<form class="pinform" id="pinform" hidden>
|
|
60
|
+
<input id="pinform-input" type="text" placeholder="/mnt/data/media/movies" autocomplete="off" spellcheck="false">
|
|
61
|
+
<button class="btn" type="submit">Go</button>
|
|
62
|
+
<button class="btn btn-quiet" type="button" id="pinform-cancel">Cancel</button>
|
|
63
|
+
<span class="pinform-err" id="pinform-err" role="alert"></span>
|
|
64
|
+
</form>
|
|
65
|
+
<nav class="crumbs" id="crumbs" aria-label="Breadcrumb"></nav>
|
|
66
|
+
<div class="treemap-wrap">
|
|
67
|
+
<svg id="treemap" role="img" aria-labelledby="stor-h" preserveAspectRatio="none"></svg>
|
|
68
|
+
</div>
|
|
69
|
+
<p id="tm-focus" class="focusline muted small" aria-live="polite">Tap a block to drill in.</p>
|
|
70
|
+
<div class="tm-bar">
|
|
71
|
+
<ul class="legend">
|
|
72
|
+
<li><span class="g">▸</span>folder</li>
|
|
73
|
+
<li><span class="g">·</span>file</li>
|
|
74
|
+
<li><span class="g">⋯</span>aggregated</li>
|
|
75
|
+
</ul>
|
|
76
|
+
<button class="chip" id="show-files" type="button" aria-pressed="false">Show files</button>
|
|
77
|
+
</div>
|
|
78
|
+
<table class="tbl" id="stor-tbl">
|
|
79
|
+
<caption class="visually-hidden">Contents of the selected directory by size</caption>
|
|
80
|
+
<thead><tr><th scope="col">Name</th><th scope="col" class="num">Size</th><th scope="col" class="num">Share</th></tr></thead>
|
|
81
|
+
<tbody></tbody>
|
|
82
|
+
</table>
|
|
83
|
+
<div class="more" id="stor-more" hidden>
|
|
84
|
+
<span class="muted small" id="stor-count"></span>
|
|
85
|
+
<button class="btn" id="stor-more-btn" type="button">Show more</button>
|
|
86
|
+
<button class="btn" id="stor-all-btn" type="button">Show all</button>
|
|
87
|
+
<button class="btn btn-quiet" id="stor-less-btn" type="button">Show fewer</button>
|
|
88
|
+
</div>
|
|
89
|
+
<p id="stor-meta" class="muted small"></p>
|
|
90
|
+
</section>
|
|
91
|
+
|
|
92
|
+
<!-- ───────── Containers ───────── -->
|
|
93
|
+
<section class="card" aria-labelledby="ctr-h">
|
|
94
|
+
<div class="card-head">
|
|
95
|
+
<h2 id="ctr-h">Containers</h2>
|
|
96
|
+
<span id="ctr-meta" class="muted small">—</span>
|
|
97
|
+
</div>
|
|
98
|
+
<p id="ctr-empty" class="muted small" hidden>Docker was not found on this machine. Containers appear here once it is running.</p>
|
|
99
|
+
<div class="sortbar" role="group" aria-label="Sort containers">
|
|
100
|
+
<button class="chip is-on" data-sort="cpu" type="button">CPU</button>
|
|
101
|
+
<button class="chip" data-sort="mem" type="button">Memory</button>
|
|
102
|
+
<button class="chip" data-sort="net" type="button">Network</button>
|
|
103
|
+
<button class="chip" data-sort="name" type="button">Name</button>
|
|
104
|
+
</div>
|
|
105
|
+
<table class="tbl ctr-tbl" id="ctr-tbl">
|
|
106
|
+
<caption class="visually-hidden">Per-container CPU, memory and network</caption>
|
|
107
|
+
<thead><tr>
|
|
108
|
+
<th scope="col">Container</th>
|
|
109
|
+
<th scope="col" class="num">CPU</th>
|
|
110
|
+
<th scope="col" class="num">Memory</th>
|
|
111
|
+
<th scope="col" class="num">Net ↓↑</th>
|
|
112
|
+
</tr></thead>
|
|
113
|
+
<tbody></tbody>
|
|
114
|
+
</table>
|
|
115
|
+
</section>
|
|
116
|
+
|
|
117
|
+
<footer class="foot muted small">
|
|
118
|
+
<span id="foot-meta">—</span>
|
|
119
|
+
<span class="foot-actions">
|
|
120
|
+
<button id="access-open" class="btn btn-quiet" type="button" aria-haspopup="dialog">Network</button>
|
|
121
|
+
<button id="theme" class="btn btn-quiet" type="button" aria-label="Toggle colour theme">Theme</button>
|
|
122
|
+
</span>
|
|
123
|
+
</footer>
|
|
124
|
+
<p id="access-line" class="access-line muted small" aria-live="polite"></p>
|
|
125
|
+
</main>
|
|
126
|
+
|
|
127
|
+
<dialog id="access-dlg" class="dlg" aria-labelledby="access-h">
|
|
128
|
+
<form method="dialog" id="access-form">
|
|
129
|
+
<h2 id="access-h">Who can open this dashboard</h2>
|
|
130
|
+
<p class="muted small">This computer can always open it, at localhost.</p>
|
|
131
|
+
<fieldset id="access-opts">
|
|
132
|
+
<legend class="visually-hidden">Also allow</legend>
|
|
133
|
+
<label><input type="checkbox" name="lan"><span><b>Local network</b><span class="muted small">Phones and computers on the same Wi-Fi or wired network.</span></span></label>
|
|
134
|
+
<label><input type="checkbox" name="tailscale"><span><b>Tailscale</b><span class="muted small">Your devices on your tailnet, from anywhere.</span></span></label>
|
|
135
|
+
<label><input type="checkbox" name="all"><span><b>Every network</b><span class="muted small">⚠ Every interface, public ones included. Kanshi has no login, so only use this behind a firewall or a proxy that asks for one.</span></span></label>
|
|
136
|
+
</fieldset>
|
|
137
|
+
<p id="access-extra" class="muted small" hidden></p>
|
|
138
|
+
<ul id="access-links" class="links" aria-label="Open it at"></ul>
|
|
139
|
+
<p id="access-reason" class="muted small"></p>
|
|
140
|
+
<p id="access-err" class="err small" role="alert"></p>
|
|
141
|
+
<div class="dlg-actions">
|
|
142
|
+
<button class="btn btn-quiet" value="close" type="submit">Close</button>
|
|
143
|
+
<button id="access-save" class="btn btn-primary" type="button">Save</button>
|
|
144
|
+
</div>
|
|
145
|
+
</form>
|
|
146
|
+
</dialog>
|
|
147
|
+
|
|
148
|
+
<script src="/static/treemap.js"></script>
|
|
149
|
+
<script src="/static/app.js"></script>
|
|
150
|
+
</body>
|
|
151
|
+
</html>
|
package/web/style.css
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/* ── Palette roles ───────────────────────────────────────────────────────────
|
|
2
|
+
Values from the validated reference palette. Categorical slots in use: two
|
|
3
|
+
(blue = folder, orange = file); validated all-pairs in both modes, so the
|
|
4
|
+
treemap is safe under every CVD simulation. Everything else is ink, surface
|
|
5
|
+
or a reserved status token. */
|
|
6
|
+
.viz-root {
|
|
7
|
+
color-scheme: light;
|
|
8
|
+
--plane: #f9f9f7;
|
|
9
|
+
--surface-1: #fcfcfb;
|
|
10
|
+
--text-primary: #0b0b0b;
|
|
11
|
+
--text-secondary: #52514e;
|
|
12
|
+
--text-muted: #898781;
|
|
13
|
+
--gridline: #e1e0d9;
|
|
14
|
+
--baseline: #c3c2b7;
|
|
15
|
+
--border: rgba(11, 11, 11, 0.10);
|
|
16
|
+
|
|
17
|
+
--series-1: #2a78d6; /* the one categorical slot in use */
|
|
18
|
+
--neutral-mark: #c3c2b7; /* aggregated "other" — deliberately not a slot */
|
|
19
|
+
--tile: #1c5cab; /* blue ramp step 550; white label = 7.1:1 */
|
|
20
|
+
--tile-ink: #ffffff;
|
|
21
|
+
--tile-rest-ink: #0b0b0b;
|
|
22
|
+
|
|
23
|
+
--status-good: #0ca30c;
|
|
24
|
+
--status-warning: #fab219;
|
|
25
|
+
--status-critical: #d03b3b;
|
|
26
|
+
|
|
27
|
+
/* Meter tracks: a lighter step of the fill's own ramp, so state reads across
|
|
28
|
+
the whole bar rather than only the filled part. */
|
|
29
|
+
--track-ok: #cde2fb;
|
|
30
|
+
--track-warn: #fdeac4;
|
|
31
|
+
--track-crit: #f4d2d2;
|
|
32
|
+
--ghost: rgba(11, 11, 11, 0.05);
|
|
33
|
+
}
|
|
34
|
+
@media (prefers-color-scheme: dark) {
|
|
35
|
+
:root:where(:not([data-theme="light"])) .viz-root {
|
|
36
|
+
color-scheme: dark;
|
|
37
|
+
--plane: #0d0d0d;
|
|
38
|
+
--surface-1: #1a1a19;
|
|
39
|
+
--text-primary: #ffffff;
|
|
40
|
+
--text-secondary: #c3c2b7;
|
|
41
|
+
--text-muted: #898781;
|
|
42
|
+
--gridline: #2c2c2a;
|
|
43
|
+
--baseline: #383835;
|
|
44
|
+
--border: rgba(255, 255, 255, 0.10);
|
|
45
|
+
--series-1: #3987e5;
|
|
46
|
+
--neutral-mark: #55544f;
|
|
47
|
+
--tile: #256abf;
|
|
48
|
+
--tile-ink: #ffffff;
|
|
49
|
+
--tile-rest-ink: #ffffff;
|
|
50
|
+
--track-ok: #184f95;
|
|
51
|
+
--track-warn: #5c410a;
|
|
52
|
+
--track-crit: #4f1a1a;
|
|
53
|
+
--ghost: rgba(255, 255, 255, 0.06);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
:root[data-theme="dark"] .viz-root {
|
|
57
|
+
color-scheme: dark;
|
|
58
|
+
--plane: #0d0d0d;
|
|
59
|
+
--surface-1: #1a1a19;
|
|
60
|
+
--text-primary: #ffffff;
|
|
61
|
+
--text-secondary: #c3c2b7;
|
|
62
|
+
--text-muted: #898781;
|
|
63
|
+
--gridline: #2c2c2a;
|
|
64
|
+
--baseline: #383835;
|
|
65
|
+
--border: rgba(255, 255, 255, 0.10);
|
|
66
|
+
--series-1: #3987e5;
|
|
67
|
+
--neutral-mark: #55544f;
|
|
68
|
+
--tile: #256abf;
|
|
69
|
+
--tile-ink: #ffffff;
|
|
70
|
+
--tile-rest-ink: #ffffff;
|
|
71
|
+
--track-ok: #184f95;
|
|
72
|
+
--track-warn: #5c410a;
|
|
73
|
+
--track-crit: #4f1a1a;
|
|
74
|
+
--ghost: rgba(255, 255, 255, 0.06);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
* { box-sizing: border-box; }
|
|
78
|
+
|
|
79
|
+
html { -webkit-text-size-adjust: 100%; }
|
|
80
|
+
|
|
81
|
+
body {
|
|
82
|
+
margin: 0;
|
|
83
|
+
padding: 0 0 env(safe-area-inset-bottom);
|
|
84
|
+
background: var(--plane);
|
|
85
|
+
color: var(--text-primary);
|
|
86
|
+
font: 15px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
87
|
+
transition: opacity .25s ease;
|
|
88
|
+
}
|
|
89
|
+
body.is-stale { opacity: .55; } /* hold the last render, never a skeleton */
|
|
90
|
+
|
|
91
|
+
.visually-hidden {
|
|
92
|
+
position: absolute; width: 1px; height: 1px; overflow: hidden;
|
|
93
|
+
clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* ── Top bar ─────────────────────────────────────────────────────────────── */
|
|
97
|
+
.topbar {
|
|
98
|
+
position: sticky; top: 0; z-index: 10;
|
|
99
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
100
|
+
gap: 12px; padding: 10px 16px calc(10px + env(safe-area-inset-top)) 16px;
|
|
101
|
+
padding-top: calc(10px + env(safe-area-inset-top));
|
|
102
|
+
background: color-mix(in srgb, var(--plane) 88%, transparent);
|
|
103
|
+
backdrop-filter: blur(10px);
|
|
104
|
+
border-bottom: 1px solid var(--border);
|
|
105
|
+
}
|
|
106
|
+
.topbar h1 {
|
|
107
|
+
margin: 0; font-size: 15px; font-weight: 650; letter-spacing: .14em;
|
|
108
|
+
text-transform: uppercase; color: var(--text-secondary);
|
|
109
|
+
}
|
|
110
|
+
.topbar-meta { display: flex; align-items: center; gap: 12px; }
|
|
111
|
+
|
|
112
|
+
.conn { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-muted); }
|
|
113
|
+
.conn i {
|
|
114
|
+
width: 7px; height: 7px; border-radius: 50%;
|
|
115
|
+
background: var(--text-muted); flex: none;
|
|
116
|
+
}
|
|
117
|
+
.conn.live i { background: var(--status-good); }
|
|
118
|
+
.conn.down i { background: var(--status-critical); }
|
|
119
|
+
|
|
120
|
+
main { max-width: 760px; margin: 0 auto; padding: 12px 12px 28px; display: grid; gap: 12px; min-width: 0; }
|
|
121
|
+
|
|
122
|
+
/* ── Cards ───────────────────────────────────────────────────────────────── */
|
|
123
|
+
.card {
|
|
124
|
+
background: var(--surface-1);
|
|
125
|
+
border: 1px solid var(--border);
|
|
126
|
+
border-radius: 14px;
|
|
127
|
+
padding: 14px;
|
|
128
|
+
/* A grid item's automatic minimum size is its content, so without this a
|
|
129
|
+
single long path inside the card widens the page instead of truncating. */
|
|
130
|
+
min-width: 0;
|
|
131
|
+
}
|
|
132
|
+
.card-head {
|
|
133
|
+
display: flex; align-items: baseline; justify-content: space-between;
|
|
134
|
+
gap: 10px; margin-bottom: 12px;
|
|
135
|
+
}
|
|
136
|
+
.card-head h2 {
|
|
137
|
+
margin: 0; font-size: 12px; font-weight: 650; letter-spacing: .1em;
|
|
138
|
+
text-transform: uppercase; color: var(--text-secondary);
|
|
139
|
+
}
|
|
140
|
+
.muted { color: var(--text-muted); }
|
|
141
|
+
.small { font-size: 12px; }
|
|
142
|
+
|
|
143
|
+
/* ── Hero figure (exactly one on the page) ───────────────────────────────── */
|
|
144
|
+
.hero-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
|
|
145
|
+
.hero {
|
|
146
|
+
font-size: 56px; line-height: 1; font-weight: 620;
|
|
147
|
+
letter-spacing: -.02em; color: var(--text-primary);
|
|
148
|
+
font-variant-numeric: proportional-nums; /* never tabular at display size */
|
|
149
|
+
}
|
|
150
|
+
.hero-unit { font-size: 22px; font-weight: 500; color: var(--text-muted); margin-left: 3px; }
|
|
151
|
+
.hero-label { margin-top: 6px; }
|
|
152
|
+
|
|
153
|
+
.minitable { margin: 0; display: grid; grid-template-columns: auto auto; gap: 3px 12px; align-content: start; }
|
|
154
|
+
.minitable dt { color: var(--text-muted); font-size: 12px; }
|
|
155
|
+
.minitable dd { margin: 0; font-size: 12px; text-align: right; font-variant-numeric: tabular-nums; color: var(--text-secondary); }
|
|
156
|
+
|
|
157
|
+
/* Per-core bars: nominal categories, so one hue for all of them — the height
|
|
158
|
+
already carries the value and a ramp would double-encode it. */
|
|
159
|
+
.cores { display: flex; align-items: stretch; gap: 2px; height: 34px; margin-top: 14px; }
|
|
160
|
+
.cores div { flex: 1; background: var(--track-ok); border-radius: 2px; position: relative; overflow: hidden; }
|
|
161
|
+
.cores div span {
|
|
162
|
+
position: absolute; left: 0; right: 0; bottom: 0;
|
|
163
|
+
background: var(--series-1); border-radius: 2px;
|
|
164
|
+
transition: height .3s ease;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* ── Meters ──────────────────────────────────────────────────────────────── */
|
|
168
|
+
.meters { display: grid; gap: 13px; }
|
|
169
|
+
.meter-head {
|
|
170
|
+
display: flex; justify-content: space-between; align-items: baseline;
|
|
171
|
+
gap: 8px; margin-bottom: 5px; font-size: 13px;
|
|
172
|
+
}
|
|
173
|
+
.meter-name { color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
174
|
+
.meter-val { color: var(--text-primary); font-variant-numeric: tabular-nums; flex: none; font-size: 12px; }
|
|
175
|
+
.meter-val b { font-weight: 620; }
|
|
176
|
+
.meter-track { height: 9px; border-radius: 5px; background: var(--track-ok); overflow: hidden; }
|
|
177
|
+
.meter-fill { height: 100%; border-radius: 5px; background: var(--series-1); transition: width .35s ease; }
|
|
178
|
+
.meter.warn .meter-track { background: var(--track-warn); }
|
|
179
|
+
.meter.warn .meter-fill { background: var(--status-warning); }
|
|
180
|
+
.meter.crit .meter-track { background: var(--track-crit); }
|
|
181
|
+
.meter.crit .meter-fill { background: var(--status-critical); }
|
|
182
|
+
/* Status hue never carries meaning alone: the % and an icon ride alongside. */
|
|
183
|
+
.meter .flag { font-size: 11px; margin-right: 4px; }
|
|
184
|
+
.meter.ok .flag { display: none; }
|
|
185
|
+
|
|
186
|
+
/* ── Buttons & chips ─────────────────────────────────────────────────────── */
|
|
187
|
+
.btn {
|
|
188
|
+
font: inherit; font-size: 12px; padding: 6px 12px; min-height: 32px;
|
|
189
|
+
border-radius: 8px; border: 1px solid var(--border);
|
|
190
|
+
background: var(--ghost); color: var(--text-secondary); cursor: pointer;
|
|
191
|
+
}
|
|
192
|
+
.btn:active { transform: translateY(1px); }
|
|
193
|
+
.btn[disabled] { opacity: .5; cursor: default; }
|
|
194
|
+
.btn-quiet { border-color: transparent; background: transparent; }
|
|
195
|
+
|
|
196
|
+
.scan-progress { margin-bottom: 10px; }
|
|
197
|
+
.scan-progress p { margin: 5px 0 0; }
|
|
198
|
+
|
|
199
|
+
.rootbar, .sortbar { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 10px; }
|
|
200
|
+
.sortbar[hidden] { display: none; }
|
|
201
|
+
.chip {
|
|
202
|
+
font: inherit; font-size: 12px; padding: 5px 11px; min-height: 30px;
|
|
203
|
+
border-radius: 999px; border: 1px solid var(--border);
|
|
204
|
+
background: transparent; color: var(--text-muted); cursor: pointer;
|
|
205
|
+
}
|
|
206
|
+
.chip.is-on { background: var(--series-1); border-color: transparent; color: #fff; }
|
|
207
|
+
/* Paths can be arbitrarily long; a wrapping row still can't shrink a single
|
|
208
|
+
over-wide item, so each chip truncates at the container's width. */
|
|
209
|
+
.rootbar .chip { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
210
|
+
|
|
211
|
+
/* Pinned custom paths: a chip made of two buttons (jump, remove) rather than
|
|
212
|
+
one, so the base .chip padding has to move onto the inner buttons. */
|
|
213
|
+
.chip.pin { display: inline-flex; align-items: center; gap: 0; padding: 0; max-width: 100%; }
|
|
214
|
+
.chip.pin .pin-go {
|
|
215
|
+
font: inherit; font-size: 12px; padding: 5px 4px 5px 11px; border: 0;
|
|
216
|
+
background: transparent; color: var(--text-muted); cursor: pointer;
|
|
217
|
+
max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
218
|
+
}
|
|
219
|
+
.chip.pin .pin-x {
|
|
220
|
+
font: inherit; font-size: 13px; line-height: 1; padding: 5px 10px 5px 4px;
|
|
221
|
+
border: 0; background: transparent; color: var(--text-muted); cursor: pointer;
|
|
222
|
+
}
|
|
223
|
+
.chip.pin .pin-x:hover { color: var(--status-critical); }
|
|
224
|
+
.chip.pin-add { cursor: pointer; }
|
|
225
|
+
|
|
226
|
+
.pinform { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: -2px 0 10px; }
|
|
227
|
+
.pinform[hidden] { display: none; }
|
|
228
|
+
.pinform input {
|
|
229
|
+
font: inherit; font-size: 13px; flex: 1 1 220px; min-width: 0;
|
|
230
|
+
padding: 6px 10px; border-radius: 8px; border: 1px solid var(--border);
|
|
231
|
+
background: var(--surface-1); color: var(--text-primary);
|
|
232
|
+
}
|
|
233
|
+
.pinform-err { flex: 1 0 100%; font-size: 12px; color: var(--status-critical); }
|
|
234
|
+
|
|
235
|
+
/* ── Breadcrumb ──────────────────────────────────────────────────────────── */
|
|
236
|
+
.crumbs { display: flex; flex-wrap: wrap; align-items: center; gap: 2px; margin-bottom: 8px; font-size: 12px; }
|
|
237
|
+
.crumbs button {
|
|
238
|
+
font: inherit; font-size: 12px; background: none; border: 0; padding: 4px 5px;
|
|
239
|
+
color: var(--series-1); cursor: pointer; border-radius: 5px; min-height: 28px;
|
|
240
|
+
max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
241
|
+
}
|
|
242
|
+
.crumbs button[disabled] { color: var(--text-muted); cursor: default; }
|
|
243
|
+
.crumbs .sep { color: var(--text-muted); }
|
|
244
|
+
|
|
245
|
+
/* ── Treemap ─────────────────────────────────────────────────────────────── */
|
|
246
|
+
.treemap-wrap { width: 100%; }
|
|
247
|
+
#treemap { display: block; width: 100%; height: 300px; border-radius: 10px; overflow: hidden; touch-action: manipulation; }
|
|
248
|
+
#treemap .tile { cursor: pointer; outline: none; }
|
|
249
|
+
#treemap .tile rect { transition: opacity .15s ease; }
|
|
250
|
+
#treemap .tile:hover rect, #treemap .tile:focus-visible rect { opacity: .78; }
|
|
251
|
+
#treemap .tile:focus-visible rect { stroke: var(--text-primary); stroke-width: 2px; }
|
|
252
|
+
#treemap text { pointer-events: none; font-family: inherit; }
|
|
253
|
+
.focusline { margin: 8px 0 0; min-height: 1.45em; overflow-wrap: anywhere; }
|
|
254
|
+
#stor-meta { overflow-wrap: anywhere; }
|
|
255
|
+
|
|
256
|
+
.tm-bar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px; margin-top: 9px; }
|
|
257
|
+
.legend { display: flex; gap: 14px; list-style: none; margin: 0; padding: 0; font-size: 12px; color: var(--text-muted); }
|
|
258
|
+
.legend li { display: flex; align-items: center; gap: 5px; }
|
|
259
|
+
.sw { width: 10px; height: 10px; border-radius: 3px; display: inline-block; }
|
|
260
|
+
.sw-dir { background: var(--tile); } .sw-file { background: var(--tile); opacity: .55; }
|
|
261
|
+
.sw-rest { background: var(--neutral-mark); }
|
|
262
|
+
/* Kind is carried by the glyph, not the fill — one hue means no CVD exposure. */
|
|
263
|
+
.legend .g { font-size: 12px; color: var(--text-muted); width: 10px; text-align: center; }
|
|
264
|
+
|
|
265
|
+
/* ── Tables (the WCAG-clean twin of every chart on the page) ─────────────── */
|
|
266
|
+
.tbl { width: 100%; border-collapse: collapse; margin-top: 12px; font-size: 13px; }
|
|
267
|
+
.tbl th {
|
|
268
|
+
text-align: left; font-weight: 500; font-size: 11px; letter-spacing: .06em;
|
|
269
|
+
text-transform: uppercase; color: var(--text-muted);
|
|
270
|
+
padding: 6px 4px; border-bottom: 1px solid var(--gridline);
|
|
271
|
+
}
|
|
272
|
+
.tbl td { padding: 9px 4px; border-bottom: 1px solid var(--gridline); vertical-align: middle; }
|
|
273
|
+
.tbl tr:last-child td { border-bottom: 0; }
|
|
274
|
+
.tbl .num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
275
|
+
.tbl tbody tr.tap { cursor: pointer; }
|
|
276
|
+
.tbl tbody tr.tap:hover td { background: var(--ghost); }
|
|
277
|
+
#stor-tbl td:first-child, #stor-tbl th:first-child { max-width: 0; width: 60%; }
|
|
278
|
+
.more { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 6px; margin-top: 8px; }
|
|
279
|
+
.more[hidden], .more .btn[hidden] { display: none; }
|
|
280
|
+
.more .muted { margin-right: auto; }
|
|
281
|
+
.name-cell { display: flex; align-items: center; gap: 7px; min-width: 0; }
|
|
282
|
+
.name-cell .sw { flex: none; }
|
|
283
|
+
.name-cell .g { flex: none; width: 10px; text-align: center; color: var(--text-muted); }
|
|
284
|
+
.name-cell span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
285
|
+
|
|
286
|
+
/* ── Container rows ──────────────────────────────────────────────────────── */
|
|
287
|
+
.ctr-tbl td:first-child { max-width: 0; width: 45%; }
|
|
288
|
+
.ctr-name { font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
289
|
+
.ctr-sub { font-size: 11px; color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
290
|
+
.ctr-bar { height: 3px; border-radius: 2px; background: var(--track-ok); margin-top: 5px; overflow: hidden; }
|
|
291
|
+
.ctr-bar i { display: block; height: 100%; background: var(--series-1); border-radius: 2px; transition: width .3s ease; }
|
|
292
|
+
/* Port mappings. These sit in the same quiet register as .ctr-sub above: 11px,
|
|
293
|
+
dot-separated, no box. Pills are the page's vocabulary for controls (sortbar,
|
|
294
|
+
rootbar, pins), so borrowing that shape for row metadata would both add
|
|
295
|
+
chrome and promise a click most of these do not have. Linkness is carried by
|
|
296
|
+
an underline instead, which needs no hue to read. */
|
|
297
|
+
.ctr-ports {
|
|
298
|
+
font-size: 11px; line-height: 1.5; margin-top: 3px; color: var(--text-secondary);
|
|
299
|
+
font-variant-numeric: tabular-nums;
|
|
300
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
301
|
+
}
|
|
302
|
+
/* Only the separator is dimmed to --baseline, and only because it is pure
|
|
303
|
+
punctuation. Everything carrying a number stays at --text-secondary: at 11px
|
|
304
|
+
these are small text, and --text-muted would put them at 3.5:1 in light mode.
|
|
305
|
+
Hierarchy comes from the underline instead, which costs no contrast. */
|
|
306
|
+
.ctr-ports .sep { font-style: normal; color: var(--baseline); padding: 0 5px; }
|
|
307
|
+
.ctr-ports .arr { font-style: normal; padding: 0 1px; }
|
|
308
|
+
.ctr-ports .pr { font-style: normal; padding-left: 3px; }
|
|
309
|
+
.ctr-ports a {
|
|
310
|
+
color: var(--text-primary); text-decoration: underline;
|
|
311
|
+
text-decoration-color: var(--baseline); text-underline-offset: 2px;
|
|
312
|
+
}
|
|
313
|
+
.ctr-ports a:hover { text-decoration-color: var(--series-1); }
|
|
314
|
+
.ctr-ports a:focus-visible { outline: 2px solid var(--series-1); outline-offset: 2px; border-radius: 2px; }
|
|
315
|
+
|
|
316
|
+
.dot { width: 6px; height: 6px; border-radius: 50%; display: inline-block; flex: none; }
|
|
317
|
+
.dot.run { background: var(--status-good); } .dot.stop { background: var(--text-muted); }
|
|
318
|
+
.dot.bad { background: var(--status-critical); }
|
|
319
|
+
tr.is-stopped { opacity: .55; }
|
|
320
|
+
|
|
321
|
+
.foot { display: flex; justify-content: space-between; align-items: center; padding: 0 4px; }
|
|
322
|
+
.foot-actions { display: flex; gap: 2px; }
|
|
323
|
+
.access-line { margin: -8px 4px 0; overflow-wrap: anywhere; }
|
|
324
|
+
.access-line:empty { display: none; }
|
|
325
|
+
|
|
326
|
+
/* ── Network dialog ──────────────────────────────────────────────────────── */
|
|
327
|
+
.dlg {
|
|
328
|
+
width: min(430px, calc(100vw - 24px)); padding: 18px;
|
|
329
|
+
border: 1px solid var(--border); border-radius: 14px;
|
|
330
|
+
background: var(--surface-1); color: var(--text-primary);
|
|
331
|
+
}
|
|
332
|
+
.dlg::backdrop { background: rgba(0, 0, 0, .4); }
|
|
333
|
+
.dlg h2 { margin: 0 0 4px; font-size: 15px; font-weight: 620; }
|
|
334
|
+
.dlg p { margin: 6px 0; }
|
|
335
|
+
.dlg fieldset { border: 0; padding: 0; margin: 14px 0 10px; display: grid; gap: 12px; }
|
|
336
|
+
.dlg fieldset[disabled] { opacity: .6; }
|
|
337
|
+
.dlg label { display: flex; gap: 10px; align-items: flex-start; font-size: 13px; cursor: pointer; }
|
|
338
|
+
.dlg label > span { display: grid; gap: 1px; }
|
|
339
|
+
.dlg label b { font-weight: 600; }
|
|
340
|
+
.dlg input[type="checkbox"] { margin: 2px 0 0; width: 16px; height: 16px; flex: none; accent-color: var(--series-1); }
|
|
341
|
+
.dlg .links { list-style: none; margin: 10px 0 0; padding: 10px 0 0; border-top: 1px solid var(--gridline); display: grid; gap: 4px; font-size: 12px; }
|
|
342
|
+
.dlg .links:empty { display: none; }
|
|
343
|
+
.dlg .links a { color: var(--series-1); overflow-wrap: anywhere; }
|
|
344
|
+
.dlg .links .k { display: inline-block; min-width: 96px; margin-right: 8px; color: var(--text-muted); }
|
|
345
|
+
.dlg-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 14px; }
|
|
346
|
+
.btn-primary { background: var(--series-1); border-color: transparent; color: #fff; }
|
|
347
|
+
.err { margin: 0; color: var(--status-critical); }
|
|
348
|
+
.err:empty { display: none; }
|
|
349
|
+
|
|
350
|
+
@media (prefers-reduced-motion: reduce) {
|
|
351
|
+
* { transition: none !important; animation: none !important; }
|
|
352
|
+
}
|
|
353
|
+
@media (min-width: 560px) {
|
|
354
|
+
#treemap { height: 380px; }
|
|
355
|
+
.hero { font-size: 64px; }
|
|
356
|
+
}
|