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

O Operator
Diff

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

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

Added
+0
lines
Removed
-0
lines
Context
797
unchanged
Blobs
from 1cde422f1d6c
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.
11#!/usr/bin/perl
22#======================================================================
33# Meta-Admin -- /server.cgi
44#
55# Live disk + memory + cpu + per-vhost footprint, with hand-rolled
66# SVG visuals. No JS frameworks, no charting libs. Just SVG primitives
77# plus the synthwave palette.
88#
99# Graphs on this page:
1010# 1. Disk donut -- used vs avail on the root partition
1111# 2. Per-vhost bars -- du -sb per configured vhost_path
1212# 3. Load gauge -- semi-circle for 1-minute load vs CPU cores
1313# 4. Memory bars -- used / cached / free split
1414# 5. Tile row -- uptime, load 1/5/15, cores, kernel, OS
1515# 6. Top processes -- ps top 5 by CPU
1616#======================================================================
1717use strict;
1818use warnings;
1919use lib '/var/www/vhosts/3dshawn.com/admin.3dshawn.com';
2020use MODS::Login;
2121use MODS::DBConnect;
2222use MODS::MetaAdmin::Config;
2323use MODS::MetaAdmin::Wrapper;
2424use MODS::MetaAdmin::Stats;
2525use MODS::MetaAdmin::Aggregate;
2626
2727my $auth = MODS::Login->new;
2828my $u = $auth->login_verify;
2929unless ($u) { print "Status: 302 Found\nLocation: /login.cgi\n\n"; exit; }
3030
3131my $stats = MODS::MetaAdmin::Stats->new;
3232my $disk = $stats->disk_for('/var'); # portfolio lives here, not on /
3333my $mem = $stats->memory;
3434my $cpu = $stats->cpu;
3535my $host = $stats->host;
3636my $tops = $stats->top_processes;
3737my $parts = $stats->partitions;
3838my $pdisk = $stats->physical_disks;
3939my $varbd = $stats->var_breakdown;
4040
4141# Pull site list from local DB for the per-vhost disk pass.
4242my $db = MODS::DBConnect->new;
4343my $dbh = $db->db_connect;
4444my @sites = $db->db_readwrite_multiple($dbh, q~
4545 SELECT slug, display_name, brand_color, vhost_path
4646 FROM site_connections
4747 WHERE vhost_path IS NOT NULL AND vhost_path<>''
4848 ORDER BY sort_order, id
4949~, __FILE__, __LINE__);
5050$db->db_disconnect($dbh);
5151
5252my $vhost_disk = $stats->disk_per_vhost(\@sites);
5353my $vhost_total = 0;
5454$vhost_total += $_->{used_b} for @$vhost_disk;
5555
5656my $footprints = $stats->site_footprints(\@sites);
5757my $db_sizes = MODS::MetaAdmin::Aggregate->new->db_sizes;
5858my $sites_disk_total = 0;
5959my $unreachable_ct = 0;
6060my %ram_by_owner_seen;
6161foreach my $f (@$footprints) {
6262 if ($f->{unreachable}) { $unreachable_ct++; next; }
6363 $sites_disk_total += ($f->{disk_b} || 0);
6464 # De-dupe RAM by owner user so shared pools don't get counted N times.
6565 if ($f->{owner_user}) {
6666 $ram_by_owner_seen{$f->{owner_user}} = ($f->{ram_b} || 0);
6767 }
6868}
6969my $sites_ram_total = 0;
7070$sites_ram_total += $_ for values %ram_by_owner_seen;
7171my $var_stats = $stats->disk_for('/var');
7272my $var_avail_b = $var_stats->{avail_b} || 0;
7373my $var_total_b = $var_stats->{total_b} || 0;
7474my $mem_avail_b = $mem->{avail_b} || 0;
7575my $mem_total_b = $mem->{total_b} || 0;
7676
7777sub _esc {
7878 my $s = shift; $s //= '';
7979 $s =~ s/&/&amp;/g; $s =~ s/</&lt;/g; $s =~ s/>/&gt;/g;
8080 $s =~ s/"/&quot;/g;
8181 return $s;
8282}
8383
8484#---------------------------------------------------------------------
8585# SVG helpers (hand-rolled -- no charting library)
8686#---------------------------------------------------------------------
8787
8888# Polar-to-cartesian -- used by donut + gauge arc math.
8989sub _polar {
9090 my ($cx, $cy, $r, $deg) = @_;
9191 my $rad = ($deg - 90) * 3.14159265 / 180;
9292 return ($cx + $r * cos($rad), $cy + $r * sin($rad));
9393}
9494
9595# Donut chart. $values is arrayref of { v, color, label }. Total
9696# computed automatically. Renders a single-ring chart.
9797sub svg_donut {
9898 my (%a) = @_;
9999 my $size = $a{size} || 200;
100100 my $values = $a{values} || [];
101101 my $center_label = $a{center_label} || '';
102102 my $center_sub = $a{center_sub} || '';
103103 my $thickness = $a{thickness} || 22;
104104
105105 my $cx = $size / 2;
106106 my $cy = $size / 2;
107107 my $r = ($size / 2) - ($thickness / 2) - 4;
108108
109109 my $total = 0;
110110 foreach my $v (@$values) { $total += $v->{v} || 0; }
111111 $total = 1 unless $total > 0;
112112
113113 my $svg = qq~<svg viewBox="0 0 $size $size" width="$size" height="$size" style="overflow:visible">~;
114114 $svg .= qq~<defs>~;
115115 foreach my $i (0 .. $#$values) {
116116 my $color = $values->[$i]->{color} || '#807aa8';
117117 $svg .= qq~<filter id="donutGlow$i"><feGaussianBlur stdDeviation="3"/><feMerge><feMergeNode/><feMergeNode in="SourceGraphic"/></feMerge></filter>~;
118118 }
119119 $svg .= qq~</defs>~;
120120
121121 # Background ring
122122 $svg .= qq~<circle cx="$cx" cy="$cy" r="$r" stroke="#0d0a07" stroke-width="$thickness" fill="none"/>~;
123123
124124 my $start = 0;
125125 foreach my $i (0 .. $#$values) {
126126 my $v = $values->[$i];
127127 my $angle = ($v->{v} / $total) * 360;
128128 next if $angle < 0.5;
129129 my $end = $start + $angle;
130130 my $large = ($angle > 180) ? 1 : 0;
131131 my ($x1, $y1) = _polar($cx, $cy, $r, $start);
132132 my ($x2, $y2) = _polar($cx, $cy, $r, $end);
133133 my $color = $v->{color} || '#92400e';
134134 $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"/>~;
135135 $start = $end;
136136 }
137137
138138 # Center text
139139 my $cl = _esc($center_label); my $cs = _esc($center_sub);
140140 $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>~;
141141 $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>~;
142142
143143 $svg .= qq~</svg>~;
144144 return $svg;
145145}
146146
147147# Horizontal bar chart. $rows is arrayref of { label, value, color }.
148148# Renders one bar per row, padded labels at left, value at right.
149149sub svg_hbars {
150150 # HTML bar chart (kept the name for callers). Labels are real HTML
151151 # text so they inherit the page font exactly -- SVG text never matches
152152 # HTML font hinting. Plate rendered when `letter` is in the row.
153153 my (%a) = @_;
154154 my $rows = $a{rows} || [];
155155 my $max = 0;
156156 foreach my $r (@$rows) { $max = $r->{value} if ($r->{value} || 0) > $max; }
157157 $max = 1 unless $max > 0;
158158 my $html = '<div class="hbar-chart">';
159159 foreach my $r (@$rows) {
160160 my $val = $r->{value} || 0;
161161 my $pct = $val > 0 ? int(($val/$max)*100) : 0;
162162 $pct = 1 if $pct < 1 && $val > 0;
163163 my $c = $r->{color} || '#92400e';
164164 my $lbl = _esc($r->{label} || '');
165165 my $vl = _esc($r->{value_label} // $val);
166166 my $ltr = _esc($r->{letter} || '');
167167 my $plate = $ltr ? qq~<span class="brand-icon" style="--site-glow:$c" data-letter="$ltr"></span>~ : '';
168168 $html .= qq~<div class="hbar-row">
169169 <div class="hbar-label">$plate<span>$lbl</span></div>
170170 <div class="hbar-track"><div class="hbar-fill" style="width:$pct%;background:$c;box-shadow:0 0 8px $c"></div></div>
171171 <div class="hbar-value">$vl</div>
172172</div>~;
173173 }
174174 $html .= '</div>';
175175 return $html;
176176}
177177
178178sub _brand_letters {
179179 my ($r) = @_;
180180 my %map = (webstls=>'WS',ptmatrix=>'PT',affsoft=>'AS',shopcart=>'SC',
181181 repricer=>'RP',abforge=>'AB',contactforge=>'CF');
182182 my $slug = $r->{slug} || '';
183183 return $map{$slug} || uc(substr($r->{display_name} || '?', 0, 1));
184184}
185185
186186# Semi-circle load gauge. Maps current load against cpu cores;
187187# anything past cores is "overloaded" -- bar saturates red.
188188sub svg_gauge {
189189 my (%a) = @_;
190190 my $size = $a{size} || 240;
191191 my $value = ($a{value} || 0) + 0;
192192 my $max = ($a{max} || 1) + 0;
193193 my $label = $a{label} || '';
194194 my $sub = $a{sub} || '';
195195 my $r = $size * 0.40;
196196 my $cx = $size / 2;
197197 my $cy = $size * 0.62;
198198 my $pct = $max > 0 ? ($value / $max) : 0;
199199 $pct = 1 if $pct > 1;
200200 my $deg = $pct * 180 - 90; # -90..+90 across the top
201201 # Color band: cyan -> amber -> pink
202202 my $color = $pct < 0.6 ? '#92400e' : ($pct < 0.9 ? '#92400e' : '#ef4444');
203203
204204 my $vbh = int($size * 0.95);
205205 my $svg = qq~<svg viewBox="0 0 $size $vbh" width="$size" height="$vbh" style="max-width:100%;display:block;margin:0 auto">~;
206206 # background arc
207207 my ($lx, $ly) = _polar($cx, $cy, $r, -90);
208208 my ($rx, $ry) = _polar($cx, $cy, $r, 90);
209209 $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"/>~;
210210 # filled arc
211211 my ($ax, $ay) = _polar($cx, $cy, $r, $deg);
212212 my $large = $deg - (-90) > 180 ? 1 : 0;
213213 $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)"/>~;
214214 # needle
215215 my ($nx, $ny) = _polar($cx, $cy, $r - 10, $deg);
216216 $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)"/>~;
217217 $svg .= qq~<circle cx="$cx" cy="$cy" r="6" fill="$color" style="filter:drop-shadow(0 0 8px $color)"/>~;
218218 # center labels
219219 my $lbl = _esc($label); my $sub_l = _esc($sub);
220220 $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>~;
221221 $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>~;
222222 $svg .= qq~</svg>~;
223223 return $svg;
224224}
225225
226226# Stacked memory bar: used | buffers/cached | free
227227sub svg_memory_bar {
228228 my (%a) = @_;
229229 my $width = $a{width} || 600;
230230 my $height = 26;
231231 my $total = $a{total} || 1;
232232 my $used = $a{used} || 0;
233233 my $cached = $a{cached} || 0;
234234 my $avail = $a{avail} || 0;
235235 # Used = used - cached (real used); cached separate; avail separate.
236236 my $real_used = $used - $cached;
237237 $real_used = 0 if $real_used < 0;
238238 my $wu = int(($real_used / $total) * $width);
239239 my $wc = int(($cached / $total) * $width);
240240 my $wa = $width - $wu - $wc; $wa = 0 if $wa < 0;
241241 my $svg = qq~<svg viewBox="0 0 $width $height" width="100%" height="$height" preserveAspectRatio="none">~;
242242 $svg .= qq~<rect x="0" y="0" width="$width" height="$height" rx="6" fill="#0d0a07"/>~;
243243 $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)"/>~;
244244 $svg .= qq~<rect x="$wu" y="0" width="$wc" height="$height" fill="#aaff00" opacity="0.85" style="filter:drop-shadow(0 0 6px #aaff00)"/>~;
245245 $svg .= qq~</svg>~;
246246 return $svg;
247247}
248248
249249#---------------------------------------------------------------------
250250# Build the page body
251251#---------------------------------------------------------------------
252252
253253my $disk_used_pct = $disk->{total_b} > 0 ? int($disk->{used_b} * 100 / $disk->{total_b}) : 0;
254254my $mem_used_pct = $mem->{total_b} > 0 ? int($mem->{used_b} * 100 / $mem->{total_b}) : 0;
255255my $load_color = $cpu->{cores} > 0 && ($cpu->{load1} / $cpu->{cores}) >= 0.9 ? 'magenta' : 'cyan';
256256my $uptime_label = MODS::MetaAdmin::Stats::human_uptime($cpu->{uptime_s});
257257
258258my $body = '';
259259
260260# Page head
261261$body .= q~
262262<div class="page-head"><div class="left">
263263 <span class="page-eyebrow"><span class="dot"></span> Health</span>
264264 <h1 class="page-title">Server</h1>
265265 <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>
266266</div></div>
267267~;
268268
269269# Tile row: uptime, load, cores, host
270270my $host_h = _esc($host->{host} || '-');
271271my $os_h = _esc($host->{os} || '-');
272272my $kernel_h = _esc($host->{kernel} || '-');
273273$body .= qq~
274274<div class="dash-grid">
275275 <div class="module glow-cyan">
276276 <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>
277277 <div class="module-body kpi kpi-cyan">
278278 <div class="num">$uptime_label</div>
279279 <div class="lbl">since last boot</div>
280280 <div class="sub">$host_h</div>
281281 </div>
282282 </div>
283283 <div class="module glow-magenta">
284284 <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>
285285 <div class="module-body kpi kpi-magenta">
286286 <div class="num">$cpu->{load1}</div>
287287 <div class="lbl">on $cpu->{cores} cores</div>
288288 <div class="sub">5m $cpu->{load5} &middot; 15m $cpu->{load15}</div>
289289 </div>
290290 </div>
291291 <div class="module glow-lime">
292292 <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>
293293 <div class="module-body kpi kpi-lime">
294294 <div class="num">$disk_used_pct%</div>
295295 <div class="lbl">used of ~ . MODS::MetaAdmin::Stats::human_size($disk->{total_b}) . qq~</div>
296296 <div class="sub">${\ MODS::MetaAdmin::Stats::human_size($disk->{avail_b}) } free</div>
297297 </div>
298298 </div>
299299 <div class="module glow-violet">
300300 <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>
301301 <div class="module-body kpi kpi-violet">
302302 <div class="num">$mem_used_pct%</div>
303303 <div class="lbl">used of ~ . MODS::MetaAdmin::Stats::human_size($mem->{total_b}) . qq~</div>
304304 <div class="sub">${\ MODS::MetaAdmin::Stats::human_size($mem->{avail_b}) } available</div>
305305 </div>
306306 </div>
307307</div>
308308~;
309309
310310# Top row: disk donut + load gauge side by side
311311my $disk_donut = svg_donut(
312312 size => 220, thickness => 26,
313313 values => [
314314 { v => $disk->{used_b}, color => '#92400e', label => 'Used' },
315315 { v => $disk->{avail_b}, color => '#92400e', label => 'Available' },
316316 ],
317317 center_label => "$disk_used_pct%",
318318 center_sub => 'OF DISK USED',
319319);
320320
321321my $load_gauge = svg_gauge(
322322 size => 240,
323323 value => $cpu->{load1},
324324 max => ($cpu->{cores} || 1) * 1.5, # show headroom up to 1.5x cores
325325 label => sprintf('%.2f', $cpu->{load1}),
326326 sub => 'LOAD (1 MIN)',
327327);
328328
329329# /var breakdown rows — what's actually eating the sites partition.
330330my $var_used_b = $disk->{used_b} || 0;
331331my $bd_rows_html = '';
332332foreach my $r (@$varbd) {
333333 my $lbl = _esc($r->{label});
334334 my $path = _esc($r->{path});
335335 my $used = MODS::MetaAdmin::Stats::human_size($r->{used_b});
336336 my $pct = $var_used_b > 0 ? int($r->{used_b} * 100 / $var_used_b + 0.5) : 0;
337337 my $pct_w = $pct < 1 && $r->{used_b} > 0 ? 1 : $pct;
338338 my $col = $r->{color} || '#807aa8';
339339 $bd_rows_html .= qq~
340340 <div class="varbd-row">
341341 <div class="varbd-lbl">
342342 <span class="varbd-dot" style="background:$col;box-shadow:0 0 8px $col"></span>
343343 <div class="varbd-txt"><div class="varbd-name">$lbl</div><div class="varbd-path mono dim">$path</div></div>
344344 </div>
345345 <div class="varbd-bar"><div class="varbd-fill" style="width:${pct_w}%;background:$col;box-shadow:0 0 6px $col"></div></div>
346346 <div class="varbd-val mono"><strong>$used</strong> <span class="dim">$pct%</span></div>
347347 </div>~;
348348}
349349$bd_rows_html ||= '<div class="dim" style="padding:14px 4px">No breakdown available (du failed on all probes).</div>';
350350
351351$body .= qq~
352352<style>
353353.varbd-row { display:grid; grid-template-columns: 1.4fr 2fr 1fr; gap:14px; align-items:center;
354354 padding:8px 0; border-bottom:1px solid rgba(255,255,255,.03); font-size:12.5px; }
355355.varbd-row:last-child { border-bottom:none; }
356356.varbd-lbl { display:flex; align-items:center; gap:9px; min-width:0; }
357357.varbd-dot { width:9px; height:9px; border-radius:50%; flex:0 0 auto; }
358358.varbd-txt { min-width:0; }
359359.varbd-name { font-weight:600; color:var(--col-text); }
360360.varbd-path { font-size:10.5px; letter-spacing:.2px; }
361361.varbd-bar { height:6px; background:rgba(255,255,255,.04); border-radius:999px; overflow:hidden;
362362 border:1px solid rgba(255,255,255,.05); }
363363.varbd-fill { height:100%; border-radius:999px; transition:width .4s ease; }
364364.varbd-val { text-align:right; font-size:12px; }
365365</style>
366366<div class="dash-grid" style="grid-template-columns: 1.4fr 1fr">
367367 <div class="module glow-magenta">
368368 <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>
369369 <div class="module-body" style="display:flex;align-items:center;gap:32px;flex-wrap:wrap;padding:20px 24px">
370370 <div style="flex:0 0 auto">$disk_donut</div>
371371 <div style="flex:1 1 220px;min-width:200px">
372372 $bd_rows_html
373373 </div>
374374 </div>
375375 </div>
376376 <div class="module glow-cyan">
377377 <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>
378378 <div class="module-body" style="display:flex;flex-direction:column;align-items:center;gap:14px">
379379 $load_gauge
380380 <div style="font-size:12px;color:var(--col-text-3);letter-spacing:1px;text-transform:uppercase">
381381 $cpu->{cores} cores &middot; 1m ${\ sprintf('%.2f',$cpu->{load1}) } &middot; 5m ${\ sprintf('%.2f',$cpu->{load5}) } &middot; 15m ${\ sprintf('%.2f',$cpu->{load15}) }
382382 </div>
383383 </div>
384384 </div>
385385</div>
386386~;
387387
388388# All partitions + physical disks
389389# ------------------------------------------------------------------
390390my $parts_total_b = 0; my $parts_used_b = 0;
391391foreach my $p (@$parts) { $parts_total_b += $p->{total_b}; $parts_used_b += $p->{used_b}; }
392392my $parts_pct = $parts_total_b > 0 ? int($parts_used_b * 100 / $parts_total_b + 0.5) : 0;
393393my $phys_total_b = 0;
394394foreach my $d (@$pdisk) { $phys_total_b += $d->{size_b}; }
395395
396396# Colour ramp so a full-ish partition reads red without hue-drift
397397# (matches the wine palette on the rest of the console).
398398sub _part_color {
399399 my $pct = shift;
400400 return '#4a1a20' if $pct < 40; # deep wine (calm)
401401 return '#92400e' if $pct < 75; # amber (heads-up)
402402 return '#b53048' if $pct < 90; # bright wine (warm)
403403 return '#ef4444'; # red (danger)
404404}
405405
406406my $parts_html = '<div class="parts-table">';
407407$parts_html .= q~
408408<div class="parts-head">
409409 <div class="parts-mount">Mount</div>
410410 <div class="parts-dev">Device</div>
411411 <div class="parts-fs">FS</div>
412412 <div class="parts-bar">Used</div>
413413 <div class="parts-nums">Used / Total</div>
414414</div>
415415~;
416416foreach my $p (@$parts) {
417417 my $mnt = _esc($p->{mount});
418418 my $dev = _esc($p->{device});
419419 my $fs = _esc($p->{fstype});
420420 my $pct = $p->{used_pct};
421421 my $col = _part_color($pct);
422422 my $used = MODS::MetaAdmin::Stats::human_size($p->{used_b});
423423 my $total = MODS::MetaAdmin::Stats::human_size($p->{total_b});
424424 my $pct_disp = $pct;
425425 $pct_disp = 1 if $pct_disp < 1 && $p->{used_b} > 0;
426426 $parts_html .= qq~
427427 <div class="parts-row">
428428 <div class="parts-mount"><span class="parts-mount-code">$mnt</span></div>
429429 <div class="parts-dev mono dim">$dev</div>
430430 <div class="parts-fs"><span class="parts-fs-pill">$fs</span></div>
431431 <div class="parts-bar">
432432 <div class="parts-bar-track">
433433 <div class="parts-bar-fill" style="width:${pct_disp}%;background:$col;box-shadow:0 0 8px $col"></div>
434434 </div>
435435 <div class="parts-bar-pct">$pct%</div>
436436 </div>
437437 <div class="parts-nums mono">$used <span class="dim">/ $total</span></div>
438438 </div>~;
439439}
440440$parts_html .= '</div>';
441441
442442my $phys_lines = '';
443443foreach my $d (@$pdisk) {
444444 my $nm = _esc($d->{name});
445445 my $md = _esc($d->{model}) || 'unknown';
446446 my $sz = MODS::MetaAdmin::Stats::human_size($d->{size_b});
447447 $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>~;
448448}
449449$phys_lines ||= '<div class="dim" style="padding:8px 0">No physical disks reported (lsblk unavailable).</div>';
450450
451451my $phys_total_h = MODS::MetaAdmin::Stats::human_size($phys_total_b);
452452my $parts_used_h = MODS::MetaAdmin::Stats::human_size($parts_used_b);
453453my $parts_total_h = MODS::MetaAdmin::Stats::human_size($parts_total_b);
454454
455455$body .= qq~
456456<style>
457457.parts-table { display:grid; row-gap:2px; font-size:13px; }
458458.parts-head { display:grid; grid-template-columns: 1.4fr 1.6fr 60px 3fr 1.4fr; gap:14px; align-items:center;
459459 padding:8px 14px; color:var(--col-text-dim); font-size:10.5px; letter-spacing:1.4px;
460460 text-transform:uppercase; font-weight:700; border-bottom:1px solid var(--col-border); }
461461.parts-row { display:grid; grid-template-columns: 1.4fr 1.6fr 60px 3fr 1.4fr; gap:14px; align-items:center;
462462 padding:11px 14px; border-bottom:1px solid rgba(255,255,255,.03); transition:background .12s ease; }
463463.parts-row:hover { background:rgba(255,255,255,.02); }
464464.parts-mount-code { font-family:'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace; font-weight:600;
465465 color:var(--col-text); background:rgba(255,255,255,.04); padding:3px 8px; border-radius:6px;
466466 border:1px solid rgba(255,255,255,.06); font-size:12px; }
467467.parts-fs-pill { font-family:'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace; font-size:10.5px;
468468 letter-spacing:.6px; padding:2px 7px; border-radius:999px; background:rgba(181,48,72,.15);
469469 color:#e5a1ac; border:1px solid rgba(181,48,72,.35); }
470470.parts-bar { display:flex; align-items:center; gap:10px; }
471471.parts-bar-track { flex:1 1 auto; height:8px; background:rgba(255,255,255,.04); border-radius:999px; overflow:hidden;
472472 border:1px solid rgba(255,255,255,.05); }
473473.parts-bar-fill { height:100%; border-radius:999px; transition:width .4s ease; }
474474.parts-bar-pct { flex:0 0 auto; min-width:38px; text-align:right; font-family:'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace;
475475 font-size:12px; font-weight:600; color:var(--col-text); }
476476.parts-nums { text-align:right; font-size:12.5px; }
477477.phys-disk { display:flex; align-items:center; gap:16px; padding:9px 14px;
478478 border-bottom:1px solid rgba(255,255,255,.03); font-size:13px; }
479479.phys-disk:last-child { border-bottom:none; }
480480.phys-name { font-family:'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace; font-weight:700;
481481 color:var(--col-text); min-width:80px; }
482482.phys-model { flex:1 1 auto; font-size:12px; }
483483.phys-size { text-align:right; }
484484\@media (max-width: 780px) {
485485 .parts-head, .parts-row { grid-template-columns: 1fr 1fr; }
486486 .parts-head .parts-fs, .parts-head .parts-dev, .parts-row .parts-fs, .parts-row .parts-dev { display:none; }
487487}
488488</style>
489489<div class="dash-grid" style="grid-template-columns: 2fr 1fr; margin-bottom:16px">
490490 <div class="module glow-magenta">
491491 <div class="module-head">
492492 <div class="left">
493493 <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>
494494 <div class="module-title">Partitions</div>
495495 <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>
496496 </div>
497497 <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>
498498 </div>
499499 <div class="module-body" style="padding:0">$parts_html</div>
500500 </div>
501501 <div class="module glow-lime">
502502 <div class="module-head">
503503 <div class="left">
504504 <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>
505505 <div class="module-title">Physical disks</div>
506506 <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>
507507 </div>
508508 </div>
509509 <div class="module-body" style="padding:8px 0 4px">
510510 $phys_lines
511511 <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">
512512 <span class="dim" style="font-size:11px;letter-spacing:1.4px;text-transform:uppercase;font-weight:700">Server total</span>
513513 <span style="margin-left:auto;font-size:15px;font-weight:700;font-family:'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace">$phys_total_h</span>
514514 </div>
515515 </div>
516516 </div>
517517</div>
518518~;
519519
520520# Memory module with stacked bar
521521my $mem_bar = svg_memory_bar(
522522 total => $mem->{total_b},
523523 used => $mem->{used_b},
524524 cached => $mem->{cached_b},
525525 avail => $mem->{avail_b},
526526);
527527my $swap_pct = $mem->{swap_total_b} > 0 ? int($mem->{swap_used_b} * 100 / $mem->{swap_total_b}) : 0;
528528$body .= qq~
529529<div class="module glow-violet" style="margin-bottom:16px">
530530 <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>
531531 <div class="module-body">
532532 $mem_bar
533533 <div style="display:flex;gap:24px;margin-top:12px;font-size:12px;color:var(--col-text-2);flex-wrap:wrap">
534534 <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>
535535 <div><span class="dot" style="background:#aaff00;color:#aaff00"></span> Cached <strong>${\ MODS::MetaAdmin::Stats::human_size($mem->{cached_b}) }</strong></div>
536536 <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>
537537 <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>
538538 </div>
539539 </div>
540540</div>
541541~;
542542
543543# Websites folders + Sites RAM footprint (paired modules)
544544# ------------------------------------------------------------------
545545my $disk_max_b = 1;
546546my $ram_max_b = 1;
547547foreach my $f (@$footprints) {
548548 $disk_max_b = $f->{disk_b} if defined($f->{disk_b}) && $f->{disk_b} > $disk_max_b;
549549 $ram_max_b = $f->{ram_b} if defined($f->{ram_b}) && $f->{ram_b} > $ram_max_b;
550550}
551551
552552my $folders_rows = '';
553553foreach my $f (@$footprints) {
554554 my $name = _esc($f->{display_name});
555555 my $path = _esc($f->{vhost_path});
556556 my $col = _esc($f->{brand_color});
557557 my $ltr = _brand_letters($f);
558558
559559 my ($sz, $pct, $sz_class, $bar_html);
560560 if ($f->{unreachable}) {
561561 $sz = 'n/a';
562562 $sz_class = 'fp-size dim';
563563 $bar_html = '<div class="fp-bar-track"><div class="fp-bar-fill" style="width:0%"></div></div>';
564564 } else {
565565 $sz = MODS::MetaAdmin::Stats::human_size($f->{disk_b});
566566 $sz_class = 'fp-size';
567567 $pct = int((($f->{disk_b} || 0) / $disk_max_b) * 100);
568568 $pct = 1 if $pct < 1 && ($f->{disk_b} || 0) > 0;
569569 $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>~;
570570 }
571571 $folders_rows .= qq~
572572 <div class="fp-row">
573573 <div class="fp-icon"><span class="brand-icon" style="--site-glow:$col" data-letter="$ltr"></span></div>
574574 <div class="fp-name">
575575 <div class="fp-name-h">$name</div>
576576 <div class="fp-path mono dim">$path</div>
577577 </div>
578578 <div class="fp-bar-wrap">$bar_html</div>
579579 <div class="$sz_class mono"><strong>$sz</strong></div>
580580 </div>~;
581581}
582582$folders_rows ||= '<div class="dim" style="padding:14px">No configured vhost paths.</div>';
583583
584584my $ram_rows = '';
585585foreach my $f (@$footprints) {
586586 my $name = _esc($f->{display_name});
587587 my $col = _esc($f->{brand_color});
588588 my $ltr = _brand_letters($f);
589589
590590 my ($sub_html, $sz, $pct, $sz_class, $bar_html);
591591 if ($f->{unreachable}) {
592592 $sub_html = '<div class="fp-path dim">not visible from this box</div>';
593593 $sz = 'n/a';
594594 $sz_class = 'fp-size dim';
595595 $bar_html = '<div class="fp-bar-track"><div class="fp-bar-fill" style="width:0%"></div></div>';
596596 } else {
597597 my $owner = _esc($f->{owner_user} || '-');
598598 $sub_html = qq~<div class="fp-path mono dim">runs as <code>$owner</code></div>~;
599599 $sz = MODS::MetaAdmin::Stats::human_size($f->{ram_b});
600600 $sz_class = ($f->{ram_b} || 0) > 0 ? 'fp-size' : 'fp-size dim';
601601 $pct = int((($f->{ram_b} || 0) / $ram_max_b) * 100);
602602 $pct = 1 if $pct < 1 && ($f->{ram_b} || 0) > 0;
603603 $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>~;
604604 }
605605 $ram_rows .= qq~
606606 <div class="fp-row">
607607 <div class="fp-icon"><span class="brand-icon" style="--site-glow:$col" data-letter="$ltr"></span></div>
608608 <div class="fp-name">
609609 <div class="fp-name-h">$name</div>
610610 $sub_html
611611 </div>
612612 <div class="fp-bar-wrap">$bar_html</div>
613613 <div class="$sz_class mono"><strong>$sz</strong></div>
614614 </div>~;
615615}
616616$ram_rows ||= '<div class="dim" style="padding:14px">No configured vhost paths.</div>';
617617
618618my $sites_disk_total_h = MODS::MetaAdmin::Stats::human_size($sites_disk_total);
619619my $var_avail_h = MODS::MetaAdmin::Stats::human_size($var_avail_b);
620620my $sites_ram_total_h = MODS::MetaAdmin::Stats::human_size($sites_ram_total);
621621my $mem_avail_h = MODS::MetaAdmin::Stats::human_size($mem_avail_b);
622622
623623$body .= qq~
624624<style>
625625.fp-list { display:grid; row-gap:2px; font-size:13px; padding:6px 0; }
626626.fp-row { display:grid; grid-template-columns: 36px 1.6fr 3fr 1.2fr; gap:14px; align-items:center;
627627 padding:10px 14px; border-bottom:1px solid rgba(255,255,255,.03); transition:background .12s ease; }
628628.fp-row:hover { background:rgba(255,255,255,.02); }
629629.fp-icon { display:flex; align-items:center; justify-content:center; }
630630.fp-name { min-width:0; }
631631.fp-name-h { font-weight:600; color:var(--col-text); font-size:13px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
632632.fp-path { font-size:11px; margin-top:2px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
633633.fp-bar-wrap { display:flex; align-items:center; }
634634.fp-bar-track { flex:1 1 auto; height:8px; background:rgba(255,255,255,.04); border-radius:999px; overflow:hidden;
635635 border:1px solid rgba(255,255,255,.05); }
636636.fp-bar-fill { height:100%; border-radius:999px; transition:width .4s ease; }
637637.fp-size { text-align:right; font-size:12.5px; }
638638.fp-foot { display:flex; align-items:center; gap:12px; padding:12px 14px 4px; margin-top:6px;
639639 border-top:1px solid var(--col-border); font-size:12.5px; }
640640.fp-foot .lbl { font-size:10.5px; letter-spacing:1.4px; text-transform:uppercase; color:var(--col-text-dim);
641641 font-weight:700; }
642642.fp-foot .val { font-family:'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace; font-weight:700; font-size:13px; }
643643.fp-foot .free { margin-left:auto; text-align:right; }
644644.fp-foot .free-lbl { font-size:10.5px; letter-spacing:1.4px; text-transform:uppercase; color:var(--col-text-dim);
645645 font-weight:700; }
646646.fp-foot .free-val { font-family:'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace; font-weight:700;
647647 font-size:15px; color:#a7f3d0; }
648648</style>
649649<div class="dash-grid" style="grid-template-columns: 1fr 1fr; margin-bottom:16px">
650650 <div class="module glow-magenta">
651651 <div class="module-head">
652652 <div class="left">
653653 <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>
654654 <div class="module-title">Websites folders</div>
655655 <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>
656656 </div>
657657 </div>
658658 <div class="module-body" style="padding:0">
659659 <div class="fp-list">$folders_rows</div>
660660 <div class="fp-foot">
661661 <span class="lbl">All sites</span> <span class="val">$sites_disk_total_h</span>
662662 <span class="free">
663663 <div class="free-lbl">Free on /var</div>
664664 <div class="free-val">$var_avail_h</div>
665665 </span>
666666 </div>
667667 </div>
668668 </div>
669669 <div class="module glow-lime">
670670 <div class="module-head">
671671 <div class="left">
672672 <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>
673673 <div class="module-title">Sites RAM footprint</div>
674674 <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>
675675 </div>
676676 </div>
677677 <div class="module-body" style="padding:0">
678678 <div class="fp-list">$ram_rows</div>
679679 <div class="fp-foot">
680680 <span class="lbl">Sites total</span> <span class="val">$sites_ram_total_h</span>
681681 <span class="free">
682682 <div class="free-lbl">System memory free</div>
683683 <div class="free-val">$mem_avail_h</div>
684684 </span>
685685 </div>
686686 </div>
687687 </div>
688688</div>
689689~;
690690
691691# Per-site MySQL DB sizes
692692# ------------------------------------------------------------------
693693my $db_max_b = 1;
694694my $db_total_b = 0;
695695my $db_tables_ct = 0;
696696foreach my $d (@$db_sizes) {
697697 $db_max_b = $d->{total_b} if $d->{total_b} > $db_max_b;
698698 $db_total_b += $d->{total_b};
699699 $db_tables_ct += $d->{tables_ct};
700700}
701701
702702my $db_rows = '';
703703foreach my $d (@$db_sizes) {
704704 my $name = _esc($d->{display_name});
705705 my $dbn = _esc($d->{db_name});
706706 my $col = _esc($d->{brand_color});
707707 my $ltr = _brand_letters($d);
708708 my $sz = MODS::MetaAdmin::Stats::human_size($d->{total_b});
709709 my $data = MODS::MetaAdmin::Stats::human_size($d->{data_b});
710710 my $idx = MODS::MetaAdmin::Stats::human_size($d->{indexes_b});
711711 my $tct = $d->{tables_ct};
712712 my $pct = int(($d->{total_b} / $db_max_b) * 100);
713713 $pct = 1 if $pct < 1 && $d->{total_b} > 0;
714714 my $err = _esc($d->{err});
715715 my $sub = $err
716716 ? qq~<div class="fp-path dim">$err</div>~
717717 : qq~<div class="fp-path mono dim">$dbn &middot; $tct tables &middot; data $data + idx $idx</div>~;
718718 my $bar = $err
719719 ? '<div class="fp-bar-track"><div class="fp-bar-fill" style="width:0%"></div></div>'
720720 : qq~<div class="fp-bar-track"><div class="fp-bar-fill" style="width:${pct}%;background:$col;box-shadow:0 0 8px $col"></div></div>~;
721721 my $sz_class = $err ? 'fp-size dim' : 'fp-size';
722722 $sz = 'n/a' if $err;
723723 $db_rows .= qq~
724724 <div class="fp-row fp-row-db">
725725 <div class="fp-icon"><span class="brand-icon" style="--site-glow:$col" data-letter="$ltr"></span></div>
726726 <div class="fp-name">
727727 <div class="fp-name-h">$name</div>
728728 $sub
729729 </div>
730730 <div class="fp-bar-wrap">$bar</div>
731731 <div class="$sz_class mono"><strong>$sz</strong></div>
732732 </div>~;
733733}
734734$db_rows ||= '<div class="dim" style="padding:14px">No configured sites.</div>';
735735
736736my $db_total_h = MODS::MetaAdmin::Stats::human_size($db_total_b);
737737
738738$body .= qq~
739739<style>
740740.fp-row-db { grid-template-columns: 36px 2.2fr 3fr 1fr; }
741741</style>
742742<div class="module glow-magenta" style="margin-bottom:16px">
743743 <div class="module-head">
744744 <div class="left">
745745 <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>
746746 <div class="module-title">Site databases</div>
747747 <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>
748748 </div>
749749 </div>
750750 <div class="module-body" style="padding:0">
751751 <div class="fp-list">$db_rows</div>
752752 <div class="fp-foot">
753753 <span class="lbl">All databases</span> <span class="val">$db_total_h</span>
754754 <span class="dim" style="margin-left:14px;font-size:11.5px">$db_tables_ct tables total</span>
755755 <span class="free">
756756 <div class="free-lbl">Free on /var (mysql lives here)</div>
757757 <div class="free-val">$var_avail_h</div>
758758 </span>
759759 </div>
760760 </div>
761761</div>
762762~;
763763
764764# Top processes table
765765$body .= qq~
766766<div class="module" style="margin-top:16px">
767767 <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>
768768 <div class="module-body" style="padding:0">
769769 <table class="tbl"><thead><tr><th>PID</th><th>User</th><th>CPU %</th><th>MEM %</th><th>Command</th></tr></thead><tbody>
770770~;
771771foreach my $p (@$tops) {
772772 my $pid = _esc($p->{pid});
773773 my $user = _esc($p->{user});
774774 my $cmd = _esc($p->{cmd});
775775 my $cpu_v = sprintf('%.1f', $p->{cpu});
776776 my $mem_v = sprintf('%.1f', $p->{mem});
777777 $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>~;
778778}
779779$body .= qq~
780780 </tbody></table>
781781 </div>
782782</div>
783783~;
784784
785785# Host info footer
786786$body .= qq~
787787<div style="margin-top:18px;text-align:right;color:var(--col-text-dim);font-size:11px;letter-spacing:1px;text-transform:uppercase">
788788 $os_h &middot; kernel $kernel_h &middot; host $host_h
789789</div>
790790~;
791791
792792MODS::MetaAdmin::Wrapper->new->render(
793793 title => 'Server',
794794 page_key => 'server',
795795 body => $body,
796796 userinfo => $u,
797797);