Diff -- /var/www/vhosts/3dshawn.com/admin.3dshawn.com/server.cgi
Diff

/var/www/vhosts/3dshawn.com/admin.3dshawn.com/server.cgi

added on local at 2026-07-10 10:51:46

Added
+797
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 1cde422f1d6c
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
1#!/usr/bin/perl
2#======================================================================
3# Meta-Admin -- /server.cgi
4#
5# Live disk + memory + cpu + per-vhost footprint, with hand-rolled
6# SVG visuals. No JS frameworks, no charting libs. Just SVG primitives
7# plus the synthwave palette.
8#
9# Graphs on this page:
10# 1. Disk donut -- used vs avail on the root partition
11# 2. Per-vhost bars -- du -sb per configured vhost_path
12# 3. Load gauge -- semi-circle for 1-minute load vs CPU cores
13# 4. Memory bars -- used / cached / free split
14# 5. Tile row -- uptime, load 1/5/15, cores, kernel, OS
15# 6. Top processes -- ps top 5 by CPU
16#======================================================================
17use strict;
18use warnings;
19use lib '/var/www/vhosts/3dshawn.com/admin.3dshawn.com';
20use MODS::Login;
21use MODS::DBConnect;
22use MODS::MetaAdmin::Config;
23use MODS::MetaAdmin::Wrapper;
24use MODS::MetaAdmin::Stats;
25use MODS::MetaAdmin::Aggregate;
26
27my $auth = MODS::Login->new;
28my $u = $auth->login_verify;
29unless ($u) { print "Status: 302 Found\nLocation: /login.cgi\n\n"; exit; }
30
31my $stats = MODS::MetaAdmin::Stats->new;
32my $disk = $stats->disk_for('/var'); # portfolio lives here, not on /
33my $mem = $stats->memory;
34my $cpu = $stats->cpu;
35my $host = $stats->host;
36my $tops = $stats->top_processes;
37my $parts = $stats->partitions;
38my $pdisk = $stats->physical_disks;
39my $varbd = $stats->var_breakdown;
40
41# Pull site list from local DB for the per-vhost disk pass.
42my $db = MODS::DBConnect->new;
43my $dbh = $db->db_connect;
44my @sites = $db->db_readwrite_multiple($dbh, q~
45 SELECT slug, display_name, brand_color, vhost_path
46 FROM site_connections
47 WHERE vhost_path IS NOT NULL AND vhost_path<>''
48 ORDER BY sort_order, id
49~, __FILE__, __LINE__);
50$db->db_disconnect($dbh);
51
52my $vhost_disk = $stats->disk_per_vhost(\@sites);
53my $vhost_total = 0;
54$vhost_total += $_->{used_b} for @$vhost_disk;
55
56my $footprints = $stats->site_footprints(\@sites);
57my $db_sizes = MODS::MetaAdmin::Aggregate->new->db_sizes;
58my $sites_disk_total = 0;
59my $unreachable_ct = 0;
60my %ram_by_owner_seen;
61foreach my $f (@$footprints) {
62 if ($f->{unreachable}) { $unreachable_ct++; next; }
63 $sites_disk_total += ($f->{disk_b} || 0);
64 # De-dupe RAM by owner user so shared pools don't get counted N times.
65 if ($f->{owner_user}) {
66 $ram_by_owner_seen{$f->{owner_user}} = ($f->{ram_b} || 0);
67 }
68}
69my $sites_ram_total = 0;
70$sites_ram_total += $_ for values %ram_by_owner_seen;
71my $var_stats = $stats->disk_for('/var');
72my $var_avail_b = $var_stats->{avail_b} || 0;
73my $var_total_b = $var_stats->{total_b} || 0;
74my $mem_avail_b = $mem->{avail_b} || 0;
75my $mem_total_b = $mem->{total_b} || 0;
76
77sub _esc {
78 my $s = shift; $s //= '';
79 $s =~ s/&/&amp;/g; $s =~ s/</&lt;/g; $s =~ s/>/&gt;/g;
80 $s =~ s/"/&quot;/g;
81 return $s;
82}
83
84#---------------------------------------------------------------------
85# SVG helpers (hand-rolled -- no charting library)
86#---------------------------------------------------------------------
87
88# Polar-to-cartesian -- used by donut + gauge arc math.
89sub _polar {
90 my ($cx, $cy, $r, $deg) = @_;
91 my $rad = ($deg - 90) * 3.14159265 / 180;
92 return ($cx + $r * cos($rad), $cy + $r * sin($rad));
93}
94
95# Donut chart. $values is arrayref of { v, color, label }. Total
96# computed automatically. Renders a single-ring chart.
97sub svg_donut {
98 my (%a) = @_;
99 my $size = $a{size} || 200;
100 my $values = $a{values} || [];
101 my $center_label = $a{center_label} || '';
102 my $center_sub = $a{center_sub} || '';
103 my $thickness = $a{thickness} || 22;
104
105 my $cx = $size / 2;
106 my $cy = $size / 2;
107 my $r = ($size / 2) - ($thickness / 2) - 4;
108
109 my $total = 0;
110 foreach my $v (@$values) { $total += $v->{v} || 0; }
111 $total = 1 unless $total > 0;
112
113 my $svg = qq~<svg viewBox="0 0 $size $size" width="$size" height="$size" style="overflow:visible">~;
114 $svg .= qq~<defs>~;
115 foreach my $i (0 .. $#$values) {
116 my $color = $values->[$i]->{color} || '#807aa8';
117 $svg .= qq~<filter id="donutGlow$i"><feGaussianBlur stdDeviation="3"/><feMerge><feMergeNode/><feMergeNode in="SourceGraphic"/></feMerge></filter>~;
118 }
119 $svg .= qq~</defs>~;
120
121 # Background ring
122 $svg .= qq~<circle cx="$cx" cy="$cy" r="$r" stroke="#0d0a07" stroke-width="$thickness" fill="none"/>~;
123
124 my $start = 0;
125 foreach my $i (0 .. $#$values) {
126 my $v = $values->[$i];
127 my $angle = ($v->{v} / $total) * 360;
128 next if $angle < 0.5;
129 my $end = $start + $angle;
130 my $large = ($angle > 180) ? 1 : 0;
131 my ($x1, $y1) = _polar($cx, $cy, $r, $start);
132 my ($x2, $y2) = _polar($cx, $cy, $r, $end);
133 my $color = $v->{color} || '#92400e';
134 $svg .= qq~<path d="M $x1 $y1 A $r $r 0 $large 1 $x2 $y2" stroke="$color" stroke-width="$thickness" fill="none" stroke-linecap="butt" filter="url(#donutGlow$i)" opacity="0.95"/>~;
135 $start = $end;
136 }
137
138 # Center text
139 my $cl = _esc($center_label); my $cs = _esc($center_sub);
140 $svg .= qq~<text x="$cx" y="$cy" text-anchor="middle" dy="2" font-family="'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-weight="600" font-size="24" font-weight="700" font-weight="800" fill="#f0f0ff">$cl</text>~;
141 $svg .= qq~<text x="$cx" y="$cy" text-anchor="middle" dy="22" font-family="'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-weight="600" font-size="11" fill="#807aa8" letter-spacing="1.5px">$cs</text>~;
142
143 $svg .= qq~</svg>~;
144 return $svg;
145}
146
147# Horizontal bar chart. $rows is arrayref of { label, value, color }.
148# Renders one bar per row, padded labels at left, value at right.
149sub svg_hbars {
150 # HTML bar chart (kept the name for callers). Labels are real HTML
151 # text so they inherit the page font exactly -- SVG text never matches
152 # HTML font hinting. Plate rendered when `letter` is in the row.
153 my (%a) = @_;
154 my $rows = $a{rows} || [];
155 my $max = 0;
156 foreach my $r (@$rows) { $max = $r->{value} if ($r->{value} || 0) > $max; }
157 $max = 1 unless $max > 0;
158 my $html = '<div class="hbar-chart">';
159 foreach my $r (@$rows) {
160 my $val = $r->{value} || 0;
161 my $pct = $val > 0 ? int(($val/$max)*100) : 0;
162 $pct = 1 if $pct < 1 && $val > 0;
163 my $c = $r->{color} || '#92400e';
164 my $lbl = _esc($r->{label} || '');
165 my $vl = _esc($r->{value_label} // $val);
166 my $ltr = _esc($r->{letter} || '');
167 my $plate = $ltr ? qq~<span class="brand-icon" style="--site-glow:$c" data-letter="$ltr"></span>~ : '';
168 $html .= qq~<div class="hbar-row">
169 <div class="hbar-label">$plate<span>$lbl</span></div>
170 <div class="hbar-track"><div class="hbar-fill" style="width:$pct%;background:$c;box-shadow:0 0 8px $c"></div></div>
171 <div class="hbar-value">$vl</div>
172</div>~;
173 }
174 $html .= '</div>';
175 return $html;
176}
177
178sub _brand_letters {
179 my ($r) = @_;
180 my %map = (webstls=>'WS',ptmatrix=>'PT',affsoft=>'AS',shopcart=>'SC',
181 repricer=>'RP',abforge=>'AB',contactforge=>'CF');
182 my $slug = $r->{slug} || '';
183 return $map{$slug} || uc(substr($r->{display_name} || '?', 0, 1));
184}
185
186# Semi-circle load gauge. Maps current load against cpu cores;
187# anything past cores is "overloaded" -- bar saturates red.
188sub svg_gauge {
189 my (%a) = @_;
190 my $size = $a{size} || 240;
191 my $value = ($a{value} || 0) + 0;
192 my $max = ($a{max} || 1) + 0;
193 my $label = $a{label} || '';
194 my $sub = $a{sub} || '';
195 my $r = $size * 0.40;
196 my $cx = $size / 2;
197 my $cy = $size * 0.62;
198 my $pct = $max > 0 ? ($value / $max) : 0;
199 $pct = 1 if $pct > 1;
200 my $deg = $pct * 180 - 90; # -90..+90 across the top
201 # Color band: cyan -> amber -> pink
202 my $color = $pct < 0.6 ? '#92400e' : ($pct < 0.9 ? '#92400e' : '#ef4444');
203
204 my $vbh = int($size * 0.95);
205 my $svg = qq~<svg viewBox="0 0 $size $vbh" width="$size" height="$vbh" style="max-width:100%;display:block;margin:0 auto">~;
206 # background arc
207 my ($lx, $ly) = _polar($cx, $cy, $r, -90);
208 my ($rx, $ry) = _polar($cx, $cy, $r, 90);
209 $svg .= qq~<path d="M $lx $ly A $r $r 0 0 1 $rx $ry" stroke="#1a1438" stroke-width="14" fill="none" stroke-linecap="round"/>~;
210 # filled arc
211 my ($ax, $ay) = _polar($cx, $cy, $r, $deg);
212 my $large = $deg - (-90) > 180 ? 1 : 0;
213 $svg .= qq~<path d="M $lx $ly A $r $r 0 $large 1 $ax $ay" stroke="$color" stroke-width="14" fill="none" stroke-linecap="round" style="filter:drop-shadow(0 0 10px $color)"/>~;
214 # needle
215 my ($nx, $ny) = _polar($cx, $cy, $r - 10, $deg);
216 $svg .= qq~<line x1="$cx" y1="$cy" x2="$nx" y2="$ny" stroke="$color" stroke-width="3" stroke-linecap="round" style="filter:drop-shadow(0 0 6px $color)"/>~;
217 $svg .= qq~<circle cx="$cx" cy="$cy" r="6" fill="$color" style="filter:drop-shadow(0 0 8px $color)"/>~;
218 # center labels
219 my $lbl = _esc($label); my $sub_l = _esc($sub);
220 $svg .= qq~<text x="$cx" y="${\ int($cy + 30) }" text-anchor="middle" font-family="'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-weight="600" font-size="24" font-weight="700" font-weight="800" fill="$color">$lbl</text>~;
221 $svg .= qq~<text x="$cx" y="${\ int($cy + 48) }" text-anchor="middle" font-family="'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" font-weight="600" font-size="11" fill="#807aa8" letter-spacing="1.5px">$sub_l</text>~;
222 $svg .= qq~</svg>~;
223 return $svg;
224}
225
226# Stacked memory bar: used | buffers/cached | free
227sub svg_memory_bar {
228 my (%a) = @_;
229 my $width = $a{width} || 600;
230 my $height = 26;
231 my $total = $a{total} || 1;
232 my $used = $a{used} || 0;
233 my $cached = $a{cached} || 0;
234 my $avail = $a{avail} || 0;
235 # Used = used - cached (real used); cached separate; avail separate.
236 my $real_used = $used - $cached;
237 $real_used = 0 if $real_used < 0;
238 my $wu = int(($real_used / $total) * $width);
239 my $wc = int(($cached / $total) * $width);
240 my $wa = $width - $wu - $wc; $wa = 0 if $wa < 0;
241 my $svg = qq~<svg viewBox="0 0 $width $height" width="100%" height="$height" preserveAspectRatio="none">~;
242 $svg .= qq~<rect x="0" y="0" width="$width" height="$height" rx="6" fill="#0d0a07"/>~;
243 $svg .= qq~<rect x="0" y="0" width="$wu" height="$height" rx="6" fill="#92400e" opacity="0.92" style="filter:drop-shadow(0 0 6px #92400e)"/>~;
244 $svg .= qq~<rect x="$wu" y="0" width="$wc" height="$height" fill="#aaff00" opacity="0.85" style="filter:drop-shadow(0 0 6px #aaff00)"/>~;
245 $svg .= qq~</svg>~;
246 return $svg;
247}
248
249#---------------------------------------------------------------------
250# Build the page body
251#---------------------------------------------------------------------
252
253my $disk_used_pct = $disk->{total_b} > 0 ? int($disk->{used_b} * 100 / $disk->{total_b}) : 0;
254my $mem_used_pct = $mem->{total_b} > 0 ? int($mem->{used_b} * 100 / $mem->{total_b}) : 0;
255my $load_color = $cpu->{cores} > 0 && ($cpu->{load1} / $cpu->{cores}) >= 0.9 ? 'magenta' : 'cyan';
256my $uptime_label = MODS::MetaAdmin::Stats::human_uptime($cpu->{uptime_s});
257
258my $body = '';
259
260# Page head
261$body .= q~
262<div class="page-head"><div class="left">
263 <span class="page-eyebrow"><span class="dot"></span> Health</span>
264 <h1 class="page-title">Server</h1>
265 <p class="page-subtitle">Live disk / memory / CPU / per-vhost footprint. Pulled directly from <code>/proc</code> and <code>du -sb</code>. Refresh to update.</p>
266</div></div>
267~;
268
269# Tile row: uptime, load, cores, host
270my $host_h = _esc($host->{host} || '-');
271my $os_h = _esc($host->{os} || '-');
272my $kernel_h = _esc($host->{kernel} || '-');
273$body .= qq~
274<div class="dash-grid">
275 <div class="module glow-cyan">
276 <div class="module-head"><div class="left"><div class="module-icon"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg></div><div class="module-title">Uptime</div></div></div>
277 <div class="module-body kpi kpi-cyan">
278 <div class="num">$uptime_label</div>
279 <div class="lbl">since last boot</div>
280 <div class="sub">$host_h</div>
281 </div>
282 </div>
283 <div class="module glow-magenta">
284 <div class="module-head"><div class="left"><div class="module-icon"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg></div><div class="module-title">Load (1m / 5m / 15m)</div></div></div>
285 <div class="module-body kpi kpi-magenta">
286 <div class="num">$cpu->{load1}</div>
287 <div class="lbl">on $cpu->{cores} cores</div>
288 <div class="sub">5m $cpu->{load5} &middot; 15m $cpu->{load15}</div>
289 </div>
290 </div>
291 <div class="module glow-lime">
292 <div class="module-head"><div class="left"><div class="module-icon"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/></svg></div><div class="module-title">Sites disk (/var)</div></div></div>
293 <div class="module-body kpi kpi-lime">
294 <div class="num">$disk_used_pct%</div>
295 <div class="lbl">used of ~ . MODS::MetaAdmin::Stats::human_size($disk->{total_b}) . qq~</div>
296 <div class="sub">${\ MODS::MetaAdmin::Stats::human_size($disk->{avail_b}) } free</div>
297 </div>
298 </div>
299 <div class="module glow-violet">
300 <div class="module-head"><div class="left"><div class="module-icon"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg></div><div class="module-title">Memory</div></div></div>
301 <div class="module-body kpi kpi-violet">
302 <div class="num">$mem_used_pct%</div>
303 <div class="lbl">used of ~ . MODS::MetaAdmin::Stats::human_size($mem->{total_b}) . qq~</div>
304 <div class="sub">${\ MODS::MetaAdmin::Stats::human_size($mem->{avail_b}) } available</div>
305 </div>
306 </div>
307</div>
308~;
309
310# Top row: disk donut + load gauge side by side
311my $disk_donut = svg_donut(
312 size => 220, thickness => 26,
313 values => [
314 { v => $disk->{used_b}, color => '#92400e', label => 'Used' },
315 { v => $disk->{avail_b}, color => '#92400e', label => 'Available' },
316 ],
317 center_label => "$disk_used_pct%",
318 center_sub => 'OF DISK USED',
319);
320
321my $load_gauge = svg_gauge(
322 size => 240,
323 value => $cpu->{load1},
324 max => ($cpu->{cores} || 1) * 1.5, # show headroom up to 1.5x cores
325 label => sprintf('%.2f', $cpu->{load1}),
326 sub => 'LOAD (1 MIN)',
327);
328
329# /var breakdown rows — what's actually eating the sites partition.
330my $var_used_b = $disk->{used_b} || 0;
331my $bd_rows_html = '';
332foreach my $r (@$varbd) {
333 my $lbl = _esc($r->{label});
334 my $path = _esc($r->{path});
335 my $used = MODS::MetaAdmin::Stats::human_size($r->{used_b});
336 my $pct = $var_used_b > 0 ? int($r->{used_b} * 100 / $var_used_b + 0.5) : 0;
337 my $pct_w = $pct < 1 && $r->{used_b} > 0 ? 1 : $pct;
338 my $col = $r->{color} || '#807aa8';
339 $bd_rows_html .= qq~
340 <div class="varbd-row">
341 <div class="varbd-lbl">
342 <span class="varbd-dot" style="background:$col;box-shadow:0 0 8px $col"></span>
343 <div class="varbd-txt"><div class="varbd-name">$lbl</div><div class="varbd-path mono dim">$path</div></div>
344 </div>
345 <div class="varbd-bar"><div class="varbd-fill" style="width:${pct_w}%;background:$col;box-shadow:0 0 6px $col"></div></div>
346 <div class="varbd-val mono"><strong>$used</strong> <span class="dim">$pct%</span></div>
347 </div>~;
348}
349$bd_rows_html ||= '<div class="dim" style="padding:14px 4px">No breakdown available (du failed on all probes).</div>';
350
351$body .= qq~
352<style>
353.varbd-row { display:grid; grid-template-columns: 1.4fr 2fr 1fr; gap:14px; align-items:center;
354 padding:8px 0; border-bottom:1px solid rgba(255,255,255,.03); font-size:12.5px; }
355.varbd-row:last-child { border-bottom:none; }
356.varbd-lbl { display:flex; align-items:center; gap:9px; min-width:0; }
357.varbd-dot { width:9px; height:9px; border-radius:50%; flex:0 0 auto; }
358.varbd-txt { min-width:0; }
359.varbd-name { font-weight:600; color:var(--col-text); }
360.varbd-path { font-size:10.5px; letter-spacing:.2px; }
361.varbd-bar { height:6px; background:rgba(255,255,255,.04); border-radius:999px; overflow:hidden;
362 border:1px solid rgba(255,255,255,.05); }
363.varbd-fill { height:100%; border-radius:999px; transition:width .4s ease; }
364.varbd-val { text-align:right; font-size:12px; }
365</style>
366<div class="dash-grid" style="grid-template-columns: 1.4fr 1fr">
367 <div class="module glow-magenta">
368 <div class="module-head"><div class="left"><div class="module-icon"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/></svg></div><div class="module-title">Sites partition (/var)</div><span class="module-subtitle">Where every portfolio site lives, plus MySQL data, mail spool, logs and Plesk services. Breakdown at right shows what is eating the used bytes.</span></div></div>
369 <div class="module-body" style="display:flex;align-items:center;gap:32px;flex-wrap:wrap;padding:20px 24px">
370 <div style="flex:0 0 auto">$disk_donut</div>
371 <div style="flex:1 1 220px;min-width:200px">
372 $bd_rows_html
373 </div>
374 </div>
375 </div>
376 <div class="module glow-cyan">
377 <div class="module-head"><div class="left"><div class="module-icon"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg></div><div class="module-title">CPU load</div></div></div>
378 <div class="module-body" style="display:flex;flex-direction:column;align-items:center;gap:14px">
379 $load_gauge
380 <div style="font-size:12px;color:var(--col-text-3);letter-spacing:1px;text-transform:uppercase">
381 $cpu->{cores} cores &middot; 1m ${\ sprintf('%.2f',$cpu->{load1}) } &middot; 5m ${\ sprintf('%.2f',$cpu->{load5}) } &middot; 15m ${\ sprintf('%.2f',$cpu->{load15}) }
382 </div>
383 </div>
384 </div>
385</div>
386~;
387
388# All partitions + physical disks
389# ------------------------------------------------------------------
390my $parts_total_b = 0; my $parts_used_b = 0;
391foreach my $p (@$parts) { $parts_total_b += $p->{total_b}; $parts_used_b += $p->{used_b}; }
392my $parts_pct = $parts_total_b > 0 ? int($parts_used_b * 100 / $parts_total_b + 0.5) : 0;
393my $phys_total_b = 0;
394foreach my $d (@$pdisk) { $phys_total_b += $d->{size_b}; }
395
396# Colour ramp so a full-ish partition reads red without hue-drift
397# (matches the wine palette on the rest of the console).
398sub _part_color {
399 my $pct = shift;
400 return '#4a1a20' if $pct < 40; # deep wine (calm)
401 return '#92400e' if $pct < 75; # amber (heads-up)
402 return '#b53048' if $pct < 90; # bright wine (warm)
403 return '#ef4444'; # red (danger)
404}
405
406my $parts_html = '<div class="parts-table">';
407$parts_html .= q~
408<div class="parts-head">
409 <div class="parts-mount">Mount</div>
410 <div class="parts-dev">Device</div>
411 <div class="parts-fs">FS</div>
412 <div class="parts-bar">Used</div>
413 <div class="parts-nums">Used / Total</div>
414</div>
415~;
416foreach my $p (@$parts) {
417 my $mnt = _esc($p->{mount});
418 my $dev = _esc($p->{device});
419 my $fs = _esc($p->{fstype});
420 my $pct = $p->{used_pct};
421 my $col = _part_color($pct);
422 my $used = MODS::MetaAdmin::Stats::human_size($p->{used_b});
423 my $total = MODS::MetaAdmin::Stats::human_size($p->{total_b});
424 my $pct_disp = $pct;
425 $pct_disp = 1 if $pct_disp < 1 && $p->{used_b} > 0;
426 $parts_html .= qq~
427 <div class="parts-row">
428 <div class="parts-mount"><span class="parts-mount-code">$mnt</span></div>
429 <div class="parts-dev mono dim">$dev</div>
430 <div class="parts-fs"><span class="parts-fs-pill">$fs</span></div>
431 <div class="parts-bar">
432 <div class="parts-bar-track">
433 <div class="parts-bar-fill" style="width:${pct_disp}%;background:$col;box-shadow:0 0 8px $col"></div>
434 </div>
435 <div class="parts-bar-pct">$pct%</div>
436 </div>
437 <div class="parts-nums mono">$used <span class="dim">/ $total</span></div>
438 </div>~;
439}
440$parts_html .= '</div>';
441
442my $phys_lines = '';
443foreach my $d (@$pdisk) {
444 my $nm = _esc($d->{name});
445 my $md = _esc($d->{model}) || 'unknown';
446 my $sz = MODS::MetaAdmin::Stats::human_size($d->{size_b});
447 $phys_lines .= qq~<div class="phys-disk"><span class="phys-name">/dev/$nm</span><span class="phys-model dim">$md</span><span class="phys-size mono"><strong>$sz</strong></span></div>~;
448}
449$phys_lines ||= '<div class="dim" style="padding:8px 0">No physical disks reported (lsblk unavailable).</div>';
450
451my $phys_total_h = MODS::MetaAdmin::Stats::human_size($phys_total_b);
452my $parts_used_h = MODS::MetaAdmin::Stats::human_size($parts_used_b);
453my $parts_total_h = MODS::MetaAdmin::Stats::human_size($parts_total_b);
454
455$body .= qq~
456<style>
457.parts-table { display:grid; row-gap:2px; font-size:13px; }
458.parts-head { display:grid; grid-template-columns: 1.4fr 1.6fr 60px 3fr 1.4fr; gap:14px; align-items:center;
459 padding:8px 14px; color:var(--col-text-dim); font-size:10.5px; letter-spacing:1.4px;
460 text-transform:uppercase; font-weight:700; border-bottom:1px solid var(--col-border); }
461.parts-row { display:grid; grid-template-columns: 1.4fr 1.6fr 60px 3fr 1.4fr; gap:14px; align-items:center;
462 padding:11px 14px; border-bottom:1px solid rgba(255,255,255,.03); transition:background .12s ease; }
463.parts-row:hover { background:rgba(255,255,255,.02); }
464.parts-mount-code { font-family:'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace; font-weight:600;
465 color:var(--col-text); background:rgba(255,255,255,.04); padding:3px 8px; border-radius:6px;
466 border:1px solid rgba(255,255,255,.06); font-size:12px; }
467.parts-fs-pill { font-family:'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace; font-size:10.5px;
468 letter-spacing:.6px; padding:2px 7px; border-radius:999px; background:rgba(181,48,72,.15);
469 color:#e5a1ac; border:1px solid rgba(181,48,72,.35); }
470.parts-bar { display:flex; align-items:center; gap:10px; }
471.parts-bar-track { flex:1 1 auto; height:8px; background:rgba(255,255,255,.04); border-radius:999px; overflow:hidden;
472 border:1px solid rgba(255,255,255,.05); }
473.parts-bar-fill { height:100%; border-radius:999px; transition:width .4s ease; }
474.parts-bar-pct { flex:0 0 auto; min-width:38px; text-align:right; font-family:'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace;
475 font-size:12px; font-weight:600; color:var(--col-text); }
476.parts-nums { text-align:right; font-size:12.5px; }
477.phys-disk { display:flex; align-items:center; gap:16px; padding:9px 14px;
478 border-bottom:1px solid rgba(255,255,255,.03); font-size:13px; }
479.phys-disk:last-child { border-bottom:none; }
480.phys-name { font-family:'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace; font-weight:700;
481 color:var(--col-text); min-width:80px; }
482.phys-model { flex:1 1 auto; font-size:12px; }
483.phys-size { text-align:right; }
484\@media (max-width: 780px) {
485 .parts-head, .parts-row { grid-template-columns: 1fr 1fr; }
486 .parts-head .parts-fs, .parts-head .parts-dev, .parts-row .parts-fs, .parts-row .parts-dev { display:none; }
487}
488</style>
489<div class="dash-grid" style="grid-template-columns: 2fr 1fr; margin-bottom:16px">
490 <div class="module glow-magenta">
491 <div class="module-head">
492 <div class="left">
493 <div class="module-icon"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="4" width="20" height="6" rx="1"/><rect x="2" y="14" width="20" height="6" rx="1"/><circle cx="6" cy="7" r="0.6"/><circle cx="6" cy="17" r="0.6"/></svg></div>
494 <div class="module-title">Partitions</div>
495 <span class="module-subtitle">Every real filesystem currently mounted. Pseudo-mounts (tmpfs, cgroup, etc.) are hidden so what you see is real writable space.</span>
496 </div>
497 <div class="right" style="font-size:11px;color:var(--col-text-3);padding-right:14px">${parts_pct}% used across all mounts &middot; $parts_used_h / $parts_total_h</div>
498 </div>
499 <div class="module-body" style="padding:0">$parts_html</div>
500 </div>
501 <div class="module glow-lime">
502 <div class="module-head">
503 <div class="left">
504 <div class="module-icon"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5v6c0 1.7 4 3 9 3s9-1.3 9-3V5"/><path d="M3 11v6c0 1.7 4 3 9 3s9-1.3 9-3v-6"/></svg></div>
505 <div class="module-title">Physical disks</div>
506 <span class="module-subtitle">Raw hardware exposed by <code>lsblk</code>. Total is the sum of all physical disks -- RAID mirroring makes usable space smaller than this number.</span>
507 </div>
508 </div>
509 <div class="module-body" style="padding:8px 0 4px">
510 $phys_lines
511 <div style="display:flex;align-items:center;gap:12px;padding:14px 14px 6px;margin-top:6px;border-top:1px solid var(--col-border);font-size:13px">
512 <span class="dim" style="font-size:11px;letter-spacing:1.4px;text-transform:uppercase;font-weight:700">Server total</span>
513 <span style="margin-left:auto;font-size:15px;font-weight:700;font-family:'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace">$phys_total_h</span>
514 </div>
515 </div>
516 </div>
517</div>
518~;
519
520# Memory module with stacked bar
521my $mem_bar = svg_memory_bar(
522 total => $mem->{total_b},
523 used => $mem->{used_b},
524 cached => $mem->{cached_b},
525 avail => $mem->{avail_b},
526);
527my $swap_pct = $mem->{swap_total_b} > 0 ? int($mem->{swap_used_b} * 100 / $mem->{swap_total_b}) : 0;
528$body .= qq~
529<div class="module glow-violet" style="margin-bottom:16px">
530 <div class="module-head"><div class="left"><div class="module-icon"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/></svg></div><div class="module-title">Memory breakdown</div></div></div>
531 <div class="module-body">
532 $mem_bar
533 <div style="display:flex;gap:24px;margin-top:12px;font-size:12px;color:var(--col-text-2);flex-wrap:wrap">
534 <div><span class="dot" style="background:#92400e;color:#92400e"></span> Used <strong>${\ MODS::MetaAdmin::Stats::human_size($mem->{used_b} - $mem->{cached_b}) }</strong></div>
535 <div><span class="dot" style="background:#aaff00;color:#aaff00"></span> Cached <strong>${\ MODS::MetaAdmin::Stats::human_size($mem->{cached_b}) }</strong></div>
536 <div><span class="dot" style="background:#0d0a07;color:#0d0a07;border:1px solid #4f4a73"></span> Available <strong>${\ MODS::MetaAdmin::Stats::human_size($mem->{avail_b}) }</strong></div>
537 <div style="margin-left:auto;color:var(--col-text-3)">Swap ${\ MODS::MetaAdmin::Stats::human_size($mem->{swap_used_b}) } / ${\ MODS::MetaAdmin::Stats::human_size($mem->{swap_total_b}) } ($swap_pct%)</div>
538 </div>
539 </div>
540</div>
541~;
542
543# Websites folders + Sites RAM footprint (paired modules)
544# ------------------------------------------------------------------
545my $disk_max_b = 1;
546my $ram_max_b = 1;
547foreach my $f (@$footprints) {
548 $disk_max_b = $f->{disk_b} if defined($f->{disk_b}) && $f->{disk_b} > $disk_max_b;
549 $ram_max_b = $f->{ram_b} if defined($f->{ram_b}) && $f->{ram_b} > $ram_max_b;
550}
551
552my $folders_rows = '';
553foreach my $f (@$footprints) {
554 my $name = _esc($f->{display_name});
555 my $path = _esc($f->{vhost_path});
556 my $col = _esc($f->{brand_color});
557 my $ltr = _brand_letters($f);
558
559 my ($sz, $pct, $sz_class, $bar_html);
560 if ($f->{unreachable}) {
561 $sz = 'n/a';
562 $sz_class = 'fp-size dim';
563 $bar_html = '<div class="fp-bar-track"><div class="fp-bar-fill" style="width:0%"></div></div>';
564 } else {
565 $sz = MODS::MetaAdmin::Stats::human_size($f->{disk_b});
566 $sz_class = 'fp-size';
567 $pct = int((($f->{disk_b} || 0) / $disk_max_b) * 100);
568 $pct = 1 if $pct < 1 && ($f->{disk_b} || 0) > 0;
569 $bar_html = qq~<div class="fp-bar-track"><div class="fp-bar-fill" style="width:${pct}%;background:$col;box-shadow:0 0 8px $col"></div></div>~;
570 }
571 $folders_rows .= qq~
572 <div class="fp-row">
573 <div class="fp-icon"><span class="brand-icon" style="--site-glow:$col" data-letter="$ltr"></span></div>
574 <div class="fp-name">
575 <div class="fp-name-h">$name</div>
576 <div class="fp-path mono dim">$path</div>
577 </div>
578 <div class="fp-bar-wrap">$bar_html</div>
579 <div class="$sz_class mono"><strong>$sz</strong></div>
580 </div>~;
581}
582$folders_rows ||= '<div class="dim" style="padding:14px">No configured vhost paths.</div>';
583
584my $ram_rows = '';
585foreach my $f (@$footprints) {
586 my $name = _esc($f->{display_name});
587 my $col = _esc($f->{brand_color});
588 my $ltr = _brand_letters($f);
589
590 my ($sub_html, $sz, $pct, $sz_class, $bar_html);
591 if ($f->{unreachable}) {
592 $sub_html = '<div class="fp-path dim">not visible from this box</div>';
593 $sz = 'n/a';
594 $sz_class = 'fp-size dim';
595 $bar_html = '<div class="fp-bar-track"><div class="fp-bar-fill" style="width:0%"></div></div>';
596 } else {
597 my $owner = _esc($f->{owner_user} || '-');
598 $sub_html = qq~<div class="fp-path mono dim">runs as <code>$owner</code></div>~;
599 $sz = MODS::MetaAdmin::Stats::human_size($f->{ram_b});
600 $sz_class = ($f->{ram_b} || 0) > 0 ? 'fp-size' : 'fp-size dim';
601 $pct = int((($f->{ram_b} || 0) / $ram_max_b) * 100);
602 $pct = 1 if $pct < 1 && ($f->{ram_b} || 0) > 0;
603 $bar_html = qq~<div class="fp-bar-track"><div class="fp-bar-fill" style="width:${pct}%;background:$col;box-shadow:0 0 8px $col"></div></div>~;
604 }
605 $ram_rows .= qq~
606 <div class="fp-row">
607 <div class="fp-icon"><span class="brand-icon" style="--site-glow:$col" data-letter="$ltr"></span></div>
608 <div class="fp-name">
609 <div class="fp-name-h">$name</div>
610 $sub_html
611 </div>
612 <div class="fp-bar-wrap">$bar_html</div>
613 <div class="$sz_class mono"><strong>$sz</strong></div>
614 </div>~;
615}
616$ram_rows ||= '<div class="dim" style="padding:14px">No configured vhost paths.</div>';
617
618my $sites_disk_total_h = MODS::MetaAdmin::Stats::human_size($sites_disk_total);
619my $var_avail_h = MODS::MetaAdmin::Stats::human_size($var_avail_b);
620my $sites_ram_total_h = MODS::MetaAdmin::Stats::human_size($sites_ram_total);
621my $mem_avail_h = MODS::MetaAdmin::Stats::human_size($mem_avail_b);
622
623$body .= qq~
624<style>
625.fp-list { display:grid; row-gap:2px; font-size:13px; padding:6px 0; }
626.fp-row { display:grid; grid-template-columns: 36px 1.6fr 3fr 1.2fr; gap:14px; align-items:center;
627 padding:10px 14px; border-bottom:1px solid rgba(255,255,255,.03); transition:background .12s ease; }
628.fp-row:hover { background:rgba(255,255,255,.02); }
629.fp-icon { display:flex; align-items:center; justify-content:center; }
630.fp-name { min-width:0; }
631.fp-name-h { font-weight:600; color:var(--col-text); font-size:13px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
632.fp-path { font-size:11px; margin-top:2px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
633.fp-bar-wrap { display:flex; align-items:center; }
634.fp-bar-track { flex:1 1 auto; height:8px; background:rgba(255,255,255,.04); border-radius:999px; overflow:hidden;
635 border:1px solid rgba(255,255,255,.05); }
636.fp-bar-fill { height:100%; border-radius:999px; transition:width .4s ease; }
637.fp-size { text-align:right; font-size:12.5px; }
638.fp-foot { display:flex; align-items:center; gap:12px; padding:12px 14px 4px; margin-top:6px;
639 border-top:1px solid var(--col-border); font-size:12.5px; }
640.fp-foot .lbl { font-size:10.5px; letter-spacing:1.4px; text-transform:uppercase; color:var(--col-text-dim);
641 font-weight:700; }
642.fp-foot .val { font-family:'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace; font-weight:700; font-size:13px; }
643.fp-foot .free { margin-left:auto; text-align:right; }
644.fp-foot .free-lbl { font-size:10.5px; letter-spacing:1.4px; text-transform:uppercase; color:var(--col-text-dim);
645 font-weight:700; }
646.fp-foot .free-val { font-family:'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace; font-weight:700;
647 font-size:15px; color:#a7f3d0; }
648</style>
649<div class="dash-grid" style="grid-template-columns: 1fr 1fr; margin-bottom:16px">
650 <div class="module glow-magenta">
651 <div class="module-head">
652 <div class="left">
653 <div class="module-icon"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg></div>
654 <div class="module-title">Websites folders</div>
655 <span class="module-subtitle">Each portfolio site's disk footprint on <code>/var</code>. Bars are relative to the largest site so the smallest ones still register.</span>
656 </div>
657 </div>
658 <div class="module-body" style="padding:0">
659 <div class="fp-list">$folders_rows</div>
660 <div class="fp-foot">
661 <span class="lbl">All sites</span> <span class="val">$sites_disk_total_h</span>
662 <span class="free">
663 <div class="free-lbl">Free on /var</div>
664 <div class="free-val">$var_avail_h</div>
665 </span>
666 </div>
667 </div>
668 </div>
669 <div class="module glow-lime">
670 <div class="module-head">
671 <div class="left">
672 <div class="module-icon"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg></div>
673 <div class="module-title">Sites RAM footprint</div>
674 <span class="module-subtitle">RSS of processes owned by each site's vhost user. Sites sharing an owner (all <code>3dshawn.com</code> sub-vhosts run as <code>mocnwahs3d</code>) show the same shared-pool total. Perl-CGI holds no memory between requests, so this is a live snapshot -- the total only counts each unique owner once.</span>
675 </div>
676 </div>
677 <div class="module-body" style="padding:0">
678 <div class="fp-list">$ram_rows</div>
679 <div class="fp-foot">
680 <span class="lbl">Sites total</span> <span class="val">$sites_ram_total_h</span>
681 <span class="free">
682 <div class="free-lbl">System memory free</div>
683 <div class="free-val">$mem_avail_h</div>
684 </span>
685 </div>
686 </div>
687 </div>
688</div>
689~;
690
691# Per-site MySQL DB sizes
692# ------------------------------------------------------------------
693my $db_max_b = 1;
694my $db_total_b = 0;
695my $db_tables_ct = 0;
696foreach my $d (@$db_sizes) {
697 $db_max_b = $d->{total_b} if $d->{total_b} > $db_max_b;
698 $db_total_b += $d->{total_b};
699 $db_tables_ct += $d->{tables_ct};
700}
701
702my $db_rows = '';
703foreach my $d (@$db_sizes) {
704 my $name = _esc($d->{display_name});
705 my $dbn = _esc($d->{db_name});
706 my $col = _esc($d->{brand_color});
707 my $ltr = _brand_letters($d);
708 my $sz = MODS::MetaAdmin::Stats::human_size($d->{total_b});
709 my $data = MODS::MetaAdmin::Stats::human_size($d->{data_b});
710 my $idx = MODS::MetaAdmin::Stats::human_size($d->{indexes_b});
711 my $tct = $d->{tables_ct};
712 my $pct = int(($d->{total_b} / $db_max_b) * 100);
713 $pct = 1 if $pct < 1 && $d->{total_b} > 0;
714 my $err = _esc($d->{err});
715 my $sub = $err
716 ? qq~<div class="fp-path dim">$err</div>~
717 : qq~<div class="fp-path mono dim">$dbn &middot; $tct tables &middot; data $data + idx $idx</div>~;
718 my $bar = $err
719 ? '<div class="fp-bar-track"><div class="fp-bar-fill" style="width:0%"></div></div>'
720 : qq~<div class="fp-bar-track"><div class="fp-bar-fill" style="width:${pct}%;background:$col;box-shadow:0 0 8px $col"></div></div>~;
721 my $sz_class = $err ? 'fp-size dim' : 'fp-size';
722 $sz = 'n/a' if $err;
723 $db_rows .= qq~
724 <div class="fp-row fp-row-db">
725 <div class="fp-icon"><span class="brand-icon" style="--site-glow:$col" data-letter="$ltr"></span></div>
726 <div class="fp-name">
727 <div class="fp-name-h">$name</div>
728 $sub
729 </div>
730 <div class="fp-bar-wrap">$bar</div>
731 <div class="$sz_class mono"><strong>$sz</strong></div>
732 </div>~;
733}
734$db_rows ||= '<div class="dim" style="padding:14px">No configured sites.</div>';
735
736my $db_total_h = MODS::MetaAdmin::Stats::human_size($db_total_b);
737
738$body .= qq~
739<style>
740.fp-row-db { grid-template-columns: 36px 2.2fr 3fr 1fr; }
741</style>
742<div class="module glow-magenta" style="margin-bottom:16px">
743 <div class="module-head">
744 <div class="left">
745 <div class="module-icon"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5v6c0 1.7 4 3 9 3s9-1.3 9-3V5"/><path d="M3 11v6c0 1.7 4 3 9 3s9-1.3 9-3v-6"/></svg></div>
746 <div class="module-title">Site databases</div>
747 <span class="module-subtitle">Per-site MySQL data + index size straight from each DB's <code>information_schema.TABLES</code>. Data lives on <code>/var/lib/mysql</code> (not in the vhost folder). When one site's bar starts crowding the others, that's the growth signal that says "move it to its own server".</span>
748 </div>
749 </div>
750 <div class="module-body" style="padding:0">
751 <div class="fp-list">$db_rows</div>
752 <div class="fp-foot">
753 <span class="lbl">All databases</span> <span class="val">$db_total_h</span>
754 <span class="dim" style="margin-left:14px;font-size:11.5px">$db_tables_ct tables total</span>
755 <span class="free">
756 <div class="free-lbl">Free on /var (mysql lives here)</div>
757 <div class="free-val">$var_avail_h</div>
758 </span>
759 </div>
760 </div>
761</div>
762~;
763
764# Top processes table
765$body .= qq~
766<div class="module" style="margin-top:16px">
767 <div class="module-head"><div class="left"><div class="module-icon"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/><polyline points="2 7 12 12 22 7"/></svg></div><div class="module-title">Top processes by CPU</div></div></div>
768 <div class="module-body" style="padding:0">
769 <table class="tbl"><thead><tr><th>PID</th><th>User</th><th>CPU %</th><th>MEM %</th><th>Command</th></tr></thead><tbody>
770~;
771foreach my $p (@$tops) {
772 my $pid = _esc($p->{pid});
773 my $user = _esc($p->{user});
774 my $cmd = _esc($p->{cmd});
775 my $cpu_v = sprintf('%.1f', $p->{cpu});
776 my $mem_v = sprintf('%.1f', $p->{mem});
777 $body .= qq~<tr><td class="mono">$pid</td><td>$user</td><td class="mono"><strong>$cpu_v</strong></td><td class="mono">$mem_v</td><td class="mono dim">$cmd</td></tr>~;
778}
779$body .= qq~
780 </tbody></table>
781 </div>
782</div>
783~;
784
785# Host info footer
786$body .= qq~
787<div style="margin-top:18px;text-align:right;color:var(--col-text-dim);font-size:11px;letter-spacing:1px;text-transform:uppercase">
788 $os_h &middot; kernel $kernel_h &middot; host $host_h
789</div>
790~;
791
792MODS::MetaAdmin::Wrapper->new->render(
793 title => 'Server',
794 page_key => 'server',
795 body => $body,
796 userinfo => $u,
797);
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help