@stacksjs/ts-cloud 0.7.104 → 0.7.108
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aws/index.js +2 -14
- package/dist/bin/cli.js +784 -785
- package/dist/bin/dashboard-server.js +291 -291
- package/dist/{chunk-dmzez5f8.js → chunk-33sy72r8.js} +52 -3
- package/dist/{chunk-5cz14833.js → chunk-ef2dwfrg.js} +3 -3
- package/dist/{chunk-gtz4jar2.js → chunk-eg8ck25v.js} +859 -151
- package/dist/{chunk-6hymgy61.js → chunk-gttvakpv.js} +1 -85
- package/dist/{chunk-adgz2c49.js → chunk-s9c2x5jv.js} +2 -2
- package/dist/deploy/dashboard-bundle-encoding.test.d.ts +1 -0
- package/dist/deploy/dashboard-payload.d.ts +20 -0
- package/dist/deploy/dashboard-payload.test.d.ts +1 -0
- package/dist/deploy/dashboard-topology.d.ts +85 -0
- package/dist/deploy/dashboard-topology.test.d.ts +1 -0
- package/dist/deploy/egress-collection.d.ts +60 -0
- package/dist/deploy/index.d.ts +2 -0
- package/dist/deploy/index.js +16 -4
- package/dist/dns/index.d.ts +0 -2
- package/dist/dns/index.js +2 -14
- package/dist/drivers/index.js +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.js +5 -17
- package/dist/ui/404.html +1 -0
- package/dist/ui/access-denied.html +2 -2
- package/dist/ui/account/automation.html +4 -4
- package/dist/ui/account/security.html +2 -2
- package/dist/ui/applications/compose.html +3 -3
- package/dist/ui/applications/new.html +2 -2
- package/dist/ui/data/backups.html +22 -9
- package/dist/ui/data/services.html +22 -9
- package/dist/ui/data/volumes.html +22 -9
- package/dist/ui/index.html +22 -9
- package/dist/ui/infrastructure/topology.html +2223 -0
- package/dist/ui/integrations.html +2 -2
- package/dist/ui/operations/alerts.html +22 -9
- package/dist/ui/operations/configuration.html +4 -4
- package/dist/ui/operations/jobs.html +22 -9
- package/dist/ui/operations/maintenance.html +4 -4
- package/dist/ui/operations/observability.html +21 -8
- package/dist/ui/operations/previews.html +4 -4
- package/dist/ui/operations/queue.html +4 -4
- package/dist/ui/operations/regions.html +3 -3
- package/dist/ui/operations/releases.html +4 -4
- package/dist/ui/operations/workloads.html +22 -9
- package/dist/ui/security.html +2 -2
- package/dist/ui/server/actions.html +4 -4
- package/dist/ui/server/activity.html +2 -2
- package/dist/ui/server/capacity.html +4 -4
- package/dist/ui/server/database.html +4 -4
- package/dist/ui/server/deployments.html +4 -4
- package/dist/ui/server/diagnostics.html +2 -2
- package/dist/ui/server/firewall.html +3 -3
- package/dist/ui/server/fleet.html +22 -9
- package/dist/ui/server/logs.html +22 -9
- package/dist/ui/server/metrics.html +130 -14
- package/dist/ui/server/security.html +2 -2
- package/dist/ui/server/services.html +2 -2
- package/dist/ui/server/sites.html +21 -8
- package/dist/ui/server/ssh-keys.html +4 -4
- package/dist/ui/server/team.html +4 -4
- package/dist/ui/server/terminal.html +1 -1
- package/dist/ui/serverless/alarms.html +4 -4
- package/dist/ui/serverless/assets.html +2 -2
- package/dist/ui/serverless/cost.html +1 -1
- package/dist/ui/serverless/data.html +4 -4
- package/dist/ui/serverless/deployments.html +2 -2
- package/dist/ui/serverless/firewall.html +2 -2
- package/dist/ui/serverless/functions.html +4 -4
- package/dist/ui/serverless/logs.html +22 -9
- package/dist/ui/serverless/metrics.html +2 -2
- package/dist/ui/serverless/queues.html +4 -4
- package/dist/ui/serverless/secrets.html +4 -4
- package/dist/ui/serverless/traces.html +4 -4
- package/dist/ui/serverless.html +4 -4
- package/dist/ui-src/functions/requestJson.ts +41 -0
- package/dist/ui-src/functions/useOpConfirm.ts +132 -0
- package/dist/ui-src/functions/usePolling.ts +98 -0
- package/dist/ui-src/functions/useTerminal.ts +79 -0
- package/dist/ui-src/pages/infrastructure/topology.stx +517 -0
- package/dist/ui-src/pages/partials/nav.stx +2 -0
- package/dist/ui-src/pages/server/metrics.stx +140 -6
- package/dist/ui-src/src/topology.ts +485 -0
- package/package.json +3 -3
- package/dist/dns/mail-records.d.ts +0 -82
|
@@ -25,6 +25,27 @@ const bytes = value => {
|
|
|
25
25
|
if (n >= 1024 ** 2) return (n / 1024 ** 2).toFixed(1) + ' MB'
|
|
26
26
|
return Math.round(n / 1024) + ' KB'
|
|
27
27
|
}
|
|
28
|
+
const rate = value => {
|
|
29
|
+
const n = Number(value || 0)
|
|
30
|
+
if (n >= 1024 ** 3) return (n / 1024 ** 3).toFixed(2) + ' GB/s'
|
|
31
|
+
if (n >= 1024 ** 2) return (n / 1024 ** 2).toFixed(1) + ' MB/s'
|
|
32
|
+
if (n >= 1024) return Math.round(n / 1024) + ' KB/s'
|
|
33
|
+
return Math.round(n) + ' B/s'
|
|
34
|
+
}
|
|
35
|
+
const monthBytes = Number(m.networkRxBytesMonth || 0) + Number(m.networkTxBytesMonth || 0)
|
|
36
|
+
// Providers quote allowances in decimal TB and bill overage the same way, so the
|
|
37
|
+
// projection has to use the same unit the invoice will.
|
|
38
|
+
const bandwidthBudget = Number(__d?.bandwidthBudgetBytes || 0)
|
|
39
|
+
const bandwidthNote = (() => {
|
|
40
|
+
const now = new Date()
|
|
41
|
+
const dayOfMonth = now.getUTCDate()
|
|
42
|
+
const daysInMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 0)).getUTCDate()
|
|
43
|
+
const projected = dayOfMonth > 0 ? monthBytes / dayOfMonth * daysInMonth : monthBytes
|
|
44
|
+
const tb = value => (value / 1000 ** 4).toFixed(2) + ' TB'
|
|
45
|
+
if (!bandwidthBudget) return `On track for ${tb(projected)} by month end`
|
|
46
|
+
const pct = Math.round(monthBytes / bandwidthBudget * 100)
|
|
47
|
+
return `${pct}% of ${tb(bandwidthBudget)} — on track for ${tb(projected)}`
|
|
48
|
+
})()
|
|
28
49
|
const memUsedGb = gb(m.memUsedMb)
|
|
29
50
|
const memTotalGb = gb(m.memTotalMb)
|
|
30
51
|
const memAvailableGb = gb(m.memAvailableMb || Math.max(0, m.memTotalMb - m.memUsedMb))
|
|
@@ -75,6 +96,10 @@ function historyParams() {
|
|
|
75
96
|
'host.memory.used_percent', 'host.memory.used', 'host.memory.total',
|
|
76
97
|
'host.disk.used_percent', 'host.swap.used_percent',
|
|
77
98
|
'host.disk.inode_used_percent',
|
|
99
|
+
'host.network.rx_rate', 'host.network.tx_rate',
|
|
100
|
+
'storage.egress.day', 'storage.egress.month',
|
|
101
|
+
'storage.egress.budget', 'storage.egress.budget_used_percent',
|
|
102
|
+
'storage.egress.projected_month',
|
|
78
103
|
].join(','),
|
|
79
104
|
aggregation: 'avg',
|
|
80
105
|
bucketMs: String(bucketMs),
|
|
@@ -117,6 +142,27 @@ function liveBytes(value) {
|
|
|
117
142
|
if (n >= 1024 ** 2) return (n / 1024 ** 2).toFixed(1) + ' MB'
|
|
118
143
|
return Math.round(n / 1024) + ' KB'
|
|
119
144
|
}
|
|
145
|
+
function liveRate(value) {
|
|
146
|
+
const n = Number(value || 0)
|
|
147
|
+
if (n >= 1024 ** 3) return (n / 1024 ** 3).toFixed(2) + ' GB/s'
|
|
148
|
+
if (n >= 1024 ** 2) return (n / 1024 ** 2).toFixed(1) + ' MB/s'
|
|
149
|
+
if (n >= 1024) return Math.round(n / 1024) + ' KB/s'
|
|
150
|
+
return Math.round(n) + ' B/s'
|
|
151
|
+
}
|
|
152
|
+
function liveMonthBytes() {
|
|
153
|
+
return Number(liveMetric().networkRxBytesMonth || 0) + Number(liveMetric().networkTxBytesMonth || 0)
|
|
154
|
+
}
|
|
155
|
+
function liveBandwidthNote() {
|
|
156
|
+
const used = liveMonthBytes()
|
|
157
|
+
const budget = Number(currentData()?.bandwidthBudgetBytes || 0)
|
|
158
|
+
const now = new Date()
|
|
159
|
+
const dayOfMonth = now.getUTCDate()
|
|
160
|
+
const daysInMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 0)).getUTCDate()
|
|
161
|
+
const projected = dayOfMonth > 0 ? used / dayOfMonth * daysInMonth : used
|
|
162
|
+
const tb = value => (value / 1000 ** 4).toFixed(2) + ' TB'
|
|
163
|
+
if (!budget) return `On track for ${tb(projected)} by month end`
|
|
164
|
+
return `${Math.round(used / budget * 100)}% of ${tb(budget)} — on track for ${tb(projected)}`
|
|
165
|
+
}
|
|
120
166
|
const liveCards = derived(() => [
|
|
121
167
|
{ label: 'CPU utilization', pct: liveCpu(), detail: `${liveMetric().load} load across ${liveMetric().cpus} vCPU` },
|
|
122
168
|
{ label: 'Memory', pct: liveMemory(), detail: `${liveGb(liveMetric().memUsedMb)} / ${liveGb(liveMetric().memTotalMb)} GB` },
|
|
@@ -133,7 +179,50 @@ function ratioPoints(primary, numerator, denominator, multiplier = 100) {
|
|
|
133
179
|
.filter(point => Number(den.get(point.timestamp)) > 0)
|
|
134
180
|
.map(point => ({ ...point, value: Math.max(0, Math.min(100, Number(point.value) / Number(den.get(point.timestamp)) * multiplier)) }))
|
|
135
181
|
}
|
|
182
|
+
const RATE_METRICS = { 'network-in': 'host.network.rx_rate', 'network-out': 'host.network.tx_rate' }
|
|
183
|
+
const BYTE_METRICS = { 'egress-daily': 'storage.egress.day' }
|
|
184
|
+
function isByteMetric(name) { return Object.hasOwn(BYTE_METRICS, name) }
|
|
185
|
+
function bytesLabel(value) {
|
|
186
|
+
const n = Number(value || 0)
|
|
187
|
+
if (n >= 1000 ** 4) return (n / 1000 ** 4).toFixed(2) + ' TB'
|
|
188
|
+
if (n >= 1000 ** 3) return (n / 1000 ** 3).toFixed(1) + ' GB'
|
|
189
|
+
if (n >= 1000 ** 2) return (n / 1000 ** 2).toFixed(1) + ' MB'
|
|
190
|
+
return Math.round(n / 1000) + ' KB'
|
|
191
|
+
}
|
|
192
|
+
function latestValue(name) {
|
|
193
|
+
const values = points(name)
|
|
194
|
+
return values.length ? Number(values.at(-1).value) : null
|
|
195
|
+
}
|
|
196
|
+
function egressBudget() { return latestValue('storage.egress.budget') }
|
|
197
|
+
function egressMonth() { return latestValue('storage.egress.month') }
|
|
198
|
+
function egressProjected() { return latestValue('storage.egress.projected_month') }
|
|
199
|
+
function egressPercent() { return latestValue('storage.egress.budget_used_percent') }
|
|
200
|
+
function hasEgress() { return points('storage.egress.day').length > 0 || egressMonth() != null }
|
|
201
|
+
function egressTone() {
|
|
202
|
+
const pct = egressPercent()
|
|
203
|
+
if (pct == null) return 'ok'
|
|
204
|
+
return pct >= 90 ? 'bad' : pct >= 75 ? 'warn' : 'ok'
|
|
205
|
+
}
|
|
206
|
+
function egressSummary() {
|
|
207
|
+
const month = egressMonth()
|
|
208
|
+
if (month == null) return 'No egress reported yet.'
|
|
209
|
+
const budget = egressBudget()
|
|
210
|
+
const projected = egressProjected()
|
|
211
|
+
const projectedNote = projected == null ? '' : ` — on track for ${bytesLabel(projected)}`
|
|
212
|
+
if (!budget) return `${bytesLabel(month)} this month${projectedNote}`
|
|
213
|
+
return `${bytesLabel(month)} of ${bytesLabel(budget)} (${egressPercent() ?? 0}%)${projectedNote}`
|
|
214
|
+
}
|
|
215
|
+
// A projection above the allowance is the whole point of the panel: it is the
|
|
216
|
+
// window in which the overrun is still avoidable.
|
|
217
|
+
function egressWillExceed() {
|
|
218
|
+
const budget = egressBudget()
|
|
219
|
+
const projected = egressProjected()
|
|
220
|
+
return !!budget && projected != null && projected > budget
|
|
221
|
+
}
|
|
222
|
+
function isRateMetric(name) { return Object.hasOwn(RATE_METRICS, name) }
|
|
136
223
|
function metricPoints(name) {
|
|
224
|
+
if (isRateMetric(name)) return points(RATE_METRICS[name])
|
|
225
|
+
if (isByteMetric(name)) return points(BYTE_METRICS[name])
|
|
137
226
|
if (name === 'cpu') return ratioPoints('host.cpu.used_percent', 'host.load', 'host.cpu.capacity')
|
|
138
227
|
if (name === 'memory') return ratioPoints('host.memory.used_percent', 'host.memory.used', 'host.memory.total')
|
|
139
228
|
if (name === 'disk') return points('host.disk.used_percent')
|
|
@@ -144,7 +233,9 @@ function metricPoints(name) {
|
|
|
144
233
|
function linePath(metric) {
|
|
145
234
|
const values = metricPoints(metric)
|
|
146
235
|
if (!values.length) return ''
|
|
147
|
-
const max = metric === 'load'
|
|
236
|
+
const max = metric === 'load' || isRateMetric(metric) || isByteMetric(metric)
|
|
237
|
+
? Math.max(1, ...values.map(point => Number(point.value)))
|
|
238
|
+
: 100
|
|
148
239
|
return values.map((point, index) => {
|
|
149
240
|
const x = values.length === 1 ? 50 : index / (values.length - 1) * 100
|
|
150
241
|
const y = 42 - Math.max(0, Math.min(1, Number(point.value) / max)) * 38
|
|
@@ -163,7 +254,12 @@ function peak(metric) {
|
|
|
163
254
|
const values = metricPoints(metric)
|
|
164
255
|
return values.length ? Math.max(...values.map(point => Number(point.value))) : null
|
|
165
256
|
}
|
|
166
|
-
function display(value, suffix = '%') {
|
|
257
|
+
function display(value, suffix = '%') {
|
|
258
|
+
if (value == null) return 'No data'
|
|
259
|
+
if (suffix === 'rate') return liveRate(value)
|
|
260
|
+
if (suffix === 'bytes') return bytesLabel(value)
|
|
261
|
+
return `${value.toFixed(value >= 10 ? 0 : 1)}${suffix}`
|
|
262
|
+
}
|
|
167
263
|
function samples(metric) { return metricPoints(metric).length }
|
|
168
264
|
const historyCards = [
|
|
169
265
|
{ key: 'cpu', label: 'CPU utilization', note: 'Actual busy time, with load-per-core fallback for older samples', tone: 'blue' },
|
|
@@ -172,6 +268,8 @@ const historyCards = [
|
|
|
172
268
|
{ key: 'swap', label: 'Swap usage', note: 'Swap pressure across the selected period', tone: 'violet' },
|
|
173
269
|
{ key: 'load', label: 'Load average', note: 'One-minute runnable and waiting task average', tone: 'cyan', suffix: '' },
|
|
174
270
|
{ key: 'inodes', label: 'Inode usage', note: 'Root filesystem inode capacity consumed', tone: 'rose' },
|
|
271
|
+
{ key: 'network-out', label: 'Network out', note: 'Egress throughput — the metric providers meter and bill', tone: 'amber', suffix: 'rate' },
|
|
272
|
+
{ key: 'network-in', label: 'Network in', note: 'Ingress throughput across all non-loopback interfaces', tone: 'blue', suffix: 'rate' },
|
|
175
273
|
]
|
|
176
274
|
function setRange(value) { historyRange.set(value); refresh() }
|
|
177
275
|
</script>
|
|
@@ -214,8 +312,9 @@ function setRange(value) { historyRange.set(value); refresh() }
|
|
|
214
312
|
<div class="vital"><div class="vlabel">Memory available</div><div class="vval">{{ liveGb(liveMetric().memAvailableMb) }} <small>GB</small></div><div class="vnote">{{ liveGb(liveMetric().memCacheMb) }} GB cache</div></div>
|
|
215
313
|
<div class="vital"><div class="vlabel">Disk inodes</div><div class="vval">{{ liveMetric().inodeUsedPct }}<small>% used</small></div><div class="vnote">Filesystem entry capacity</div></div>
|
|
216
314
|
<div class="vital"><div class="vlabel">Processes</div><div class="vval">{{ liveMetric().processes }}</div><div class="vnote">Current process count</div></div>
|
|
217
|
-
<div class="vital"><div class="vlabel">Network
|
|
218
|
-
<div class="vital"><div class="vlabel">Network
|
|
315
|
+
<div class="vital"><div class="vlabel">Network in</div><div class="vval">{{ liveRate(liveMetric().networkRxBytesPerSec) }}</div><div class="vnote">{{ liveBytes(liveMetric().networkRxBytesToday) }} today</div></div>
|
|
316
|
+
<div class="vital"><div class="vlabel">Network out</div><div class="vval">{{ liveRate(liveMetric().networkTxBytesPerSec) }}</div><div class="vnote">{{ liveBytes(liveMetric().networkTxBytesToday) }} today</div></div>
|
|
317
|
+
<div class="vital"><div class="vlabel">Bandwidth this month</div><div class="vval">{{ liveBytes(liveMonthBytes()) }}</div><div class="vnote">{{ liveBandwidthNote() }}</div></div>
|
|
219
318
|
<div class="vital"><div class="vlabel">Disk free</div><div class="vval">{{ Math.max(0, Number(liveMetric().diskTotalGb) - Number(liveMetric().diskUsedGb)) }} <small>GB</small></div><div class="vnote">{{ liveMetric().diskTotalGb }} GB total</div></div>
|
|
220
319
|
<div class="vital"><div class="vlabel">Swap</div><div class="vval">{{ liveSwap() }}<small>% used</small></div><div class="vnote">{{ liveGb(liveMetric().swapUsedMb) }} / {{ liveGb(liveMetric().swapTotalMb) }} GB</div></div>
|
|
221
320
|
</div>
|
|
@@ -257,8 +356,9 @@ function setRange(value) { historyRange.set(value); refresh() }
|
|
|
257
356
|
<div class="vital"><div class="vlabel">Memory available</div><div class="vval">{{ live ? memAvailableGb : 'Unavailable' }} @if (live)<small>GB</small>@endif</div><div class="vnote">{{ live ? `${gb(m.memCacheMb)} GB cache` : unavailableNote }}</div></div>
|
|
258
357
|
<div class="vital"><div class="vlabel">Swap</div><div class="vval">{{ live ? swapPct : 'Unavailable' }}@if (live)<small>% used</small>@endif</div><div class="vnote">{{ live ? `${gb(m.swapUsedMb)} / ${gb(m.swapTotalMb)} GB` : unavailableNote }}</div></div>
|
|
259
358
|
<div class="vital"><div class="vlabel">Disk inodes</div><div class="vval">{{ live ? m.inodeUsedPct : 'Unavailable' }}@if (live)<small>% used</small>@endif</div><div class="vnote">Filesystem entry capacity</div></div>
|
|
260
|
-
<div class="vital"><div class="vlabel">Network
|
|
261
|
-
<div class="vital"><div class="vlabel">Network
|
|
359
|
+
<div class="vital"><div class="vlabel">Network in</div><div class="vval">{{ live ? rate(m.networkRxBytesPerSec) : 'Unavailable' }}</div><div class="vnote">{{ live ? `${bytes(m.networkRxBytesToday)} today` : unavailableNote }}</div></div>
|
|
360
|
+
<div class="vital"><div class="vlabel">Network out</div><div class="vval">{{ live ? rate(m.networkTxBytesPerSec) : 'Unavailable' }}</div><div class="vnote">{{ live ? `${bytes(m.networkTxBytesToday)} today` : unavailableNote }}</div></div>
|
|
361
|
+
<div class="vital"><div class="vlabel">Bandwidth this month</div><div class="vval">{{ live ? bytes(monthBytes) : 'Unavailable' }}</div><div class="vnote">{{ live ? bandwidthNote : unavailableNote }}</div></div>
|
|
262
362
|
<div class="vital"><div class="vlabel">Processes</div><div class="vval">{{ live ? m.processes : 'Unavailable' }}</div><div class="vnote">Current process count</div></div>
|
|
263
363
|
<div class="vital"><div class="vlabel">Disk free</div><div class="vval">{{ live ? diskFreeGb : 'Unavailable' }} @if (live)<small>GB</small>@endif</div><div class="vnote">{{ live ? `${m.diskTotalGb} GB total` : unavailableNote }}</div></div>
|
|
264
364
|
</div>
|
|
@@ -291,10 +391,44 @@ function setRange(value) { historyRange.set(value); refresh() }
|
|
|
291
391
|
</article>
|
|
292
392
|
</template>
|
|
293
393
|
</div>
|
|
394
|
+
<div class="egress-panel panel" :class="egressTone()" @show="hasEgress()">
|
|
395
|
+
<div class="egress-head">
|
|
396
|
+
<div>
|
|
397
|
+
<h3>Object storage egress</h3>
|
|
398
|
+
<p>Reported by the applications that serve the downloads — these bytes never cross this host's network interface.</p>
|
|
399
|
+
</div>
|
|
400
|
+
<strong>{{ egressSummary() }}</strong>
|
|
401
|
+
</div>
|
|
402
|
+
<div class="egress-track" @show="!!egressBudget()">
|
|
403
|
+
<i :style="'width:' + Math.min(100, egressPercent() ?? 0) + '%'"></i>
|
|
404
|
+
</div>
|
|
405
|
+
<p class="bad notice" role="status" @show="egressWillExceed()">
|
|
406
|
+
Projected to exceed the monthly allowance — {{ bytesLabel(egressProjected()) }} against {{ bytesLabel(egressBudget()) }}.
|
|
407
|
+
</p>
|
|
408
|
+
<svg viewBox="0 0 100 46" preserveAspectRatio="none" role="img" aria-label="Daily object storage egress">
|
|
409
|
+
<line x1="0" x2="100" y1="23" y2="23" class="history-gridline"></line>
|
|
410
|
+
<path :d="areaPath('egress-daily')" class="history-area"></path>
|
|
411
|
+
<path :d="linePath('egress-daily')" class="history-line"></path>
|
|
412
|
+
</svg>
|
|
413
|
+
<div class="history-meta">
|
|
414
|
+
<span>{{ samples('egress-daily') }} days</span>
|
|
415
|
+
<span>peak {{ display(peak('egress-daily'), 'bytes') }}</span>
|
|
416
|
+
</div>
|
|
417
|
+
</div>
|
|
294
418
|
<p class="note">Retention, source freshness, comparisons, logs, and exports are available in <a href="/operations/observability">Observability</a>.</p>
|
|
295
419
|
</div>
|
|
296
420
|
</div>
|
|
297
421
|
<style>
|
|
422
|
+
.egress-panel { margin-top:18px; padding:18px; }
|
|
423
|
+
.egress-head { display:flex; align-items:flex-start; justify-content:space-between; gap:16px; }
|
|
424
|
+
.egress-head h3 { margin:0; font-size:14px; }
|
|
425
|
+
.egress-head p { margin:4px 0 0; color:var(--txt3); font-size:11px; max-width:52ch; }
|
|
426
|
+
.egress-head strong { color:var(--txt); font-size:13px; white-space:nowrap; }
|
|
427
|
+
.egress-track { height:7px; margin:14px 0 4px; overflow:hidden; border-radius:999px; background:rgba(255,255,255,.07); }
|
|
428
|
+
.egress-track > i { display:block; height:100%; border-radius:999px; background:var(--ok); }
|
|
429
|
+
.egress-panel.warn .egress-track > i { background:var(--warn); }
|
|
430
|
+
.egress-panel.bad .egress-track > i { background:var(--bad); }
|
|
431
|
+
.egress-panel svg { width:100%; height:88px; margin-top:10px; }
|
|
298
432
|
.host-vitals { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
299
433
|
.live-gauges { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
300
434
|
.live-bars { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:18px 24px; }
|