v0.308.0: controller batch 2026-10-12 — clear-address warning (R-922), secret sweep over every page (R-255), read_failed (R-266), no NVMe heat band (R-333), share globe (R-577), Hungarian dates and numbers (R-562), lost-drive option pinned (R-521)

Operator rulings 2026-10-12, 09 §3 decisions 196, 204-207, 223, 227. MinAgent 0.131.0 unchanged.
R-255's sweep found one leak, fixed here: an optional-config secret_input was rendered into its
settings page; it is now revealed on demand. Hungarian parity fixtures re-captured once for R-562.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-12 15:13:59 +02:00
parent dcd89364a4
commit fd28d70bff
183 changed files with 2671 additions and 411 deletions
@@ -148,9 +148,9 @@ async function loadEstimate() {
// Task 4: the class-scoped selection UI (classified apps only; legacy apps see the plain estimate).
var lastEst = null;
function humanBytes(b) {
if (b >= 1073741824) return (b/1073741824).toFixed(1) + ' GB';
if (b >= 1048576) return (b/1048576).toFixed(1) + ' MB';
if (b >= 1024) return (b/1024).toFixed(1) + ' KB';
if (b >= 1073741824) return fhNum(b/1073741824, 1) + ' GB';
if (b >= 1048576) return fhNum(b/1048576, 1) + ' MB';
if (b >= 1024) return fhNum(b/1024, 1) + ' KB';
return b + ' B';
}
function renderClassSelect(est) {
@@ -49,8 +49,8 @@
{{range .Bundles}}
<tr>
<td><strong>{{if .DisplayName}}{{.DisplayName}}{{else}}{{.AppName}}{{end}}</strong></td>
<td>{{.ExportedAt}}</td>
<td>{{.SizeHuman}}</td>
<td>{{fmtTimeStr .ExportedAt}}</td>
<td>{{numText .SizeHuman}}</td>
<td>{{if .DriveLabel}}{{.DriveLabel}}{{else}}{{.DrivePath}}{{end}}</td>
<td>{{if .Encrypted}}&#128274;{{end}}</td>
<td><button class="btn btn-sm btn-outline" onclick="showPreview('{{.Path}}', {{.Encrypted}})">{{T "app_import.reszletek"}}</button></td>
@@ -195,7 +195,7 @@ function showManifest(m) {
document.getElementById('previewTitle').textContent = (m.display_name || m.app_name) + ' {{T "app_import.csomag_reszletei_2"}}';
document.getElementById('mfName').textContent = m.display_name || m.app_name;
document.getElementById('mfDate').textContent = m.exported_at ? new Date(m.exported_at).toLocaleString('hu-HU') : '-';
document.getElementById('mfDate').textContent = m.exported_at ? fhDateTime(new Date(m.exported_at)) : '-';
document.getElementById('mfVersion').textContent = m.controller_version || '-';
if (m.has_database && m.db_type) {
@@ -212,9 +212,9 @@ function showManifest(m) {
// Format size
var bytes = m.total_size_bytes || 0;
var sizeStr = bytes > 1073741824 ? (bytes / 1073741824).toFixed(1) + ' GB' :
bytes > 1048576 ? (bytes / 1048576).toFixed(1) + ' MB' :
bytes > 1024 ? (bytes / 1024).toFixed(1) + ' KB' : bytes + ' B';
var sizeStr = bytes > 1073741824 ? fhNum(bytes / 1073741824, 1) + ' GB' :
bytes > 1048576 ? fhNum(bytes / 1048576, 1) + ' MB' :
bytes > 1024 ? fhNum(bytes / 1024, 1) + ' KB' : bytes + ' B';
document.getElementById('mfSize').textContent = sizeStr;
// Check if app already exists — show overwrite warning
@@ -317,7 +317,7 @@ function fabPick() {
}
function fabGB(b) {
return (b / 1073741824).toFixed(2);
return fhNum(b / 1073741824, 2);
}
function fabProgress(done, total) {
@@ -175,7 +175,7 @@
<div id="app-migrate-progress" style="display:none;margin-top:.75rem;padding:.75rem;border:1px solid var(--blue);border-radius:6px;background:rgba(0,136,204,0.06)"></div>
</div>
<script>
function appMigFmtGB(b){ return (Number(b||0)/1e9).toFixed(1)+' GB'; }
function appMigFmtGB(b){ return fhNum(Number(b||0)/1e9, 1)+' GB'; }
function appMigRender(job){
var names={stop:'{{T "app_info.leallitas"}}',copy:'{{T "app_info.adatok_masolasa"}}',verify:'{{T "app_info.ellenorzes"}}',flip:'{{T "app_info.ujratelepites"}}',redeploy:'{{T "app_info.ujratelepites"}}',cleanup:'{{T "app_info.regi_adatok_torlese"}}'};
var s=names[job.phase]||job.phase;
@@ -164,7 +164,7 @@
<span class="tag tag-warn">{{T "backups_apps.meghajto_levalasztva"}}</span>
{{else if .HasHDDData}}
{{if .StorageLabel}}<span class="meta-badge meta-badge-storage">{{.StorageLabel}}</span>{{end}}
<span class="mono app-backup-size" style="font-size:.8rem">{{.HDDSizeHuman}}</span>
<span class="mono app-backup-size" style="font-size:.8rem">{{numText .HDDSizeHuman}}</span>
{{else if .HasVolumeData}}
<span class="meta-badge">{{T "backups_apps.konfig"}}{{if .HasDB}} + DB{{end}} {{T "backups_apps.adatok"}}</span>
{{else}}
@@ -277,7 +277,7 @@
<span class="{{if eq .Tier2LastStatus "error"}}text-error{{else}}text-muted{{end}}">{{.Tier2StatusBadge}}</span>
</span>
{{end}}
{{if .Tier2SizeHuman}}<span class="tier-size">{{.Tier2SizeHuman}}</span>{{end}}
{{if .Tier2SizeHuman}}<span class="tier-size">{{numText .Tier2SizeHuman}}</span>{{end}}
{{if .Tier2LastWarning}}<span class="layer-reason" style="color:var(--warn);opacity:.9">{{.Tier2LastWarning}}</span>{{end}}
{{/* R-403: a run that PRESERVED the copy's package instead of refreshing it must
not render as a plain fresh copy. The status line above is about the RUN;
@@ -47,7 +47,7 @@
<div class="stat-label">{{T "backups_remote.utolso_tavoli_mentes"}}{{if .Offbox.LastRun}}<br><span class="relative-time">{{timeAgoStr .Offbox.LastRun}}</span>{{end}}</div>
</div>
<div class="stat-card">
<div class="stat-value" style="font-size:1.15rem">{{if and .Offbox.StatsKnown .Offbox.RepoSizeHuman}}{{.Offbox.RepoSizeHuman}}{{else}}–{{end}}</div>
<div class="stat-value" style="font-size:1.15rem">{{if and .Offbox.StatsKnown .Offbox.RepoSizeHuman}}{{numText .Offbox.RepoSizeHuman}}{{else}}–{{end}}</div>
<div class="stat-label">{{T "backups_remote.tarolo_meret"}} {{if .Offbox.StatsKnown}}{{T "backups_remote.pillanatkep"}}{{else}}{{T "backups_remote.a_pillanatkepek_szama_meg_ismeretlen"}}{{end}}</div>
</div>
<div class="stat-card">
@@ -60,7 +60,7 @@
picture of emptiness, and a picture is a claim. -->
<!-- SLICE 4: soft-quota usage bar (shared model; quota_gb from the hub descriptor). -->
<div id="offbox-quota-bar" style="max-width:560px;margin:.5rem 0">
<div class="stat-label" style="margin-bottom:.25rem">{{T "backups_remote.tarhelykeret"}} {{if .Offbox.StatsKnown}}{{if .Offbox.RepoSizeHuman}}{{.Offbox.RepoSizeHuman}}{{else}}0{{end}} / {{.Offbox.QuotaGB}} GB ({{.OffboxQuotaPct}}%){{else}}{{T "backups_remote.meg_nem_tudjuk_mennyi_van"}}{{end}}</div>
<div class="stat-label" style="margin-bottom:.25rem">{{T "backups_remote.tarhelykeret"}} {{if .Offbox.StatsKnown}}{{if .Offbox.RepoSizeHuman}}{{numText .Offbox.RepoSizeHuman}}{{else}}0{{end}} / {{.Offbox.QuotaGB}} GB ({{.OffboxQuotaPct}}%){{else}}{{T "backups_remote.meg_nem_tudjuk_mennyi_van"}}{{end}}</div>
{{if .Offbox.StatsKnown}}<div style="background:var(--border,#334);border-radius:4px;height:8px;overflow:hidden">
<div style="height:8px;border-radius:4px;width:{{.OffboxQuotaPct}}%;background:{{if ge .OffboxQuotaPct 100}}var(--crit,#e5484d){{else if ge .OffboxQuotaPct 80}}var(--warn,#f5a524){{else}}var(--ok,#30a46c){{end}}"></div>
</div>{{end}}
@@ -174,7 +174,7 @@
<p class="form-hint" style="margin-bottom:.75rem">{{T "backups_restore.ezek_a_visszaallitott_masolatok_helyet"}}</p>
<div class="app-row-list">
{{range .OffsiteRestoreCopies}}
{{template "app_list_row" dict "Slug" .Stack "Name" .Stack "Secondary" (printf "%s · %s · %s" .SizeHuman (.Created.Format "2006. 01. 02. 15:04") .Path)}}
{{template "app_list_row" dict "Slug" .Stack "Name" .Stack "Secondary" (printf "%s · %s · %s" (numText .SizeHuman) (fmtTime .Created) .Path)}}
<form method="POST" action="/backup/offbox/verify-copy/delete" style="display:inline">
{{$.CSRFField}}
<input type="hidden" name="stack" value="{{.Stack}}">
@@ -299,7 +299,7 @@ var huDays = ['{{T "backups_restore.vasarnap"}}', '{{T "backups_restore.hetfo"}}
function formatSnapshot(s) {
var t = new Date(s.time);
var pad = function(n) { return n < 10 ? '0' + n : '' + n; };
var label = t.getFullYear() + '-' + pad(t.getMonth()+1) + '-' + pad(t.getDate()) +
var label = fhIsoDate(t.getFullYear() + '-' + pad(t.getMonth()+1) + '-' + pad(t.getDate())) +
' ' + huDays[t.getDay()] + ' ' + pad(t.getHours()) + ':' + pad(t.getMinutes()) +
' (' + s.short_id + ')';
var tierLabel = s.tier === 2 ? '{{T "backups_restore.2_szint"}}' : '{{T "backups_restore.1_szint"}}';
+22 -5
View File
@@ -59,7 +59,7 @@
{{if .StorageInfo.DataSizeHuman}}
<div class="settings-row">
<span class="settings-label">{{T "deploy.adatmeret"}}</span>
<span class="settings-value mono">{{.StorageInfo.DataSizeHuman}}</span>
<span class="settings-value mono">{{numText .StorageInfo.DataSizeHuman}}</span>
</div>
{{end}}
{{if .StorageInfo.FreeHuman}}
@@ -91,7 +91,7 @@
</div>
<div class="settings-row">
<span class="settings-label">{{T "deploy.meret"}}</span>
<span class="settings-value mono">{{.SizeHuman}}</span>
<span class="settings-value mono">{{numText .SizeHuman}}</span>
</div>
<div class="settings-row">
<span class="settings-label">{{T "deploy.mappak"}}</span>
@@ -328,13 +328,26 @@
<label for="opt-{{.EnvVar}}">{{.Label}}</label>
{{if .HelpText}}<p class="config-field-help">{{.HelpText}}</p>{{end}}
{{if .HelpURL}}<p class="config-field-help"><a href="{{.HelpURL}}" target="_blank">{{T "deploy.regisztracios_utmutato"}}</a></p>{{end}}
<input type="{{if eq .Type "secret_input"}}password{{else}}text{{end}}"
{{if eq .Type "secret_input"}}
{{/* R-255 (v0.308.0): a stored secret is NOT in the page; it is fetched on demand like the
auto-generated ones (R-254). A blank field that was never revealed is not sent, so
saving other fields keeps it. */}}
{{$set := index $.CurrentValues .EnvVar}}
<div class="input-with-button">
<input type="password" id="opt-{{.EnvVar}}" name="{{.EnvVar}}" class="config-input"
value="" placeholder="{{if $set}}••••••••••••{{else}}{{.Label}}{{end}}"
{{if $set}}data-secret-set="1" {{end}}autocomplete="off">
{{if $set}}<button type="button" class="btn btn-sm btn-outline" onclick="revealAutoField('{{$.Stack.Name}}','{{.EnvVar}}', this, 'opt-{{.EnvVar}}')">{{T "deploy.megjelenites"}}</button>{{end}}
</div>
{{else}}
<input type="text"
id="opt-{{.EnvVar}}"
name="{{.EnvVar}}"
class="config-input"
value="{{index $.CurrentValues .EnvVar}}"
placeholder="{{.Label}}"
autocomplete="off">
{{end}}
</div>
{{end}}
</div>
@@ -357,6 +370,8 @@
var values = {};
inputs.forEach(function(input) {
// R-255: a stored secret the household never revealed and left blank stays as it is.
if (input.dataset.secretSet === '1' && input.dataset.revealed !== '1' && input.value === '') return;
values[input.name] = input.value;
});
@@ -836,15 +851,17 @@ function revealPasswordField(stackName, envVar, btn) {
if (!j.ok) { showAlert(j.error || '{{T "deploy.a_lekeres_nem_sikerult"}}'); return; }
el.value = j.data.value;
el.type = 'text';
el.dataset.revealed = '1';
btn.dataset.shown = '1';
}).catch(function () { btn.disabled = false; showAlert('{{T "deploy.a_lekeres_nem_sikerult"}}'); });
}
function revealAutoField(stackName, envVar, btn) {
var el = document.getElementById('auto-field-' + envVar);
function revealAutoField(stackName, envVar, btn, elId) {
var el = document.getElementById(elId || ('auto-field-' + envVar));
if (!el) return;
if (btn.dataset.shown === '1') {
el.value = '';
el.type = 'password';
el.dataset.revealed = '';
btn.dataset.shown = '';
btn.textContent = '{{T "deploy.megjelenites"}}';
return;
@@ -20,6 +20,8 @@
<input type="password" name="password" placeholder="{{T "launcher_share_password.jelszo"}}" autocomplete="current-password" autofocus>
<button type="submit" class="btn btn-primary">{{T "launcher_share_password.belepes"}}</button>
</form>
{{/* R-577 (decision 207): the guest picks their own language; the globe posts to /lang (their cookie). */}}
<div class="shell-lang">{{template "lang_globe" .}}</div>
</div>
</main>
</body>
@@ -37,6 +37,8 @@
<p>{{T "launcher_shared.jelenleg_nincs_elerheto_alkalmazas"}}</p>
</div>
{{end}}
{{/* R-577 (decision 207): the guest picks their own language; the globe posts to /lang (their cookie). */}}
<div class="shell-lang">{{template "lang_globe" .}}</div>
</main>
</body>
</html>
@@ -16,6 +16,14 @@
// mobile off-canvas drawer engages only when the drawer JS below can actually open it.
document.documentElement.className=document.documentElement.className.replace('no-js','js');
function csrfHeaders(){var el=document.querySelector('meta[name="csrf-token"]');return el?{'X-CSRF-Token':el.content}:{};}
{{/* R-562 (decision 223): numbers and dates in the page's language. The browser twin of internal/i18n
format.go: Hungarian 2026. 10. 10. 15:04 and a decimal comma, English 2026-10-10 15:04 and a point.
sv-SE is used only because it prints the ISO-like order; the Hungarian form is built from it. */ -}}
var fhEN=document.documentElement.lang==='en';
function fhNum(v,prec){var s=Number(v).toFixed(prec);return fhEN?s:s.replace('.',',');}
function fhIsoDate(iso){return fhEN?iso:iso.replace(/^(\d{4})-(\d{2})-(\d{2})/,'$1. $2. $3.');}
function fhDateTime(d){return fhIsoDate(d.toLocaleString('sv-SE',{timeZone:'Europe/Budapest',year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}));}
function fhMonthDay(d){var s=d.toLocaleDateString('sv-SE',{timeZone:'Europe/Budapest',month:'2-digit',day:'2-digit'});return fhEN?s:s.replace(/^(\d{2})-(\d{2})$/,'$1. $2.');}
// Inline two-step confirm (drill F-11): the trigger swaps IN PLACE to "<question> Igen/Mégse".
// Never native confirm() — it is an OS-modal that blocks browser automation — and no overlay:
// this is the LIGHT pattern for one-click consequential actions. Form buttons opt in with
@@ -363,7 +363,7 @@
if (isNaN(ts)) return '';
// ts should already be in ms (linear axis stores ms values)
var d = new Date(ts);
return d.toLocaleString('hu-HU', {timeZone: budaTZ, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit'});
return fhDateTime(d);
}
function formatTimeLabel(ts) {
@@ -372,7 +372,7 @@
if (isNaN(ts)) return '';
var d = new Date(ts);
if (currentTickFormat === 'date') {
return d.toLocaleDateString('hu-HU', {timeZone: budaTZ, month: '2-digit', day: '2-digit'});
return fhMonthDay(d);
}
return d.toLocaleTimeString('hu-HU', {timeZone: budaTZ, hour: '2-digit', minute: '2-digit'});
}
@@ -586,7 +586,7 @@
var osMB = Math.max(0, usedMB - appTotal);
var freeMB = Math.max(0, totalMB - usedMB);
function fmtMB(mb) { return mb >= 1024 ? (mb/1024).toFixed(1) + ' GB' : Math.round(mb) + ' MB'; }
function fmtMB(mb) { return mb >= 1024 ? fhNum(mb/1024, 1) + ' GB' : Math.round(mb) + ' MB'; }
header.textContent = '{{T "monitoring.hasznalt"}} ' + fmtMB(usedMB) + ' / ' + fmtMB(totalMB) + ' (' + Math.round(usedMB/totalMB*100) + '%)';
@@ -696,9 +696,9 @@
function fmtBytesGB(bytes) {
if (!bytes || bytes <= 0) return '0 GB';
var gb = bytes / 1073741824;
if (gb >= 1024) return (gb / 1024).toFixed(2) + ' TB';
if (gb >= 1024) return fhNum(gb / 1024, 2) + ' TB';
if (gb >= 100) return Math.round(gb) + ' GB';
return gb.toFixed(1) + ' GB';
return fhNum(gb, 1) + ' GB';
}
function escapeHtml(s) {
@@ -720,7 +720,7 @@
// CPU % + load average
document.getElementById('host-cpu').textContent =
(h.cpu_percent != null ? h.cpu_percent.toFixed(1) : '–') + '%';
(h.cpu_percent != null ? fhNum(h.cpu_percent, 1) : '–') + '%';
var load = Array.isArray(h.loadavg) ? h.loadavg.join(' ') : '–';
document.getElementById('host-load').textContent = load || '–';
@@ -824,7 +824,7 @@
}
if (d.boot_time) {
var bt = new Date(d.boot_time);
document.getElementById('sysinfo-boot').textContent = bt.toLocaleString('hu-HU', {timeZone: budaTZ, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit'});
document.getElementById('sysinfo-boot').textContent = fhDateTime(bt);
}
} catch(e) {
console.error('Failed to load sysinfo:', e);
@@ -12,7 +12,11 @@
{{if .NotificationSuccess}}<div class="alert alert-info">{{.NotificationSuccess}}</div>{{end}}
{{if .NotificationError}}<div class="alert alert-error">{{.NotificationError}}</div>{{end}}
<form method="POST" action="/settings/notifications">
{{.CSRFField}}
{{.CSRFField}}{{if .NotificationClearConfirm}}
<div class="alert alert-warning" id="notification-clear-warning">
<p>{{T "settings_notifications.clear_warning"}}</p>
<button type="submit" name="confirm_clear" value="1" class="btn btn-danger">{{T "settings_notifications.clear_confirm"}}</button>
</div>{{end}}
<div class="form-group">
<label for="notification_email">{{T "settings_notifications.e_mail_cim"}}</label>
<input type="email" id="notification_email" name="notification_email"
@@ -172,7 +172,7 @@
<strong>{{T "sharing.2_mentes_masik_meghajto"}}</strong>
{{if .SharesTier2Status}}
{{if eq .SharesTier2Status "ok"}}
{{T "sharing.rendben"}}{{if .SharesTier2LastRun}} — {{.SharesTier2LastRun}}{{end}}
{{T "sharing.rendben"}}{{if .SharesTier2LastRun}} — {{fmtTimeStr .SharesTier2LastRun}}{{end}}
{{if .SharesTier2Warning}}<span class="badge badge-warn">{{.SharesTier2Warning}}</span>{{end}}
{{else if eq .SharesTier2Status "no_target"}}
<span class="badge badge-warn">{{T "sharing.nincs_cel"}}</span>
@@ -187,7 +187,7 @@
<strong>{{T "sharing.tavoli_mentes_felho"}}</strong>
{{if .SharesOffsiteStatus}}
{{if eq .SharesOffsiteStatus "ok"}}
{{T "sharing.rendben_megosztas"}}{{if .SharesOffsiteLastRun}}, {{.SharesOffsiteLastRun}}{{end}}
{{T "sharing.rendben_megosztas"}}{{if .SharesOffsiteLastRun}}, {{fmtTimeStr .SharesOffsiteLastRun}}{{end}}
{{else if eq .SharesOffsiteStatus "blocked"}}
<span class="badge badge-warn">{{T "sharing.a_tarhelykeret_miatt_csak_a"}}</span>
{{else if eq .SharesOffsiteStatus "skipped"}}
@@ -72,7 +72,7 @@
{{if .DiskInfo}}
<div class="storage-path-disk">
<div class="system-info-header">
<span class="system-info-value">{{.DiskInfo.UsedHuman}} / {{.DiskInfo.TotalHuman}}</span>
<span class="system-info-value">{{numText .DiskInfo.UsedHuman}} / {{numText .DiskInfo.TotalHuman}}</span>
</div>
<div class="meter {{if ge .DiskInfo.UsedPercent 90.0}}crit{{else if ge .DiskInfo.UsedPercent 70.0}}warn{{else}}nominal{{end}}">
<div class="meter-track">
@@ -102,7 +102,7 @@
{{range .AppDetails}}
<div class="storage-app-row">
<a href="/apps/{{.Stack}}" class="storage-app-link">{{.Name}}</a>
{{if .SizeHuman}}<span class="mono form-hint">{{.SizeHuman}}</span>{{end}}
{{if .SizeHuman}}<span class="mono form-hint">{{numText .SizeHuman}}</span>{{end}}
<span class="btn btn-xs btn-outline" style="opacity:.45;cursor:not-allowed" title="{{T "storage.hamarosan"}}"><svg class="ico ico-sm"><use href="#i-upload"/></svg> {{T "storage.mozgatas"}}</span>
</div>
{{end}}
@@ -181,7 +181,7 @@
</div>
<script>
// Shared migration progress (used by both migrate-all here and per-app migrate on the app page).
function migFmtGB(b){ return (Number(b||0)/1e9).toFixed(1)+' GB'; }
function migFmtGB(b){ return fhNum(Number(b||0)/1e9, 1)+' GB'; }
function migRender(job){
var names={stop:'{{T "storage.alkalmazasok_leallitasa"}}',copy:'{{T "storage.adatok_masolasa"}}',verify:'{{T "storage.ellenorzes"}}',flip:'{{T "storage.ujratelepites"}}',redeploy:'{{T "storage.ujratelepites"}}',cleanup:'{{T "storage.forras_torlese"}}'};
var s=names[job.phase]||job.phase;
@@ -247,7 +247,7 @@
window.__registeredPaths=[{{range .StoragePaths}}{{if .Path}}"{{.Path}}",{{end}}{{end}}];
(function(){
function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c];}); }
function hum(b){ if(!b||b<=0) return ''; var u=['B','KB','MB','GB','TB'],i=0,v=b; while(v>=1024&&i<u.length-1){v/=1024;i++;} return (v>=10||i===0?Math.round(v):v.toFixed(1))+' '+u[i]; }
function hum(b){ if(!b||b<=0) return ''; var u=['B','KB','MB','GB','TB'],i=0,v=b; while(v>=1024&&i<u.length-1){v/=1024;i++;} return (v>=10||i===0?Math.round(v):fhNum(v, 1))+' '+u[i]; }
function usageColorClass(p){ if(p>=85) return 'crit'; if(p>=70) return 'warn'; return 'nominal'; }
var LOCK_ICO='<svg class="ico ico-sm"><use href="#i-lock"/></svg>';
function classTag(d){
@@ -58,7 +58,7 @@ function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,function(c){retur
function fmtSize(b){
if(!b) return '';
var g=b/1073741824;
return g>=1 ? g.toFixed(g>=10?0:1)+' GB' : Math.round(b/1048576)+' MB';
return g>=1 ? fhNum(g, g>=10?0:1)+' GB' : Math.round(b/1048576)+' MB';
}
async function loadDisks(){
@@ -69,7 +69,7 @@ function dataBadge(d){
function fmtSize(b){
if(!b) return '';
var g=b/1073741824;
return g>=1 ? g.toFixed(g>=10?0:1)+' GB' : Math.round(b/1048576)+' MB';
return g>=1 ? fhNum(g, g>=10?0:1)+' GB' : Math.round(b/1048576)+' MB';
}
async function loadDisks(){