2634 lines
76 KiB
JavaScript
2634 lines
76 KiB
JavaScript
/** Pure UI helpers for /bin/baretop (preamble; no import). */
|
||
|
||
/** @type {Map<string, unknown>} */
|
||
var bareTopUiMemo = new Map()
|
||
var BARE_TOP_UI_MEMO_MAX = 256
|
||
|
||
/**
|
||
* @param {string} key
|
||
* @param {() => unknown} build
|
||
*/
|
||
function bareTopMemoGet(key, build) {
|
||
if (bareTopUiMemo.has(key)) return bareTopUiMemo.get(key)
|
||
const v = build()
|
||
bareTopUiMemo.set(key, v)
|
||
if (bareTopUiMemo.size > BARE_TOP_UI_MEMO_MAX) {
|
||
const it = bareTopUiMemo.keys().next()
|
||
if (!it.done) bareTopUiMemo.delete(it.value)
|
||
}
|
||
return v
|
||
}
|
||
|
||
/**
|
||
* @param {string} line
|
||
* @returns {{ label: string, value: string }[]}
|
||
*/
|
||
function bareTopParseMeminfoPairs(line) {
|
||
const s = String(line || '').trim()
|
||
const out = []
|
||
if (!s) return out
|
||
const re = /(\w+):\s*(\d+)\s*(\w+)?/g
|
||
let m
|
||
while ((m = re.exec(s)) !== null) {
|
||
out.push({
|
||
label: m[1],
|
||
value: m[2] + (m[3] ? ' ' + m[3] : '')
|
||
})
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {string} line
|
||
* @returns {{ pairs: { label: string, value: string }[], memTotal?: number, memAvail?: number }}
|
||
*/
|
||
function bareTopParseMeminfoMetrics(line) {
|
||
const pairs = bareTopParseMeminfoPairs(line)
|
||
let memTotal
|
||
let memAvail
|
||
for (const p of pairs) {
|
||
if (p.label === 'MemTotal') memTotal = parseInt(p.value, 10)
|
||
if (p.label === 'MemAvailable') memAvail = parseInt(p.value, 10)
|
||
}
|
||
return { pairs, memTotal, memAvail }
|
||
}
|
||
|
||
/**
|
||
* @param {string} line
|
||
* @returns {{ one: number, five: number, fifteen: number, running?: number, total?: number } | null}
|
||
*/
|
||
function bareTopParseLoadavg(line) {
|
||
const s = String(line || '').trim()
|
||
if (!s) return null
|
||
const parts = s.split(/\s+/)
|
||
if (parts.length < 3) return null
|
||
const one = parseFloat(parts[0])
|
||
const five = parseFloat(parts[1])
|
||
const fifteen = parseFloat(parts[2])
|
||
if (!Number.isFinite(one)) return null
|
||
const slash = parts[3] && parts[3].includes('/') ? parts[3].split('/') : null
|
||
return {
|
||
one,
|
||
five: Number.isFinite(five) ? five : one,
|
||
fifteen: Number.isFinite(fifteen) ? fifteen : one,
|
||
running: slash ? parseInt(slash[0], 10) : undefined,
|
||
total: slash ? parseInt(slash[1], 10) : undefined
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {number} pct 0–100
|
||
* @param {number} width
|
||
* @param {boolean} ascii
|
||
* @param {{ low?: string, mid?: string, hi?: string, reset: string }} pal
|
||
* @param {boolean} useColor
|
||
*/
|
||
function bareTopFormatMeterBar(pct, width, ascii, pal, useColor) {
|
||
const w = Math.max(2, width | 0)
|
||
const p = Math.min(100, Math.max(0, Number(pct) || 0))
|
||
const fill = Math.round((p / 100) * w)
|
||
const full = ascii ? '#' : '\u2588'
|
||
const empty = ascii ? '-' : '\u2591'
|
||
let s = ''
|
||
for (let i = 0; i < w; i++) {
|
||
const on = i < fill
|
||
if (!useColor) {
|
||
s += on ? full : empty
|
||
continue
|
||
}
|
||
const c =
|
||
p >= 95 ? pal.hi || ''
|
||
: p >= 80 ? pal.mid || ''
|
||
: on ? pal.low || ''
|
||
: ''
|
||
s += c + (on ? full : empty) + (c ? pal.reset : '')
|
||
}
|
||
return s
|
||
}
|
||
|
||
/**
|
||
* @param {string} text
|
||
* @param {number} maxLen
|
||
* @param {boolean} wordBoundary
|
||
*/
|
||
function bareTopTruncateCell(text, maxLen, wordBoundary) {
|
||
const t = String(text ?? '')
|
||
const m = Math.max(1, maxLen | 0)
|
||
if (t.length <= m) return t
|
||
if (!wordBoundary || m < 5) return t.slice(0, m - 1) + '\u2026'
|
||
let cut = t.lastIndexOf(' ', m - 1)
|
||
if (cut < m >> 1) cut = m - 1
|
||
return t.slice(0, cut).trimEnd() + '\u2026'
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} pt process_table.json root
|
||
* @returns {Array<Record<string, unknown>>}
|
||
*/
|
||
function bareTopProcessRowsFromTable(pt) {
|
||
if (!pt || typeof pt !== 'object') return []
|
||
const o = /** @type {Record<string, unknown>} */ (pt)
|
||
const arr = o.processes
|
||
if (!Array.isArray(arr)) return []
|
||
return arr.filter((x) => x && typeof x === 'object')
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} pt
|
||
* @param {number} nowMs
|
||
*/
|
||
function bareTopSessionUptimeLine(pt, nowMs) {
|
||
const rows = bareTopProcessRowsFromTable(pt)
|
||
let boot = 0
|
||
for (const r of rows) {
|
||
if (bareTopProcessPid(r) === 1) {
|
||
boot = bareTopProcessStartedMs(r)
|
||
break
|
||
}
|
||
}
|
||
if (!boot) return ''
|
||
return ' uptime ' + bareTopFormatProcAge(boot, nowMs) + ' (since bare-os-kernel row)'
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} row
|
||
*/
|
||
function bareTopProcessPid(row) {
|
||
const n = Number(row.pid)
|
||
return Number.isFinite(n) ? n : 0
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} row
|
||
* @param {'pid'|'name'|'state'|'time'|'nice'|'pri'|'cpu'} key
|
||
*/
|
||
/**
|
||
* @param {Record<string, unknown>} row
|
||
* @param {number} wallMs
|
||
*/
|
||
function bareTopEffectiveCpuPct(row, wallMs) {
|
||
const c = row.cpuPct ?? row.cpu
|
||
if (typeof c === 'number' && Number.isFinite(c)) return c
|
||
const delta = row.cpuMsDelta
|
||
const w = Number(wallMs) || 0
|
||
if (typeof delta === 'number' && Number.isFinite(delta) && w > 0)
|
||
return Math.min(100, Math.max(0, (delta / w) * 100))
|
||
return 0
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} row
|
||
* @param {'pid'|'name'|'state'|'time'|'nice'|'pri'|'cpu'} key
|
||
* @param {number} [wallMs]
|
||
*/
|
||
function bareTopProcessSortKey(row, key, wallMs) {
|
||
if (key === 'name') return String(row.name || row.label || '').toLowerCase()
|
||
if (key === 'state') return String(row.state || '').toLowerCase()
|
||
if (key === 'time') return bareTopProcessStartedMs(row)
|
||
if (key === 'nice') return Number(row.nice ?? row.ni) || 0
|
||
if (key === 'pri') return Number(row.pri ?? row.priority) || 0
|
||
if (key === 'cpu') return bareTopEffectiveCpuPct(row, wallMs || 0)
|
||
return bareTopProcessPid(row)
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} row
|
||
* @param {string} pfRaw
|
||
* @param {Record<string, string>} env
|
||
* @param {{ taggedPids?: Set<number>, taggedOnly?: boolean }} [opts]
|
||
*/
|
||
function bareTopProcessRowMatchesFilter(row, pfRaw, env, opts) {
|
||
const o = opts && typeof opts === 'object' ? opts : {}
|
||
const tagged = o.taggedPids
|
||
const taggedOnly = o.taggedOnly === true
|
||
const pid = bareTopProcessPid(row)
|
||
if (taggedOnly && tagged && tagged.size && !tagged.has(pid)) return false
|
||
|
||
const invertBase =
|
||
env.BARE_TOP_FILTER_INVERT === '1' || env.BARE_TOP_FILTER_INVERT === 'true'
|
||
let pf = String(pfRaw || '').trim()
|
||
let invert = invertBase
|
||
if (pf.startsWith('!')) {
|
||
pf = pf.slice(1).trim()
|
||
invert = !invert
|
||
}
|
||
if (!pf) return true
|
||
|
||
const regexEnv =
|
||
env.BARE_TOP_FILTER_REGEX === '1' || env.BARE_TOP_FILTER_REGEX === 'true'
|
||
const name = String(row.name || row.label || '')
|
||
const user = String(row.user || row.identity || '')
|
||
/** @type {RegExp | null} */
|
||
let re = null
|
||
if (regexEnv && pf.length) {
|
||
try {
|
||
re = new RegExp(pf, 'i')
|
||
} catch {
|
||
re = null
|
||
}
|
||
} else if (pf.length >= 2 && pf.charAt(0) === '/') {
|
||
const end = pf.lastIndexOf('/')
|
||
if (end > 0) {
|
||
try {
|
||
const flags = pf.slice(end + 1) || 'i'
|
||
re = new RegExp(pf.slice(1, end), flags)
|
||
} catch {
|
||
re = null
|
||
}
|
||
}
|
||
}
|
||
let match = false
|
||
if (re) match = re.test(name) || re.test(user)
|
||
else match = name.toLowerCase().includes(pf.toLowerCase())
|
||
return invert ? !match : match
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>[]} rows
|
||
* @param {'pid'|'name'|'state'|'time'|'nice'|'pri'|'cpu'} sortKey
|
||
* @param {boolean} asc
|
||
* @param {{ tertiaryPpid?: boolean, sortWallMs?: number }} [opts]
|
||
*/
|
||
function bareTopSortProcessRows(rows, sortKey, asc, opts) {
|
||
const o = opts && typeof opts === 'object' ? opts : {}
|
||
const tertiaryPpid = o.tertiaryPpid === true
|
||
const wallMs = Number(o.sortWallMs) || 0
|
||
const dir = asc ? 1 : -1
|
||
const memoKey =
|
||
'sort:' +
|
||
sortKey +
|
||
':' +
|
||
(asc ? '1' : '0') +
|
||
':' +
|
||
(tertiaryPpid ? '1' : '0') +
|
||
':' +
|
||
wallMs +
|
||
':' +
|
||
rows
|
||
.map((r) => {
|
||
const rr = /** @type {Record<string, unknown>} */ (r)
|
||
return (
|
||
String(rr.pid ?? '') +
|
||
'/' +
|
||
String(rr.ppid ?? '') +
|
||
'/' +
|
||
String(rr.name ?? '') +
|
||
'/' +
|
||
String(rr.state ?? '') +
|
||
'/' +
|
||
String(rr.startedAtMs ?? '') +
|
||
'/' +
|
||
String(rr.cpuPct ?? rr.cpu ?? '')
|
||
)
|
||
})
|
||
.join('|')
|
||
return /** @type {Record<string, unknown>[]} */ (
|
||
bareTopMemoGet(memoKey, () => {
|
||
const out = rows.slice()
|
||
out.sort((a, b) => {
|
||
const ka = bareTopProcessSortKey(a, sortKey, wallMs)
|
||
const kb = bareTopProcessSortKey(b, sortKey, wallMs)
|
||
let c = 0
|
||
if (typeof ka === 'number' && typeof kb === 'number') c = ka - kb
|
||
else c = String(ka).localeCompare(String(kb))
|
||
if (c !== 0) return c * dir
|
||
if (tertiaryPpid) {
|
||
const pa = Number(a.ppid) || 0
|
||
const pb = Number(b.ppid) || 0
|
||
if (pa !== pb) return (pa - pb) * dir
|
||
}
|
||
return bareTopProcessPid(a) - bareTopProcessPid(b)
|
||
})
|
||
return out
|
||
})
|
||
)
|
||
}
|
||
|
||
/**
|
||
* @param {string[]} cells
|
||
* @param {number[]} widths
|
||
* @param {('l'|'r')[]} align
|
||
* @param {number} maxLineLen
|
||
*/
|
||
function bareTopFormatFixedColumns(cells, widths, align, maxLineLen) {
|
||
const parts = []
|
||
let total = 0
|
||
for (let i = 0; i < cells.length && i < widths.length; i++) {
|
||
const w = Math.max(1, widths[i] | 0)
|
||
let c = bareTopTruncateCell(cells[i], w, true)
|
||
if (c.length > w) c = c.slice(0, w)
|
||
const pad = w - c.length
|
||
if (align[i] === 'r') {
|
||
parts.push(' '.repeat(Math.max(0, pad)) + c)
|
||
} else {
|
||
parts.push(c + ' '.repeat(Math.max(0, pad)))
|
||
}
|
||
total += w + (i < cells.length - 1 ? 1 : 0)
|
||
}
|
||
let line = parts.join(' ')
|
||
if (line.length > maxLineLen) line = line.slice(0, maxLineLen)
|
||
return line
|
||
}
|
||
|
||
/**
|
||
* @param {number} score 0–100
|
||
* @param {number} width
|
||
* @param {boolean} ascii
|
||
*/
|
||
function bareTopHealthBarLine(score, width, ascii) {
|
||
const w = Math.max(4, width | 0)
|
||
const p = Math.min(100, Math.max(0, Math.round(Number(score) || 0)))
|
||
// Floor fill so the bar never reads a higher % than the integer score (round caused ~73% for 72/100).
|
||
const fill = Math.min(w, Math.floor((p * w) / 100))
|
||
const full = ascii ? '#' : '\u2588'
|
||
const empty = ascii ? '.' : '\u2591'
|
||
let bar = ''
|
||
for (let i = 0; i < w; i++) bar += i < fill ? full : empty
|
||
return String(p) + '/100 ' + bar
|
||
}
|
||
|
||
/**
|
||
* @param {string} title
|
||
* @param {number} cols
|
||
*/
|
||
function bareTopSectionRule(title, cols) {
|
||
const t = String(title || '').trim()
|
||
const c = Math.max(8, cols | 0)
|
||
const inner = c - 4
|
||
if (inner < 4) return t
|
||
const pad = inner - t.length
|
||
if (pad <= 0) return '\u2500 ' + t.slice(0, inner - 2) + ' \u2500'
|
||
const left = Math.floor(pad / 2)
|
||
const right = pad - left
|
||
return (
|
||
'\u2500'.repeat(Math.max(1, left)) + ' ' + t + ' ' + '\u2500'.repeat(Math.max(1, right))
|
||
)
|
||
}
|
||
|
||
/**
|
||
* Strip C0 controls except tab; keep printable + UTF-8 continuation for display safety.
|
||
* @param {string} s
|
||
*/
|
||
function bareTopSanitizeVisible(s) {
|
||
let t = String(s)
|
||
let o = ''
|
||
for (let i = 0; i < t.length; i++) {
|
||
const c = t.charCodeAt(i)
|
||
if (c === 9 || c === 10 || c === 13) o += t[i]
|
||
else if (c < 32) o += ' '
|
||
else o += t[i]
|
||
}
|
||
return o
|
||
}
|
||
|
||
/**
|
||
* @param {number} scrollTop
|
||
* @param {number} vis
|
||
* @param {string[]} wrapped
|
||
* @returns {string[]} visible slice (copy)
|
||
*/
|
||
function bareTopScrollSliceLines(wrapped, scrollTop, vis) {
|
||
const v = Math.max(1, vis | 0)
|
||
const s = Math.max(0, scrollTop | 0)
|
||
return wrapped.slice(s, s + v)
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} row
|
||
* @returns {number}
|
||
*/
|
||
function bareTopProcessStartedMs(row) {
|
||
if (!row || typeof row !== 'object') return 0
|
||
const n = Number(/** @type {Record<string, unknown>} */ (row).startedAtMs)
|
||
return Number.isFinite(n) ? n : 0
|
||
}
|
||
|
||
/**
|
||
* @param {number} startedAtMs
|
||
* @param {number} nowMs
|
||
*/
|
||
function bareTopPad2(n) {
|
||
const x = n | 0
|
||
return x < 10 ? '0' + x : String(x)
|
||
}
|
||
|
||
function bareTopFormatProcAge(startedAtMs, nowMs) {
|
||
const t0 = Number(startedAtMs)
|
||
const now = Number(nowMs)
|
||
if (!Number.isFinite(t0) || t0 <= 0 || !Number.isFinite(now)) return ''
|
||
let sec = Math.max(0, Math.floor((now - t0) / 1000))
|
||
if (sec < 3600) {
|
||
const m = Math.floor(sec / 60)
|
||
const s = sec % 60
|
||
return m > 0 ? m + ':' + bareTopPad2(s) : String(s) + 's'
|
||
}
|
||
const h = Math.floor(sec / 3600)
|
||
sec %= 3600
|
||
const m = Math.floor(sec / 60)
|
||
const s = sec % 60
|
||
return h + ':' + bareTopPad2(m) + ':' + bareTopPad2(s)
|
||
}
|
||
|
||
/**
|
||
* Tree order: depth-first by ppid, stable by pid.
|
||
* @param {Record<string, unknown>[]} rows
|
||
*/
|
||
function bareTopProcessTreeOrder(rows) {
|
||
const byPid = new Map()
|
||
for (const r of rows) {
|
||
const p = bareTopProcessPid(r)
|
||
if (p) byPid.set(p, r)
|
||
}
|
||
const children = new Map()
|
||
for (const r of rows) {
|
||
const ppid = Number(r.ppid) || 0
|
||
if (!children.has(ppid)) children.set(ppid, [])
|
||
children.get(ppid).push(r)
|
||
}
|
||
for (const arr of children.values()) {
|
||
arr.sort((a, b) => bareTopProcessPid(a) - bareTopProcessPid(b))
|
||
}
|
||
/** @type {Record<string, unknown>[]} */
|
||
const out = []
|
||
function walk(pid, depth) {
|
||
const ch = children.get(pid)
|
||
if (!ch) return
|
||
for (const r of ch) {
|
||
out.push(Object.assign({}, r, { _treeDepth: depth }))
|
||
walk(bareTopProcessPid(r), depth + 1)
|
||
}
|
||
}
|
||
walk(0, 0)
|
||
for (const r of rows) {
|
||
const p = bareTopProcessPid(r)
|
||
const pp = Number(r.ppid) || 0
|
||
if (!byPid.has(pp) && pp === 0 && p) {
|
||
/* orphan roots not reached */
|
||
}
|
||
}
|
||
if (out.length === 0) return rows.map((r) => Object.assign({}, r, { _treeDepth: 0 }))
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {string} title
|
||
* @param {number} cols
|
||
* @param {boolean} ascii
|
||
*/
|
||
function bareTopSectionSeparator(title, cols, ascii) {
|
||
const rule = bareTopSectionRule(title, cols)
|
||
if (ascii) return '- ' + String(title || '') + ' ' + '-'.repeat(Math.max(4, cols - title.length - 4))
|
||
return rule
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} o
|
||
* @param {number} maxRows
|
||
* @param {number} keyW
|
||
*/
|
||
function bareTopDelegateInflightTableLines(o, maxRows, keyW) {
|
||
if (!o || typeof o !== 'object') return []
|
||
const rec = /** @type {Record<string, unknown>} */ (o)
|
||
const pairs = Object.keys(rec)
|
||
.map((k) => ({ k, v: Number(rec[k]) || 0 }))
|
||
.sort((a, b) => b.v - a.v || a.k.localeCompare(b.k))
|
||
const lim = Math.min(maxRows, pairs.length)
|
||
/** @type {string[]} */
|
||
const out = []
|
||
const kw = Math.max(6, keyW | 0)
|
||
const vw = 8
|
||
for (let i = 0; i < lim; i++) {
|
||
const { k, v } = pairs[i]
|
||
const ks = bareTopTruncateCell(k, kw, true)
|
||
const padK = ' '.repeat(Math.max(0, kw - ks.length))
|
||
const vs = String(v).padStart(vw, ' ')
|
||
out.push(' ' + ks + padK + ' ' + vs)
|
||
}
|
||
if (pairs.length > lim) out.push(' … +' + (pairs.length - lim) + ' more keys')
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} o
|
||
* @param {number} cols
|
||
*/
|
||
/**
|
||
* @param {Record<string, unknown>} snap
|
||
*/
|
||
function bareTopLimitsMergedLines(snap) {
|
||
/** @type {string[]} */
|
||
const lines = []
|
||
const q =
|
||
snap &&
|
||
snap.extra &&
|
||
snap.extra.quotas &&
|
||
typeof snap.extra.quotas === 'object'
|
||
? /** @type {Record<string, unknown>} */ (snap.extra.quotas)
|
||
: null
|
||
const r =
|
||
snap &&
|
||
snap.extra &&
|
||
snap.extra.rlimits &&
|
||
typeof snap.extra.rlimits === 'object'
|
||
? /** @type {Record<string, unknown>} */ (snap.extra.rlimits)
|
||
: null
|
||
function row(prefix, o2, max) {
|
||
const keys = Object.keys(o2).slice(0, max)
|
||
for (const k of keys) {
|
||
const v = o2[k]
|
||
const s =
|
||
v != null && typeof v !== 'object'
|
||
? String(v)
|
||
: v && typeof v === 'object'
|
||
? '{…}'
|
||
: ''
|
||
lines.push(prefix + k + '=' + bareTopTruncateCell(s, 56, false))
|
||
}
|
||
}
|
||
if (q) {
|
||
lines.push(' quotas (operator caps — full detail on catalog tab)')
|
||
row(' ', q, 8)
|
||
}
|
||
if (r) {
|
||
lines.push(' rlimits (process limits mirror)')
|
||
row(' ', r, 8)
|
||
}
|
||
return lines
|
||
}
|
||
|
||
/**
|
||
* Human-readable key labels (matches baretop-tui bareTopHumanKey).
|
||
* @param {string} k
|
||
*/
|
||
function bareTopUiHumanKey(k) {
|
||
return String(k || '')
|
||
.replace(/([A-Z])/g, ' $1')
|
||
.replace(/_/g, ' ')
|
||
.replace(/\s+/g, ' ')
|
||
.trim()
|
||
.toLowerCase()
|
||
.replace(/^\w/, (c) => c.toUpperCase())
|
||
}
|
||
|
||
/**
|
||
* Flatten nested values to lines (no JSON); uses N/A for nulls.
|
||
* @param {string} prefix
|
||
* @param {unknown} v
|
||
* @param {number} depth
|
||
* @param {number} maxD
|
||
* @param {string[]} lines
|
||
* @param {number} maxKeys
|
||
*/
|
||
function bareTopUiFlatten(prefix, v, depth, maxD, lines, maxKeys) {
|
||
const na = 'N/A'
|
||
const pad = ' '.repeat(depth)
|
||
if (depth > maxD) {
|
||
lines.push(pad + (prefix ? bareTopUiHumanKey(prefix) + ': ' : '') + '\u2026')
|
||
return
|
||
}
|
||
if (v == null) {
|
||
lines.push(pad + (prefix ? bareTopUiHumanKey(prefix) + ': ' : '') + na)
|
||
return
|
||
}
|
||
const t = typeof v
|
||
if (t === 'string' || t === 'number' || t === 'boolean') {
|
||
let s = String(v)
|
||
if (t === 'string' && s.length > 800) s = s.slice(0, 797) + '\u2026'
|
||
lines.push(pad + (prefix ? bareTopUiHumanKey(prefix) + ': ' : '') + s)
|
||
return
|
||
}
|
||
if (Array.isArray(v)) {
|
||
const label = prefix ? bareTopUiHumanKey(prefix) + ': ' : ''
|
||
if (v.length === 0) {
|
||
lines.push(pad + label + '(empty)')
|
||
return
|
||
}
|
||
lines.push(pad + label + '(' + v.length + ' items)')
|
||
const lim = Math.min(v.length, 120)
|
||
for (let i = 0; i < lim; i++) {
|
||
const it = v[i]
|
||
if (it != null && typeof it === 'object' && !Array.isArray(it)) {
|
||
lines.push(pad + ' #' + i)
|
||
bareTopUiFlatten('', it, depth + 2, maxD, lines, maxKeys)
|
||
} else if (Array.isArray(it)) {
|
||
lines.push(pad + ' #' + i)
|
||
bareTopUiFlatten('', it, depth + 2, maxD, lines, maxKeys)
|
||
} else {
|
||
lines.push(
|
||
pad + ' #' + i + ': ' + (it == null ? na : String(it))
|
||
)
|
||
}
|
||
}
|
||
if (v.length > lim) lines.push(pad + ' \u2026 +' + (v.length - lim) + ' more')
|
||
return
|
||
}
|
||
if (t === 'object') {
|
||
const o = /** @type {Record<string, unknown>} */ (v)
|
||
const keys = Object.keys(o)
|
||
if (keys.length === 0) {
|
||
lines.push(pad + (prefix ? bareTopUiHumanKey(prefix) + ': ' : '') + '(empty)')
|
||
return
|
||
}
|
||
const slice = keys.slice(0, maxKeys)
|
||
if (prefix) {
|
||
lines.push(pad + bareTopUiHumanKey(prefix) + ':')
|
||
for (const k of slice) bareTopUiFlatten(k, o[k], depth + 1, maxD, lines, maxKeys)
|
||
} else {
|
||
for (const k of slice) bareTopUiFlatten(k, o[k], depth, maxD, lines, maxKeys)
|
||
}
|
||
if (keys.length > maxKeys)
|
||
lines.push(pad + '\u2026 +' + (keys.length - maxKeys) + ' keys')
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {string} prefix
|
||
* @param {unknown} v
|
||
* @param {number} depth
|
||
* @param {number} maxD
|
||
* @param {number} maxKeys
|
||
* @param {number} maxLines
|
||
*/
|
||
function bareTopUiFlattenLimited(prefix, v, depth, maxD, maxKeys, maxLines) {
|
||
let sig = ''
|
||
try {
|
||
sig = JSON.stringify(v)
|
||
} catch {
|
||
sig = String(v)
|
||
}
|
||
const key =
|
||
'flatten:' +
|
||
prefix +
|
||
':' +
|
||
depth +
|
||
':' +
|
||
maxD +
|
||
':' +
|
||
maxKeys +
|
||
':' +
|
||
maxLines +
|
||
':' +
|
||
sig
|
||
return /** @type {string[]} */ (
|
||
bareTopMemoGet(key, () => {
|
||
const lines = []
|
||
bareTopUiFlatten(prefix, v, depth, maxD, lines, maxKeys)
|
||
const cap = Math.max(4, maxLines | 0)
|
||
if (lines.length > cap) {
|
||
return lines.slice(0, cap - 1).concat([' … (truncated)'])
|
||
}
|
||
return lines
|
||
})
|
||
)
|
||
}
|
||
|
||
/**
|
||
* One-line-ish summary for nested objects (no JSON braces in the TUI).
|
||
* @param {unknown} v
|
||
* @param {number} maxChars
|
||
*/
|
||
function bareTopFormatNestedBrief(v, maxChars) {
|
||
const m = Math.max(8, maxChars | 0)
|
||
if (v == null) return ''
|
||
const t = typeof v
|
||
if (t === 'string' || t === 'number' || t === 'boolean')
|
||
return bareTopTruncateCell(String(v), m, false)
|
||
if (t === 'bigint') return String(v)
|
||
if (t !== 'object') return bareTopTruncateCell(String(v), m, false)
|
||
const lines = bareTopUiFlattenLimited('', v, 0, 6, 80, 24)
|
||
const s = lines.join(' · ')
|
||
if (s.length <= m) return s
|
||
return s.slice(0, Math.max(4, m - 18)) + '\u2026(truncated)'
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} v
|
||
* @param {number} maxChars
|
||
*/
|
||
function bareTopJsonSnippet(v, maxChars) {
|
||
const m = Math.max(8, maxChars | 0)
|
||
try {
|
||
if (v != null && typeof v === 'object')
|
||
return bareTopFormatNestedBrief(v, m)
|
||
if (typeof v === 'bigint') return String(v)
|
||
const s = JSON.stringify(v)
|
||
if (s.length <= m) return s
|
||
return s.slice(0, Math.max(4, m - 18)) + '\u2026(truncated)'
|
||
} catch {
|
||
return '(unserializable)'
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} v
|
||
* @param {number} maxLen
|
||
*/
|
||
function bareTopFormatScalarTerminal(v, maxLen) {
|
||
const m = Math.max(4, maxLen | 0)
|
||
if (v == null) return ''
|
||
const t = typeof v
|
||
if (t === 'string' || t === 'number' || t === 'boolean' || t === 'bigint')
|
||
return bareTopTruncateCell(String(v), m, false)
|
||
if (t === 'object') return bareTopJsonSnippet(v, m)
|
||
return bareTopTruncateCell(String(v), m, false)
|
||
}
|
||
|
||
/**
|
||
* Format integers with grouped thousands (en-US style).
|
||
* @param {unknown} n
|
||
*/
|
||
function bareTopFormatIntGrouped(n) {
|
||
const x = Number(n)
|
||
if (!Number.isFinite(x)) return String(n)
|
||
const neg = x < 0
|
||
const i = Math.floor(Math.abs(x))
|
||
const s = String(i)
|
||
const g = []
|
||
for (let k = s.length; k > 0; k -= 3) g.push(s.slice(Math.max(0, k - 3), k))
|
||
return (neg ? '-' : '') + g.reverse().join(',')
|
||
}
|
||
|
||
/**
|
||
* Human duration for millisecond-ish counters (shared UI).
|
||
* @param {unknown} ms
|
||
*/
|
||
function bareTopFormatDurationMs(ms) {
|
||
const t = Number(ms)
|
||
if (!Number.isFinite(t) || t < 0) return ''
|
||
if (t < 1000) return Math.round(t) + 'ms'
|
||
if (t < 60000) return (t / 1000).toFixed(1) + 's'
|
||
if (t < 3600000) return Math.floor(t / 60000) + 'm'
|
||
return Math.floor(t / 3600000) + 'h'
|
||
}
|
||
|
||
/**
|
||
* Rx/tx bytes: use global bareTopFormatBytes from baretop-snapshot preamble when bundled.
|
||
* @param {unknown} n
|
||
*/
|
||
function bareTopNetFormatBytes(n) {
|
||
if (typeof bareTopFormatBytes === 'function') return bareTopFormatBytes(n)
|
||
const x = Number(n)
|
||
if (!Number.isFinite(x) || x < 0) return '\u2014'
|
||
if (x < 1024) return String(Math.floor(x)) + ' B'
|
||
if (x < 1048576) return (x / 1024).toFixed(1) + ' KiB'
|
||
if (x < 1073741824) return (x / 1048576).toFixed(1) + ' MiB'
|
||
return (x / 1073741824).toFixed(2) + ' GiB'
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} o
|
||
*/
|
||
function bareTopNetOrderedTopKeys(o) {
|
||
const priority = [
|
||
'schema',
|
||
'schemaVersion',
|
||
'topicHex',
|
||
'peerCount',
|
||
'replicationQueueDepth',
|
||
'peerFirewallAcceptedTotal',
|
||
'peerFirewallRejectedTotal',
|
||
'peerFirewallInboundTotal',
|
||
'peerFirewallOutboundTotal',
|
||
'seedHandshakeError',
|
||
'seedRole',
|
||
'atMs'
|
||
]
|
||
const seen = new Set()
|
||
/** @type {string[]} */
|
||
const out = []
|
||
for (const k of priority) {
|
||
if (Object.prototype.hasOwnProperty.call(o, k) && o[k] != null) {
|
||
out.push(k)
|
||
seen.add(k)
|
||
}
|
||
}
|
||
const rest = Object.keys(o)
|
||
.filter((k) => !seen.has(k))
|
||
.sort((a, b) => a.localeCompare(b))
|
||
return out.concat(rest)
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} q
|
||
* @param {number} cols
|
||
*/
|
||
function bareTopNetReplicationQueueLines(q, cols) {
|
||
if (!q || typeof q !== 'object') return [' (null)']
|
||
const o = /** @type {Record<string, unknown>} */ (q)
|
||
/** @type {string[]} */
|
||
const lines = []
|
||
const scalars = [
|
||
'depth',
|
||
'pending',
|
||
'pendingCount',
|
||
'length',
|
||
'size',
|
||
'backlog',
|
||
'queued',
|
||
'inFlight',
|
||
'lastError',
|
||
'error',
|
||
'note',
|
||
'atMs'
|
||
]
|
||
for (const k of scalars) {
|
||
if (!Object.prototype.hasOwnProperty.call(o, k)) continue
|
||
const v = o[k]
|
||
if (v != null && typeof v !== 'object')
|
||
lines.push(' ' + k + ': ' + String(v))
|
||
}
|
||
for (const ak of ['items', 'queue', 'entries', 'pendingJobs']) {
|
||
if (Array.isArray(o[ak]))
|
||
lines.push(' ' + ak + ': ' + o[ak].length + ' entries')
|
||
}
|
||
const nestedKeys = Object.keys(o).filter((k) => {
|
||
const v = o[k]
|
||
return v != null && typeof v === 'object'
|
||
})
|
||
for (const nk of nestedKeys.slice(0, 6)) {
|
||
lines.push(' ' + nk + ': ' + bareTopJsonSnippet(o[nk], Math.max(24, cols - 14)))
|
||
}
|
||
if (!lines.length)
|
||
lines.push(' ' + bareTopJsonSnippet(o, Math.max(40, cols - 6)))
|
||
return lines
|
||
}
|
||
|
||
/**
|
||
* peer_firewall_stats / channel.js firewall shape: acceptedSessionCount, rejectedSessionCount, etc.
|
||
* @param {unknown} fw
|
||
* @param {number} cols
|
||
*/
|
||
function bareTopNetPeerFirewallLines(fw, cols) {
|
||
if (!fw || typeof fw !== 'object') return [' (null)']
|
||
const o = /** @type {Record<string, unknown>} */ (fw)
|
||
/** @type {string[]} */
|
||
const lines = []
|
||
const counts = [
|
||
'acceptedSessionCount',
|
||
'rejectedSessionCount',
|
||
'inboundSessionCount',
|
||
'outboundSessionCount'
|
||
]
|
||
for (const k of counts) {
|
||
if (typeof o[k] === 'number') lines.push(' ' + k + ': ' + bareTopFormatIntGrouped(o[k]))
|
||
}
|
||
if (typeof o.error === 'string' && o.error.trim())
|
||
lines.push(' error: ' + bareTopTruncateCell(o.error, cols - 8, true))
|
||
if (typeof o.note === 'string' && o.note.trim())
|
||
lines.push(' note: ' + bareTopTruncateCell(o.note, cols - 8, true))
|
||
for (const extra of ['transportBreakdown', 'saturationClass', 'wave9', 'wave10']) {
|
||
if (o[extra] != null && typeof o[extra] === 'object')
|
||
lines.push(
|
||
' ' +
|
||
extra +
|
||
': ' +
|
||
bareTopJsonSnippet(o[extra], Math.max(32, cols - 14))
|
||
)
|
||
}
|
||
if (!lines.length)
|
||
lines.push(' ' + bareTopJsonSnippet(o, Math.max(40, cols - 6)))
|
||
return lines
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} b
|
||
* @param {number} cols
|
||
*/
|
||
function bareTopNetBsdBridgeLines(b, cols) {
|
||
if (!b || typeof b !== 'object') return [' (null)']
|
||
const o = /** @type {Record<string, unknown>} */ (b)
|
||
/** @type {string[]} */
|
||
const lines = []
|
||
for (const k of [
|
||
'schema',
|
||
'policyEnv',
|
||
'activeBridgedFds',
|
||
'bridgeErrors',
|
||
'guestSocketCount'
|
||
]) {
|
||
if (!Object.prototype.hasOwnProperty.call(o, k)) continue
|
||
const v = o[k]
|
||
if (v != null && typeof v !== 'object')
|
||
lines.push(' ' + k + ': ' + String(v))
|
||
}
|
||
if (typeof o.note === 'string' && o.note.trim())
|
||
lines.push(
|
||
' note: ' + bareTopTruncateCell(String(o.note), Math.max(20, cols - 10), true)
|
||
)
|
||
const rest = Object.keys(o).filter(
|
||
(k) => !['schema', 'policyEnv', 'note', 'activeBridgedFds', 'bridgeErrors', 'guestSocketCount'].includes(k)
|
||
)
|
||
for (const k of rest.slice(0, 8)) {
|
||
const v = o[k]
|
||
lines.push(
|
||
' ' +
|
||
k +
|
||
': ' +
|
||
bareTopFormatScalarTerminal(v, Math.max(16, cols - 12))
|
||
)
|
||
}
|
||
return lines.length ? lines : [' (empty)']
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} net
|
||
* @param {unknown} metricsLive
|
||
*/
|
||
function bareTopNetHealthOverviewLines(net, metricsLive) {
|
||
/** @type {string[]} */
|
||
const parts = []
|
||
const peersMl =
|
||
metricsLive && typeof metricsLive === 'object'
|
||
? Number(/** @type {Record<string, unknown>} */ (metricsLive).peers)
|
||
: NaN
|
||
if (Number.isFinite(peersMl)) parts.push('peers_ml=' + bareTopFormatIntGrouped(peersMl))
|
||
if (net && typeof net === 'object') {
|
||
const o = /** @type {Record<string, unknown>} */ (net)
|
||
if (o.topicHex != null) parts.push('topic=' + String(o.topicHex).slice(0, 16))
|
||
if (typeof o.peerCount === 'number')
|
||
parts.push('peers_net=' + bareTopFormatIntGrouped(o.peerCount))
|
||
const err = o.seedHandshakeError
|
||
if (err != null && String(err).trim())
|
||
parts.push('seedErr=' + bareTopTruncateCell(String(err), 36, true))
|
||
const rd = o.replicationQueueDepth
|
||
if (typeof rd === 'number') parts.push('replQ=' + bareTopFormatIntGrouped(rd))
|
||
}
|
||
if (!parts.length) return []
|
||
return [' net ' + parts.join(' ') + ' (tab: network)']
|
||
}
|
||
|
||
/**
|
||
* Consolidated HDMS + DHT line for overview (traffic-light style).
|
||
* @param {Record<string, unknown>} snap
|
||
*/
|
||
function bareTopHdmsDhtTrafficLine(snap) {
|
||
const hd =
|
||
snap &&
|
||
snap.extra &&
|
||
snap.extra.hdmsHealth &&
|
||
typeof snap.extra.hdmsHealth === 'object'
|
||
? /** @type {Record<string, unknown>} */ (snap.extra.hdmsHealth)
|
||
: null
|
||
const dh =
|
||
snap &&
|
||
snap.extra &&
|
||
snap.extra.dhtStatus &&
|
||
typeof snap.extra.dhtStatus === 'object'
|
||
? /** @type {Record<string, unknown>} */ (snap.extra.dhtStatus)
|
||
: null
|
||
if (!hd && !dh) return ''
|
||
function grade(o) {
|
||
if (!o) return '?'
|
||
const e = o.error ?? o.err
|
||
if (e != null && String(e).trim()) return 'R'
|
||
const ok = o.ok ?? o.healthy ?? o.ready
|
||
if (ok === false) return 'R'
|
||
if (ok === true) return 'G'
|
||
return 'Y'
|
||
}
|
||
const bits = []
|
||
if (hd) {
|
||
bits.push('hdms[' + grade(hd) + ']=' + bareTopP2PKvString(hd, 5))
|
||
}
|
||
if (dh) {
|
||
bits.push('dht[' + grade(dh) + ']=' + bareTopP2PKvString(dh, 5))
|
||
}
|
||
return ' p2p ' + bits.join(' ')
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} o
|
||
* @param {number} maxKeys
|
||
*/
|
||
function bareTopP2PKvString(o, maxKeys) {
|
||
return Object.keys(o)
|
||
.slice(0, maxKeys)
|
||
.map((k) => {
|
||
const v = o[k]
|
||
return k + '=' + (v != null && typeof v !== 'object' ? String(v) : bareTopJsonSnippet(v, 32))
|
||
})
|
||
.join(' ')
|
||
}
|
||
|
||
function bareTopP2PStackLines(snap) {
|
||
/** @type {string[]} */
|
||
const out = []
|
||
const traffic = bareTopHdmsDhtTrafficLine(/** @type {Record<string, unknown>} */ (snap))
|
||
if (traffic) {
|
||
out.push(traffic)
|
||
return out
|
||
}
|
||
const hd =
|
||
snap &&
|
||
snap.extra &&
|
||
snap.extra.hdmsHealth &&
|
||
typeof snap.extra.hdmsHealth === 'object'
|
||
? /** @type {Record<string, unknown>} */ (snap.extra.hdmsHealth)
|
||
: null
|
||
const dh =
|
||
snap &&
|
||
snap.extra &&
|
||
snap.extra.dhtStatus &&
|
||
typeof snap.extra.dhtStatus === 'object'
|
||
? /** @type {Record<string, unknown>} */ (snap.extra.dhtStatus)
|
||
: null
|
||
if (hd)
|
||
out.push(' hdms ' + bareTopTruncateCell(bareTopP2PKvString(hd, 8), 100, false))
|
||
if (dh)
|
||
out.push(' dht ' + bareTopTruncateCell(bareTopP2PKvString(dh, 8), 100, false))
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @typedef {{ maxLines?: number, filter?: string, wideTwoCol?: boolean }} BareTopNetTabOpts
|
||
* @param {unknown} net
|
||
* @param {number} cols
|
||
* @param {BareTopNetTabOpts | null} [opts]
|
||
*/
|
||
function bareTopNetTabLines(net, cols, opts) {
|
||
const oopts = opts && typeof opts === 'object' ? opts : {}
|
||
const adaptiveMax = cols < 90 ? 256 : cols < 120 ? 384 : 512
|
||
const maxTotal = Math.max(8, (oopts.maxLines | 0) || adaptiveMax)
|
||
const filt = String(oopts.filter || '')
|
||
.trim()
|
||
.toLowerCase()
|
||
const wide = oopts.wideTwoCol === true && cols >= 100
|
||
if (!net || typeof net !== 'object') return [' N/A']
|
||
const o = /** @type {Record<string, unknown>} */ (net)
|
||
/** @type {string[]} */
|
||
const lines = []
|
||
|
||
function pushFiltered(s) {
|
||
if (filt && !String(s).toLowerCase().includes(filt)) return
|
||
lines.push(s)
|
||
}
|
||
|
||
function pushSection(title) {
|
||
pushFiltered('')
|
||
pushFiltered(' --- ' + title + ' ---')
|
||
}
|
||
|
||
if (Array.isArray(o.interfaces)) {
|
||
pushSection('interfaces (rx+tx)')
|
||
const arr = o.interfaces.slice().sort((a, b) => {
|
||
const ta =
|
||
a && typeof a === 'object'
|
||
? /** @type {Record<string, unknown>} */ (a)
|
||
: null
|
||
const tb =
|
||
b && typeof b === 'object'
|
||
? /** @type {Record<string, unknown>} */ (b)
|
||
: null
|
||
const sa =
|
||
(Number(ta && ta.rxBytes) || 0) + (Number(ta && ta.txBytes) || 0)
|
||
const sb =
|
||
(Number(tb && tb.rxBytes) || 0) + (Number(tb && tb.txBytes) || 0)
|
||
return sb - sa
|
||
})
|
||
const w = Math.max(40, cols - 4)
|
||
for (const iface of arr.slice(0, 32)) {
|
||
if (!iface || typeof iface !== 'object') continue
|
||
const i = /** @type {Record<string, unknown>} */ (iface)
|
||
const nm = String(i.name ?? i.if ?? '?')
|
||
const rxB = i.rxBytes ?? i.rx
|
||
const txB = i.txBytes ?? i.tx
|
||
const rxS =
|
||
rxB != null && rxB !== '' && Number.isFinite(Number(rxB))
|
||
? bareTopNetFormatBytes(rxB)
|
||
: String(rxB ?? '\u2014')
|
||
const txS =
|
||
txB != null && txB !== '' && Number.isFinite(Number(txB))
|
||
? bareTopNetFormatBytes(txB)
|
||
: String(txB ?? '\u2014')
|
||
const extra = Object.keys(i)
|
||
.filter((k) => !['name', 'if', 'rxBytes', 'txBytes', 'rx', 'tx'].includes(k))
|
||
.slice(0, 4)
|
||
.map((k) => {
|
||
const v = i[k]
|
||
return (
|
||
k +
|
||
'=' +
|
||
(v != null && typeof v !== 'object'
|
||
? String(v)
|
||
: bareTopJsonSnippet(v, 36))
|
||
)
|
||
})
|
||
.join(' ')
|
||
pushFiltered(
|
||
' ' +
|
||
bareTopTruncateCell(nm, 16, true) +
|
||
' rx=' +
|
||
rxS +
|
||
' tx=' +
|
||
txS +
|
||
' ' +
|
||
bareTopTruncateCell(extra, w - 24, false)
|
||
)
|
||
}
|
||
if (arr.length > 32) {
|
||
let remRx = 0
|
||
let remTx = 0
|
||
for (const iface of arr.slice(32)) {
|
||
if (!iface || typeof iface !== 'object') continue
|
||
const i = /** @type {Record<string, unknown>} */ (iface)
|
||
remRx += Number(i.rxBytes ?? i.rx) || 0
|
||
remTx += Number(i.txBytes ?? i.tx) || 0
|
||
}
|
||
pushFiltered(
|
||
' (+ ' +
|
||
(arr.length - 32) +
|
||
' more) rx=' +
|
||
bareTopNetFormatBytes(remRx) +
|
||
' tx=' +
|
||
bareTopNetFormatBytes(remTx)
|
||
)
|
||
}
|
||
}
|
||
|
||
const sectionKeys = new Set([
|
||
'interfaces',
|
||
'replicationQueue',
|
||
'peerFirewallStats',
|
||
'bsdSocketGuestBridge',
|
||
'transport',
|
||
'udxTuning',
|
||
'hyperswarmTuning',
|
||
'peerFirewallE2e',
|
||
'stagingSlot',
|
||
'snapshotHints'
|
||
])
|
||
|
||
pushSection('summary')
|
||
const ordered = bareTopNetOrderedTopKeys(o)
|
||
for (const k of ordered) {
|
||
if (sectionKeys.has(k)) continue
|
||
const v = o[k]
|
||
if (v != null && typeof v === 'object') continue
|
||
pushFiltered(
|
||
' ' +
|
||
k +
|
||
': ' +
|
||
bareTopTruncateCell(bareTopFormatScalarTerminal(v, Math.max(24, cols - 8)), Math.max(20, cols - 6), false)
|
||
)
|
||
}
|
||
|
||
if (o.replicationQueue != null) {
|
||
pushSection('replicationQueue')
|
||
for (const ln of bareTopNetReplicationQueueLines(o.replicationQueue, cols)) pushFiltered(ln)
|
||
}
|
||
if (o.peerFirewallStats != null) {
|
||
pushSection('peerFirewallStats')
|
||
for (const ln of bareTopNetPeerFirewallLines(o.peerFirewallStats, cols)) pushFiltered(ln)
|
||
}
|
||
if (o.bsdSocketGuestBridge != null) {
|
||
pushSection('bsdSocketGuestBridge')
|
||
for (const ln of bareTopNetBsdBridgeLines(o.bsdSocketGuestBridge, cols)) pushFiltered(ln)
|
||
}
|
||
|
||
const tun = ['transport', 'udxTuning', 'hyperswarmTuning', 'peerFirewallE2e']
|
||
for (const tk of tun) {
|
||
if (o[tk] == null) continue
|
||
pushSection(tk)
|
||
const v = o[tk]
|
||
if (v != null && typeof v === 'object') {
|
||
const jo = /** @type {Record<string, unknown>} */ (v)
|
||
const keys = Object.keys(jo).slice(0, 24)
|
||
for (const kk of keys) {
|
||
const vv = jo[kk]
|
||
pushFiltered(
|
||
' ' +
|
||
kk +
|
||
': ' +
|
||
bareTopFormatScalarTerminal(vv, Math.max(16, cols - 10))
|
||
)
|
||
}
|
||
} else pushFiltered(' ' + String(v))
|
||
}
|
||
|
||
for (const ek of ['stagingSlot', 'snapshotHints']) {
|
||
if (o[ek] == null) continue
|
||
pushSection(ek)
|
||
const v = o[ek]
|
||
if (v != null && typeof v === 'object') {
|
||
const jo = /** @type {Record<string, unknown>} */ (v)
|
||
for (const kk of Object.keys(jo).slice(0, 20)) {
|
||
const vv = jo[kk]
|
||
pushFiltered(
|
||
' ' +
|
||
kk +
|
||
': ' +
|
||
bareTopFormatScalarTerminal(vv, Math.max(12, cols - 12))
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
if (wide && lines.length > 6) {
|
||
/** @type {string[]} */
|
||
const merged = []
|
||
for (let i = 0; i < lines.length; i += 2) {
|
||
const a = lines[i] || ''
|
||
const b = lines[i + 1] || ''
|
||
const half = Math.floor(cols / 2) - 1
|
||
merged.push(
|
||
bareTopTruncateCell(a, half, false) +
|
||
' | ' +
|
||
bareTopTruncateCell(b, half, false)
|
||
)
|
||
}
|
||
lines.length = 0
|
||
lines.push(...merged)
|
||
}
|
||
|
||
if (lines.length > maxTotal) {
|
||
const head = lines.slice(0, maxTotal - 1)
|
||
head.push(' \u2026 (truncated, ' + (lines.length - maxTotal + 1) + ' more lines)')
|
||
return head
|
||
}
|
||
return lines.length ? lines : [' (empty net summary)']
|
||
}
|
||
|
||
function bareTopNetworkDeepDiveLines(extra, cols) {
|
||
/** @type {string[]} */
|
||
const out = []
|
||
const rs =
|
||
extra && extra.routeSummary && typeof extra.routeSummary === 'object'
|
||
? /** @type {Record<string, unknown>} */ (extra.routeSummary)
|
||
: null
|
||
if (rs) {
|
||
const direct = Number(rs.directCount ?? rs.direct ?? 0) || 0
|
||
const relay = Number(rs.relayCount ?? rs.relay ?? 0) || 0
|
||
const total = direct + relay
|
||
const ratio = total > 0 ? Math.round((direct / total) * 100) : 0
|
||
out.push(
|
||
' route direct=' +
|
||
direct +
|
||
' relay=' +
|
||
relay +
|
||
' directRatio=' +
|
||
ratio +
|
||
'%'
|
||
)
|
||
}
|
||
const hs =
|
||
extra && extra.holepunchSummary && typeof extra.holepunchSummary === 'object'
|
||
? /** @type {Record<string, unknown>} */ (extra.holepunchSummary)
|
||
: null
|
||
if (hs) {
|
||
const keys = Object.keys(hs).slice(0, 4)
|
||
const bits = keys.map((k) => k + '=' + bareTopFormatScalarTerminal(hs[k], 18))
|
||
if (bits.length) out.push(' holepunch ' + bits.join(' '))
|
||
}
|
||
const sd =
|
||
extra && extra.swarmDoctor && typeof extra.swarmDoctor === 'object'
|
||
? /** @type {Record<string, unknown>} */ (extra.swarmDoctor)
|
||
: null
|
||
if (sd) {
|
||
const verdict = String(sd.verdict || sd.status || 'unknown')
|
||
const rem = Array.isArray(sd.remediation) ? sd.remediation[0] : sd.hint
|
||
out.push(
|
||
' doctor verdict=' +
|
||
verdict +
|
||
(rem ? ' hint=' + bareTopTruncateCell(String(rem), Math.max(12, cols - 36), true) : '')
|
||
)
|
||
}
|
||
return out
|
||
}
|
||
|
||
function bareTopPeerDetailsLines(extra, cols) {
|
||
const pd =
|
||
extra && extra.peerDetails && typeof extra.peerDetails === 'object'
|
||
? /** @type {Record<string, unknown>} */ (extra.peerDetails)
|
||
: null
|
||
if (!pd || !Array.isArray(pd.peers)) return []
|
||
/** @type {string[]} */
|
||
const out = []
|
||
for (const p of pd.peers.slice(0, 12)) {
|
||
if (!p || typeof p !== 'object') continue
|
||
const r = /** @type {Record<string, unknown>} */ (p)
|
||
const key = String(r.remotePublicKey || r.peerId || '?')
|
||
const state = String(r.state || 'connected')
|
||
const ep =
|
||
r.endpoint && typeof r.endpoint === 'object'
|
||
? /** @type {Record<string, unknown>} */ (r.endpoint)
|
||
: null
|
||
const addr = String((ep && ep.address) || r.remoteAddress || '?')
|
||
const port = String((ep && ep.port) || r.remotePort || '?')
|
||
const os =
|
||
r.peerOs && typeof r.peerOs === 'object'
|
||
? String((/** @type {Record<string, unknown>} */ (r.peerOs)).osHint || 'unknown')
|
||
: 'unknown'
|
||
out.push(' peer ' + bareTopTruncateCell(key, Math.max(12, cols - 36), false))
|
||
out.push(' state=' + state + ' endpoint=' + addr + ':' + port + ' os=' + os)
|
||
}
|
||
return out
|
||
}
|
||
|
||
function bareTopDhtScanPostureLines(extra) {
|
||
const ds =
|
||
extra && extra.dhtScan && typeof extra.dhtScan === 'object'
|
||
? /** @type {Record<string, unknown>} */ (extra.dhtScan)
|
||
: null
|
||
if (!ds) return []
|
||
const fire = ds.firewalled
|
||
const boot = ds.bootstrapReachable ?? ds.bootstrap
|
||
const rnd = ds.randomizedEndpoints ?? ds.randomized
|
||
return [
|
||
' dht scan firewalled=' +
|
||
String(fire == null ? 'n/a' : fire) +
|
||
' bootstrap=' +
|
||
String(boot == null ? 'n/a' : boot) +
|
||
' randomized=' +
|
||
String(rnd == null ? 'n/a' : rnd)
|
||
]
|
||
}
|
||
|
||
function bareTopMeshdropLines(extra) {
|
||
const md =
|
||
extra && extra.meshdrop && typeof extra.meshdrop === 'object'
|
||
? /** @type {Record<string, unknown>} */ (extra.meshdrop)
|
||
: null
|
||
if (!md) return []
|
||
const rx = Number(md.rxTotal ?? md.rx ?? md.rxEnvelopes) || 0
|
||
const tx = Number(md.txTotal ?? md.tx ?? md.txEnvelopes) || 0
|
||
return [' meshdrop rx=' + rx + ' tx=' + tx]
|
||
}
|
||
|
||
function bareTopPromHeadlineLines(promText) {
|
||
const t = String(promText || '')
|
||
if (!t.trim()) return []
|
||
const keys = [
|
||
'bare_os_kernel_counters',
|
||
'bare_os_protomux',
|
||
'bare_os_replication',
|
||
'bare_os_swarm'
|
||
]
|
||
/** @type {string[]} */
|
||
const out = []
|
||
for (const line of t.split('\n')) {
|
||
const ln = line.trim()
|
||
if (!ln || ln.charAt(0) === '#') continue
|
||
for (const k of keys) {
|
||
if (ln.startsWith(k)) {
|
||
out.push(' ' + ln)
|
||
break
|
||
}
|
||
}
|
||
if (out.length >= 12) break
|
||
}
|
||
return out
|
||
}
|
||
|
||
function bareTopDelegateRateBucketLines(o, cols) {
|
||
if (!o || typeof o !== 'object') return []
|
||
const rec = /** @type {Record<string, unknown>} */ (o)
|
||
const keys = Object.keys(rec).sort()
|
||
if (!keys.length) return []
|
||
/** @type {string[]} */
|
||
const out = []
|
||
const w = Math.max(40, Math.min(cols - 2, cols))
|
||
for (const k of keys) {
|
||
const v = rec[k]
|
||
if (Array.isArray(v)) {
|
||
const nums = v.map((x) => String(x)).join(' ')
|
||
out.push(
|
||
' ' +
|
||
bareTopTruncateCell(k, Math.min(20, Math.floor(w * 0.35)), true).padEnd(20) +
|
||
' ' +
|
||
bareTopTruncateCell(nums, w - 24, false)
|
||
)
|
||
} else if (v != null && typeof v === 'object') {
|
||
out.push(
|
||
' ' +
|
||
k +
|
||
': ' +
|
||
bareTopJsonSnippet(v, Math.min(120, w - k.length - 4))
|
||
)
|
||
} else {
|
||
out.push(' ' + k + ': ' + String(v))
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* Kernel counter keys emitted by the booter (inventory for dashboards):
|
||
* initd.unit_failed_final, vfs.readfile.samples, vfs.replication_warm_full_invalidate,
|
||
* vfs.warm_read_cache_*, security.key_handle_*, audit.append*, operator.corestore_snapshot_hint,
|
||
* vfs.hyperblobs_dedup_* — see bareOsKernelMetricInc call sites in packages/bare-os-booter.
|
||
* @param {unknown} counters
|
||
* @param {unknown} prev
|
||
* @param {boolean} deltaMode
|
||
* @param {number} topN
|
||
* @param {number} colW
|
||
*/
|
||
function bareTopKernelCounterLines(counters, prev, deltaMode, topN, colW) {
|
||
if (!counters || typeof counters !== 'object') return []
|
||
const o = /** @type {Record<string, number>} */ (counters)
|
||
const po =
|
||
prev && typeof prev === 'object'
|
||
? /** @type {Record<string, number>} */ (prev)
|
||
: null
|
||
const pairs = Object.keys(o)
|
||
.map((k) => ({ k, v: Number(o[k]) || 0 }))
|
||
.sort((a, b) => b.v - a.v || a.k.localeCompare(b.k))
|
||
const lim = Math.min(Math.max(1, topN | 0), pairs.length)
|
||
/** @type {string[]} */
|
||
const out = []
|
||
const wn = Math.max(6, colW | 0)
|
||
for (let i = 0; i < lim; i++) {
|
||
const { k, v } = pairs[i]
|
||
let suf = ''
|
||
if (deltaMode && po && Object.prototype.hasOwnProperty.call(po, k)) {
|
||
const d = v - (Number(po[k]) || 0)
|
||
suf = ' (d ' + (d >= 0 ? '+' : '') + d + ')'
|
||
}
|
||
const kt = bareTopTruncateCell(k, wn - 1, true)
|
||
const kPad = kt + ' '.repeat(Math.max(0, wn - kt.length))
|
||
out.push(' ' + kPad + ' ' + String(v) + suf)
|
||
}
|
||
if (pairs.length > lim) out.push(' … +' + (pairs.length - lim) + ' counters')
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} w
|
||
*/
|
||
function bareTopWarmReadCacheSummaryLines(w) {
|
||
if (!w || typeof w !== 'object') return []
|
||
const o = /** @type {Record<string, unknown>} */ (w)
|
||
const parts = []
|
||
for (const key of ['entries', 'bytes', 'hits', 'misses', 'evictions']) {
|
||
if (o[key] != null) parts.push(key + '=' + String(o[key]))
|
||
}
|
||
if (!parts.length) {
|
||
const keys = Object.keys(o).slice(0, 8)
|
||
for (const k of keys) {
|
||
const v = o[k]
|
||
if (v != null && typeof v !== 'object') parts.push(k + '=' + String(v))
|
||
}
|
||
}
|
||
if (!parts.length) return []
|
||
return [' ' + parts.join(' ')]
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} ipc
|
||
* @param {number} maxLines
|
||
*/
|
||
function bareTopIpcTelemetrySummaryLines(ipc, maxLines) {
|
||
if (!ipc || typeof ipc !== 'object') return []
|
||
const o = /** @type {Record<string, unknown>} */ (ipc)
|
||
/** @type {string[]} */
|
||
const lines = []
|
||
for (const k of Object.keys(o).slice(0, 24)) {
|
||
const v = o[k]
|
||
if (v != null && typeof v !== 'object') {
|
||
lines.push(' ' + k + ': ' + String(v))
|
||
} else if (v && typeof v === 'object' && !Array.isArray(v)) {
|
||
const sub = /** @type {Record<string, unknown>} */ (v)
|
||
const inner = Object.keys(sub)
|
||
.slice(0, 6)
|
||
.map(
|
||
(sk) =>
|
||
sk +
|
||
'=' +
|
||
(sub[sk] != null && typeof sub[sk] !== 'object'
|
||
? String(sub[sk])
|
||
: bareTopJsonSnippet(sub[sk], 24))
|
||
)
|
||
.join(' ')
|
||
lines.push(' ' + k + ': ' + bareTopTruncateCell(inner, 72, true))
|
||
}
|
||
if (lines.length >= maxLines) break
|
||
}
|
||
if (lines.length >= maxLines && Object.keys(o).length > maxLines)
|
||
lines.push(' … (truncated)')
|
||
return lines
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} rep
|
||
* @param {string} na
|
||
*/
|
||
function bareTopReplicationOverviewLines(rep, na) {
|
||
if (!rep || typeof rep !== 'object') return []
|
||
const o = /** @type {Record<string, unknown>} */ (rep)
|
||
/** @type {string[]} */
|
||
const lines = []
|
||
const sys = o.systemCoreLength
|
||
const per = o.personalCoreLength
|
||
const aux = o.auxiliaryDriveCount
|
||
const pc = o.peerCount
|
||
if (typeof sys === 'number') lines.push(' sysCoreLen ' + sys)
|
||
if (typeof per === 'number') lines.push(' perCoreLen ' + per)
|
||
if (typeof aux === 'number') lines.push(' auxDrives ' + aux)
|
||
if (typeof pc === 'number') lines.push(' peerCount ' + pc)
|
||
const sh = o.stallHint
|
||
if (sh != null && String(sh).trim()) {
|
||
lines.push(' stallHint ' + String(sh))
|
||
const sl = String(sh).trim().toLowerCase()
|
||
if (sl === 'no_peers' || sl === 'length_unavailable' || sl === 'ok')
|
||
lines.push(
|
||
' (hint is operational — health score treats these as non-fault)'
|
||
)
|
||
}
|
||
if (!lines.length) lines.push(' ' + na)
|
||
return lines
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} cold
|
||
* @param {unknown} stdlib
|
||
* @param {unknown} tel
|
||
*/
|
||
function bareTopBootBudgetLines(cold, stdlib, tel) {
|
||
/** @type {string[]} */
|
||
const lines = []
|
||
const c = cold && typeof cold === 'object' ? /** @type {Record<string, unknown>} */ (cold) : null
|
||
const s = stdlib && typeof stdlib === 'object' ? /** @type {Record<string, unknown>} */ (stdlib) : null
|
||
if (c) {
|
||
const ex = c.exceeded === true
|
||
lines.push(
|
||
' coldBoot exceeded=' +
|
||
(ex ? 'yes' : 'no') +
|
||
' wall=' +
|
||
String(c.wallMs ?? '—') +
|
||
' limit=' +
|
||
String(c.limitMs ?? '—')
|
||
)
|
||
}
|
||
if (s) {
|
||
const ex = s.exceeded === true
|
||
lines.push(
|
||
' bareStdlib exceeded=' +
|
||
(ex ? 'yes' : 'no') +
|
||
' wall=' +
|
||
String(s.wallMs ?? '—') +
|
||
' limit=' +
|
||
String(s.limitMs ?? '—')
|
||
)
|
||
}
|
||
if (tel && typeof tel === 'object') {
|
||
const t = /** @type {Record<string, unknown>} */ (tel)
|
||
lines.push(
|
||
' policy strict=' +
|
||
String(!!t.strict) +
|
||
' bootPerf=' +
|
||
String(t.bootPerfJsonPath || '/run/bare-os/boot-perf.json')
|
||
)
|
||
}
|
||
return lines
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} sl
|
||
*/
|
||
function bareTopSwarmLifecycleLine(sl) {
|
||
if (!sl || typeof sl !== 'object') return ''
|
||
const o = /** @type {Record<string, unknown>} */ (sl)
|
||
const parts = []
|
||
for (const k of Object.keys(o).slice(0, 12)) {
|
||
const v = o[k]
|
||
if (v != null && typeof v !== 'object') parts.push(k + '=' + v)
|
||
}
|
||
if (!parts.length) return ''
|
||
return ' swarm ' + parts.join(' ') + ' (tab: operator)'
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} pt
|
||
*/
|
||
function bareTopProcessStateHistogramLines(pt) {
|
||
const rows = bareTopProcessRowsFromTable(pt)
|
||
/** @type {Record<string, number>} */
|
||
const h = {}
|
||
for (const r of rows) {
|
||
const st = String(r.state || '?')
|
||
h[st] = (h[st] || 0) + 1
|
||
}
|
||
const parts = Object.keys(h)
|
||
.sort()
|
||
.map((k) => k + ':' + h[k])
|
||
if (!parts.length) return []
|
||
return [' states ' + parts.join(' ')]
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} ir
|
||
*/
|
||
function bareTopInitdReadinessSummaryLines(ir) {
|
||
if (!ir || typeof ir !== 'object') return []
|
||
const o = /** @type {Record<string, unknown>} */ (ir)
|
||
const units = Array.isArray(o.units) ? o.units : []
|
||
let active = 0
|
||
let starting = 0
|
||
let failed = 0
|
||
let other = 0
|
||
for (const u of units) {
|
||
if (!u || typeof u !== 'object') continue
|
||
const ph = String(/** @type {Record<string, unknown>} */ (u).phase || '')
|
||
if (ph === 'active') active++
|
||
else if (ph === 'starting') starting++
|
||
else if (ph === 'failed') failed++
|
||
else other++
|
||
}
|
||
if (!units.length) return []
|
||
return [
|
||
' initd units=' +
|
||
units.length +
|
||
' active=' +
|
||
active +
|
||
' starting=' +
|
||
starting +
|
||
' failed=' +
|
||
failed +
|
||
(other ? ' other=' + other : '')
|
||
]
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} m metrics live
|
||
* @param {number} nowMs
|
||
* @param {string} na
|
||
*/
|
||
function bareTopMetaCoalesceLine(m, nowMs, na) {
|
||
if (!m || typeof m !== 'object') return ''
|
||
const o = /** @type {Record<string, unknown>} */ (m)
|
||
const co = o.coalesceMs
|
||
const at = o.atMs
|
||
let age = ''
|
||
if (typeof at === 'number' && Number.isFinite(at)) {
|
||
age = ' age=' + Math.max(0, Math.floor(nowMs - at)) + 'ms'
|
||
}
|
||
return (
|
||
' metrics coalesceMs=' +
|
||
(typeof co === 'number' ? co : na) +
|
||
' atMs=' +
|
||
(typeof at === 'number' ? at : na) +
|
||
age
|
||
)
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} wbh
|
||
*/
|
||
function bareTopWorkerBudgetCompactLine(wbh) {
|
||
if (!wbh || typeof wbh !== 'object') return ''
|
||
const o = /** @type {Record<string, unknown>} */ (wbh)
|
||
const parts = []
|
||
if (o.wallMsMax != null) parts.push('wallMax=' + String(o.wallMsMax))
|
||
if (o.cpuMsMax != null) parts.push('cpuMax=' + String(o.cpuMsMax))
|
||
if (!parts.length) return ''
|
||
return ' workerBudget ' + parts.join(' ')
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} cs collaboration session object
|
||
*/
|
||
function bareTopProtomuxCollabLine(cs) {
|
||
if (!cs || typeof cs !== 'object') return ''
|
||
const o = /** @type {Record<string, unknown>} */ (cs)
|
||
const rx = o.protomuxAppChannelRxTotal
|
||
const cap = o.protomuxCapChannelRxTotal
|
||
const parts = []
|
||
if (typeof rx === 'number') parts.push('appRx=' + rx)
|
||
if (typeof cap === 'number') parts.push('capRx=' + cap)
|
||
if (!parts.length) return ''
|
||
return ' protomux ' + parts.join(' ')
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} pt
|
||
*/
|
||
function bareTopLogicalFdAggregateLine(pt) {
|
||
const rows = bareTopProcessRowsFromTable(pt)
|
||
let n = 0
|
||
for (const r of rows) {
|
||
const ft = r.fdTable
|
||
if (Array.isArray(ft)) n += ft.length
|
||
}
|
||
if (!n) return ''
|
||
return ' logicalFds rows=' + n + ' (fdTable entries)'
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} hostStats
|
||
*/
|
||
function bareTopHostStatsSummaryLine(hostStats) {
|
||
if (!hostStats || typeof hostStats !== 'object') return ''
|
||
const o = /** @type {Record<string, unknown>} */ (hostStats)
|
||
const pick = [
|
||
'cpuBusyPct',
|
||
'cpuPct',
|
||
'memUsedPct',
|
||
'rssBytes',
|
||
'hostname',
|
||
'eventLoopLagMs',
|
||
'loopLagMs',
|
||
'lagMs'
|
||
]
|
||
const parts = []
|
||
for (const k of pick) {
|
||
if (o[k] != null && typeof o[k] !== 'object')
|
||
parts.push(k + '=' + String(o[k]))
|
||
}
|
||
if (!parts.length) return ''
|
||
return ' host ' + parts.join(' ') + ' (detail: host tab)'
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} resources
|
||
* @param {number} maxKeys
|
||
*/
|
||
function bareTopResourcesSummaryLines(resources, maxKeys) {
|
||
if (!resources || typeof resources !== 'object') return []
|
||
const o = /** @type {Record<string, unknown>} */ (resources)
|
||
const keys = Object.keys(o).slice(0, Math.max(1, maxKeys | 0))
|
||
/** @type {string[]} */
|
||
const lines = []
|
||
for (const k of keys) {
|
||
const v = o[k]
|
||
if (v != null && typeof v !== 'object')
|
||
lines.push(' ' + k + ': ' + String(v).slice(0, 120))
|
||
}
|
||
if (Object.keys(o).length > keys.length) lines.push(' … +' + (Object.keys(o).length - keys.length) + ' keys (full: diagnostics)')
|
||
return lines
|
||
}
|
||
|
||
/**
|
||
* @param {number} peersMl peers from metrics_live
|
||
* @param {unknown} repLive replicationLive object
|
||
*/
|
||
function bareTopPeerMismatchWarningLine(peersMl, repLive) {
|
||
if (!repLive || typeof repLive !== 'object') return ''
|
||
const o = /** @type {Record<string, unknown>} */ (repLive)
|
||
const pc = o.peerCount
|
||
if (typeof pc !== 'number' || !Number.isFinite(peersMl)) return ''
|
||
if (Math.abs(pc - peersMl) > 2)
|
||
return ' warn peerCount mismatch metrics peers=' + peersMl + ' replication.peerCount=' + pc
|
||
return ''
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} extraIpc from /proc/bare_os/ipc_backpressure.json
|
||
*/
|
||
function bareTopIpcBackpressureProcLine(extraIpc) {
|
||
if (!extraIpc || typeof extraIpc !== 'object') return ''
|
||
const o = /** @type {Record<string, unknown>} */ (extraIpc)
|
||
const parts = []
|
||
for (const k of Object.keys(o).slice(0, 14)) {
|
||
const v = o[k]
|
||
if (typeof v === 'number' && v !== 0)
|
||
parts.push(k + '=' + bareTopFormatIntGrouped(v))
|
||
else if (v === true) parts.push(k)
|
||
}
|
||
if (!parts.length) return ''
|
||
return ' ipcBackpressure(proc) ' + parts.join(' ')
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} syscallsParsed
|
||
*/
|
||
function bareTopSyscallsOverviewLines(syscallsParsed) {
|
||
if (!syscallsParsed || typeof syscallsParsed !== 'object') return []
|
||
const o = /** @type {Record<string, unknown>} */ (syscallsParsed)
|
||
const ops = o.ops
|
||
const n = Array.isArray(ops) ? ops.length : 0
|
||
return [
|
||
' syscalls schemaVersion=' +
|
||
String(o.schemaVersion ?? '?') +
|
||
' stockOps~' +
|
||
n
|
||
]
|
||
}
|
||
|
||
/**
|
||
* @param {string} diskRaw
|
||
*/
|
||
function bareTopDiskstatsOverviewLines(diskRaw) {
|
||
const s = String(diskRaw || '').trim()
|
||
if (!s) return []
|
||
const first = s.split('\n')[0] || ''
|
||
if (first.length < 6) return []
|
||
return [' diskstats ' + bareTopTruncateCell(first, 110, true)]
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} dr delegate_red.json
|
||
*/
|
||
function bareTopDelegateRedOverviewLines(dr) {
|
||
if (!dr || typeof dr !== 'object') return []
|
||
if (!Object.keys(dr).length) return []
|
||
return [
|
||
' delegate_red ' +
|
||
bareTopTruncateCell(bareTopJsonSnippet(dr, 220), 118, true)
|
||
]
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} sp security_posture.json
|
||
*/
|
||
function bareTopSecurityPostureOverviewLines(sp) {
|
||
if (!sp || typeof sp !== 'object') return []
|
||
const o = /** @type {Record<string, unknown>} */ (sp)
|
||
const parts = []
|
||
if (typeof o.schema === 'number') parts.push('schema=' + o.schema)
|
||
if (o.bootPolicyStrict != null) parts.push('bootStrict=' + String(o.bootPolicyStrict))
|
||
if (o.bootPolicyMerged != null) parts.push('bootPol=' + String(o.bootPolicyMerged))
|
||
if (o.ctxApiVersion != null) parts.push('ctxApi=' + String(o.ctxApiVersion).slice(0, 20))
|
||
return parts.length ? [' security_posture ' + parts.join(' ')] : []
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} bb boot_budget_summary.json
|
||
*/
|
||
function bareTopBootBudgetSummaryOverviewLines(bb) {
|
||
if (!bb || typeof bb !== 'object') return []
|
||
const o = /** @type {Record<string, unknown>} */ (bb)
|
||
const parts = []
|
||
const crit = o.criticalPathMs ?? o.criticalPathWallMs ?? o.totalWallMs
|
||
if (typeof crit === 'number') parts.push('critical~' + bareTopFormatDurationMs(crit))
|
||
const top = o.slowestUnit ?? o.criticalUnit ?? o.topUnit ?? o.bottleneck
|
||
if (top != null) parts.push('top=' + String(top).slice(0, 28))
|
||
if (o.exceeded === true) parts.push('EXCEEDED')
|
||
return parts.length ? [' boot_budget_summary ' + parts.join(' ')] : []
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} ph peer_health.json
|
||
*/
|
||
function bareTopWorstPeerOverviewLines(ph) {
|
||
if (!ph || typeof ph !== 'object') return []
|
||
const peers = /** @type {Record<string, unknown>} */ (ph).peers
|
||
if (!Array.isArray(peers) || !peers.length) return []
|
||
let worst = /** @type {Record<string, unknown> | null} */ (null)
|
||
let worstScore = -1
|
||
for (const p of peers) {
|
||
if (!p || typeof p !== 'object') continue
|
||
const r = /** @type {Record<string, unknown>} */ (p)
|
||
const err = r.error ?? r.lastError ?? r.status
|
||
const lat = Number(r.rttMs ?? r.latencyMs ?? r.rtt ?? 0)
|
||
const sc =
|
||
err != null && String(err).trim() ? 1e12 : Number.isFinite(lat) ? lat : 0
|
||
if (sc > worstScore) {
|
||
worstScore = sc
|
||
worst = r
|
||
}
|
||
}
|
||
if (!worst) return []
|
||
const id = String(
|
||
worst.publicKeyHex?.slice?.(0, 14) ||
|
||
worst.publicKey?.slice?.(0, 14) ||
|
||
worst.peerId ||
|
||
worst.id ||
|
||
'?'
|
||
)
|
||
const tail = bareTopJsonSnippet(worst, 72)
|
||
return [' worstPeer ' + id + ' ' + tail]
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} ext extensions.json
|
||
*/
|
||
function bareTopExtensionsOverviewLines(ext) {
|
||
if (!ext || typeof ext !== 'object') return []
|
||
const root = /** @type {Record<string, unknown>} */ (ext)
|
||
const list = root.extensions ?? root.names ?? root.list
|
||
const names = Array.isArray(list)
|
||
? list
|
||
.map((x) => {
|
||
if (x && typeof x === 'object')
|
||
return String(
|
||
/** @type {Record<string, unknown>} */ (x).name ??
|
||
/** @type {Record<string, unknown>} */ (x).id ??
|
||
''
|
||
)
|
||
return String(x)
|
||
})
|
||
.filter(Boolean)
|
||
: []
|
||
const head = names.slice(0, 5).join(', ')
|
||
return [
|
||
' extensions count=' +
|
||
names.length +
|
||
(head ? ' e.g. ' + bareTopTruncateCell(head, 56, true) : '')
|
||
]
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} cj capabilities.json
|
||
* @param {unknown} cn capabilities node JSON
|
||
*/
|
||
function bareTopCapabilitiesDiffOverviewLines(cj, cn) {
|
||
if (!cj || typeof cj !== 'object' || !cn || typeof cn !== 'object') return []
|
||
const a = /** @type {Record<string, unknown>} */ (cj)
|
||
const b = /** @type {Record<string, unknown>} */ (cn)
|
||
const ka = new Set(Object.keys(a))
|
||
const kb = new Set(Object.keys(b))
|
||
const onlyA = [...ka].filter((k) => !kb.has(k)).slice(0, 8)
|
||
const onlyB = [...kb].filter((k) => !ka.has(k)).slice(0, 8)
|
||
/** @type {string[]} */
|
||
const out = []
|
||
if (onlyA.length) out.push(' cap onlyInJson: ' + onlyA.join(', '))
|
||
if (onlyB.length) out.push(' cap onlyInNode: ' + onlyB.join(', '))
|
||
if (!out.length)
|
||
out.push(' cap seed JSON vs /capabilities: same top-level keys (sampled)')
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} gitDel
|
||
* @param {unknown} gitLfs
|
||
* @param {unknown} prevDel
|
||
* @param {unknown} prevLfs
|
||
*/
|
||
function bareTopGitStatsDeltaOverviewLines(gitDel, gitLfs, prevDel, prevLfs) {
|
||
function pickNum(o, keys) {
|
||
if (!o || typeof o !== 'object') return null
|
||
const r = /** @type {Record<string, unknown>} */ (o)
|
||
for (const k of keys) {
|
||
const v = r[k]
|
||
if (typeof v === 'number' && Number.isFinite(v)) return v
|
||
}
|
||
return null
|
||
}
|
||
/** @type {string[]} */
|
||
const out = []
|
||
const c1 = pickNum(gitDel, ['delegatesStarted', 'total', 'invocations'])
|
||
const p1 = pickNum(prevDel, ['delegatesStarted', 'total', 'invocations'])
|
||
if (c1 != null && p1 != null) out.push(' gitDelegate d=' + (c1 - p1))
|
||
const c2 = pickNum(gitLfs, ['pointersResolved', 'resolved', 'total'])
|
||
const p2 = pickNum(prevLfs, ['pointersResolved', 'resolved', 'total'])
|
||
if (c2 != null && p2 != null) out.push(' gitLfs d=' + (c2 - p2))
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} wb worker_budget.json
|
||
* @param {unknown} sb sandbox_profile.json
|
||
*/
|
||
function bareTopWorkerSandboxOverviewLines(wb, sb) {
|
||
const parts = []
|
||
if (wb && typeof wb === 'object') {
|
||
const o = /** @type {Record<string, unknown>} */ (wb)
|
||
const wm = Number(o.wallMsMax)
|
||
const wu = Number(o.wallMsUsed ?? o.wallMsTotal ?? o.usedWallMs)
|
||
if (Number.isFinite(wm) && wm > 0 && Number.isFinite(wu)) {
|
||
const pct = Math.min(100, Math.round((wu / wm) * 100))
|
||
if (pct >= 85) parts.push('workerBudget~' + pct + '%')
|
||
}
|
||
}
|
||
if (sb && typeof sb === 'object') {
|
||
const o = /** @type {Record<string, unknown>} */ (sb)
|
||
if (o.denied === true || o.violationCount === true || o.violations)
|
||
parts.push('sandbox:check')
|
||
}
|
||
return parts.length ? [' worker/sandbox ' + parts.join(' ')] : []
|
||
}
|
||
|
||
/** Default overview section ids in render order */
|
||
var BARE_TOP_OVERVIEW_DEFAULT_SECTIONS = [
|
||
'session',
|
||
'pipeline',
|
||
'delegates',
|
||
'replication',
|
||
'nethealth',
|
||
'kernel',
|
||
'warm',
|
||
'ipc',
|
||
'boot',
|
||
'swarm',
|
||
'prochist',
|
||
'offenders',
|
||
'microtrends',
|
||
'protomux',
|
||
'meta',
|
||
'worker',
|
||
'initd',
|
||
'fdagg',
|
||
'delegate_rates',
|
||
'fairness',
|
||
'subprocess',
|
||
'host',
|
||
'limits',
|
||
'p2p',
|
||
'diskio',
|
||
'syscallsum',
|
||
'delegate_red',
|
||
'security',
|
||
'bootsum',
|
||
'badpeer',
|
||
'capdiff',
|
||
'extsum',
|
||
'gitcounters',
|
||
'resources',
|
||
'readerr'
|
||
]
|
||
|
||
/**
|
||
* @param {string} raw
|
||
* @param {boolean} compact
|
||
* @returns {Set<string> | null} null = use default list
|
||
*/
|
||
function bareTopOverviewSectionSet(raw, compact) {
|
||
const s = String(raw || '').trim()
|
||
if (!s) {
|
||
const base = BARE_TOP_OVERVIEW_DEFAULT_SECTIONS.slice()
|
||
if (compact) {
|
||
const drop = new Set([
|
||
'fairness',
|
||
'subprocess',
|
||
'host',
|
||
'resources',
|
||
'limits',
|
||
'p2p',
|
||
'diskio',
|
||
'syscallsum',
|
||
'security',
|
||
'bootsum',
|
||
'capdiff',
|
||
'gitcounters',
|
||
'extsum'
|
||
])
|
||
return new Set(base.filter((x) => !drop.has(x)))
|
||
}
|
||
return new Set(base)
|
||
}
|
||
const parts = s.split(/[,;]+/).map((x) => x.trim().toLowerCase()).filter(Boolean)
|
||
return new Set(parts)
|
||
}
|
||
|
||
/**
|
||
* @typedef {{
|
||
* cols: number,
|
||
* na: string,
|
||
* compact: boolean,
|
||
* sectionFilter: string,
|
||
* deltaMode: boolean,
|
||
* prevMetrics: Record<string, unknown> | null,
|
||
* nowMs: number,
|
||
* asciiSep: boolean,
|
||
* flattenCap: ((v: unknown, maxLines: number, maxKeys: number) => string[]) | null,
|
||
* ringProto: number[],
|
||
* microTrendCpu?: number[],
|
||
* microTrendMem?: number[],
|
||
* microTrendNet?: number[],
|
||
* protomuxSpark: boolean,
|
||
* sparkW: number,
|
||
* sparkAscii: boolean,
|
||
* logSpark: boolean,
|
||
* braille: boolean,
|
||
* healthDetail: boolean,
|
||
* healthBreakdown: string,
|
||
* splitLeftCol: number,
|
||
* splitMiniProc: boolean,
|
||
* layoutVersion: string,
|
||
* sectionsRaw?: string,
|
||
* sessionWallRing?: number[],
|
||
* prevSnap?: Record<string, unknown> | null
|
||
* }} BareTopOverviewOpts
|
||
*/
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} snap
|
||
* @param {BareTopOverviewOpts} opts
|
||
* @returns {{ lines: string[], activeSections: string[], sectionRawLineIndex: Record<string, number> }}
|
||
*/
|
||
function bareTopOverviewLines(snap, opts) {
|
||
const na = opts.na || 'N/A'
|
||
const cols = Math.max(40, opts.cols | 0)
|
||
const fc = opts.flattenCap
|
||
const sectionSet =
|
||
opts.sectionsRaw != null && String(opts.sectionsRaw).trim()
|
||
? bareTopOverviewSectionSet(String(opts.sectionsRaw), false)
|
||
: bareTopOverviewSectionSet('', opts.compact)
|
||
|
||
const filter = String(opts.sectionFilter || '').trim().toLowerCase()
|
||
/** @type {string[]} */
|
||
const lines = []
|
||
/** @type {string[]} */
|
||
const active = []
|
||
/** @type {Record<string, number>} */
|
||
const sectionRawLineIndex = {}
|
||
|
||
const m = snap.metricsLive
|
||
const sess =
|
||
m && typeof m.session === 'object' && m.session
|
||
? /** @type {Record<string, unknown>} */ (m.session)
|
||
: {}
|
||
|
||
function want(id) {
|
||
if (!sectionSet.has(id)) return false
|
||
if (filter && !id.includes(filter) && !bareTopSectionTitleFor(id).toLowerCase().includes(filter))
|
||
return false
|
||
return true
|
||
}
|
||
|
||
function pushSec(title, id, bodyLines) {
|
||
if (!want(id)) return
|
||
sectionRawLineIndex[id] = lines.length
|
||
active.push(id)
|
||
lines.push('')
|
||
lines.push(bareTopSectionSeparator(title, cols, opts.asciiSep))
|
||
for (const ln of bodyLines) lines.push(bareTopSanitizeVisible(ln))
|
||
}
|
||
|
||
const ec = Number(sess.execLineCount) || 0
|
||
const pb = Number(sess.pipelineBytesTotal) || 0
|
||
const wm = Number(sess.execLineWallMsTotal) || 0
|
||
|
||
if (want('session')) {
|
||
sectionRawLineIndex.session = lines.length
|
||
active.push('session')
|
||
lines.push('')
|
||
lines.push(bareTopSectionSeparator('Session', cols, opts.asciiSep))
|
||
lines.push(
|
||
bareTopSanitizeVisible(
|
||
' execLineCount=' + (sess.execLineCount ?? na) + ' pipelineBytesTotal=' + (sess.pipelineBytesTotal ?? na)
|
||
)
|
||
)
|
||
lines.push(
|
||
bareTopSanitizeVisible(' execLineWallMsTotal=' + (sess.execLineWallMsTotal ?? na))
|
||
)
|
||
const ringW = opts.sessionWallRing
|
||
if (ringW && ringW.length && opts.sparkW > 0) {
|
||
const sp = bareTopSparklineOverview(
|
||
ringW,
|
||
Math.min(32, opts.sparkW),
|
||
opts.sparkAscii,
|
||
opts.logSpark,
|
||
opts.braille
|
||
)
|
||
lines.push(bareTopSanitizeVisible(' execLineWallMsTotal spark ' + sp))
|
||
}
|
||
}
|
||
|
||
if (want('pipeline')) {
|
||
const pg = bareTopPipelineGaugesFromMetrics(m && m.pipeline, cols)
|
||
pushSec('Pipeline limits', 'pipeline', pg ? [pg] : [' ' + na])
|
||
}
|
||
|
||
if (want('delegates')) {
|
||
const delI = m && m.delegateInflight
|
||
/** @type {string[]} */
|
||
const dl = []
|
||
if (delI != null && typeof delI === 'object') {
|
||
dl.push(...bareTopDelegateInflightTableLines(delI, 20, Math.min(24, Math.floor(cols * 0.35))))
|
||
} else dl.push(' ' + na)
|
||
pushSec('Delegates (inflight)', 'delegates', dl)
|
||
}
|
||
|
||
const repLive = m && m.replicationLive
|
||
if (want('replication')) {
|
||
pushSec('Replication live', 'replication', bareTopReplicationOverviewLines(repLive, na))
|
||
}
|
||
|
||
if (want('nethealth')) {
|
||
const nl = bareTopNetHealthOverviewLines(snap.netSummary, m)
|
||
if (nl.length) pushSec('Network summary', 'nethealth', nl)
|
||
}
|
||
|
||
if (want('kernel')) {
|
||
const prevK =
|
||
opts.prevMetrics && opts.prevMetrics.kernelCounters
|
||
? opts.prevMetrics.kernelCounters
|
||
: null
|
||
const kTop = opts.compact ? 12 : 22
|
||
const kl = bareTopKernelCounterLines(
|
||
m && m.kernelCounters,
|
||
prevK,
|
||
opts.deltaMode,
|
||
kTop,
|
||
Math.min(28, Math.floor(cols * 0.38))
|
||
)
|
||
pushSec('Kernel counters', 'kernel', kl.length ? kl : [' ' + na])
|
||
}
|
||
|
||
if (want('warm')) {
|
||
const wl = bareTopWarmReadCacheSummaryLines(m && m.warmReadCache)
|
||
pushSec('Warm read cache', 'warm', wl.length ? wl : [' ' + na])
|
||
}
|
||
|
||
if (want('ipc')) {
|
||
/** @type {string[]} */
|
||
const ibody = bareTopIpcTelemetrySummaryLines(m && m.ipcTelemetry, 8).slice()
|
||
const ipL = bareTopIpcBackpressureProcLine(
|
||
snap.extra && snap.extra.ipcBackpressure
|
||
)
|
||
if (ipL) ibody.push(ipL)
|
||
pushSec('IPC telemetry', 'ipc', ibody.length ? ibody : [' ' + na])
|
||
}
|
||
|
||
if (want('boot')) {
|
||
const bl = bareTopBootBudgetLines(
|
||
m && m.bootBudgetCold,
|
||
m && m.bootBudgetBareStdlib,
|
||
m && m.bootBudgetTelemetry
|
||
)
|
||
pushSec('Boot budget', 'boot', bl.length ? bl : [' ' + na])
|
||
}
|
||
|
||
if (want('swarm') && m && m.swarmLifecycle) {
|
||
const sl = bareTopSwarmLifecycleLine(m.swarmLifecycle)
|
||
pushSec('Swarm lifecycle', 'swarm', sl ? [sl] : [' ' + na])
|
||
}
|
||
|
||
const pt =
|
||
(m && m.processTable && typeof m.processTable === 'object'
|
||
? m.processTable
|
||
: null) ||
|
||
(snap.extra &&
|
||
snap.extra.processTable &&
|
||
typeof snap.extra.processTable === 'object'
|
||
? snap.extra.processTable
|
||
: null)
|
||
|
||
if (want('prochist')) {
|
||
pushSec('Process states', 'prochist', bareTopProcessStateHistogramLines(pt))
|
||
}
|
||
|
||
if (want('offenders') && pt) {
|
||
const rows = bareTopProcessRowsFromTable(pt)
|
||
const byCpu = rows
|
||
.slice()
|
||
.sort(
|
||
(a, b) =>
|
||
(Number(b.cpuPct ?? b.cpu ?? 0) || 0) - (Number(a.cpuPct ?? a.cpu ?? 0) || 0)
|
||
)
|
||
.slice(0, 3)
|
||
const byRss = rows
|
||
.slice()
|
||
.sort(
|
||
(a, b) =>
|
||
(Number(b.rssBytes ?? b.rss ?? 0) || 0) - (Number(a.rssBytes ?? a.rss ?? 0) || 0)
|
||
)
|
||
.slice(0, 3)
|
||
const byIo = rows
|
||
.slice()
|
||
.sort(
|
||
(a, b) =>
|
||
(Number(b.ioBytesDelta ?? b.ioBytes ?? 0) || 0) - (Number(a.ioBytesDelta ?? a.ioBytes ?? 0) || 0)
|
||
)
|
||
.slice(0, 3)
|
||
/** @type {string[]} */
|
||
const out = []
|
||
if (byCpu.length)
|
||
out.push(
|
||
' cpu ' +
|
||
byCpu
|
||
.map((r) => String(r.name || r.pid || '?') + ':' + String(Math.round(Number(r.cpuPct ?? r.cpu ?? 0) || 0)) + '%')
|
||
.join(' ')
|
||
)
|
||
if (byRss.length)
|
||
out.push(
|
||
' rss ' +
|
||
byRss
|
||
.map((r) => String(r.name || r.pid || '?') + ':' + bareTopNetFormatBytes(Number(r.rssBytes ?? r.rss ?? 0) || 0))
|
||
.join(' ')
|
||
)
|
||
if (byIo.length)
|
||
out.push(
|
||
' io ' +
|
||
byIo
|
||
.map((r) => String(r.name || r.pid || '?') + ':' + bareTopNetFormatBytes(Number(r.ioBytesDelta ?? r.ioBytes ?? 0) || 0))
|
||
.join(' ')
|
||
)
|
||
if (out.length) pushSec('Top offenders', 'offenders', out)
|
||
}
|
||
|
||
if (want('microtrends') && opts.sparkW > 0) {
|
||
/** @type {string[]} */
|
||
const tr = []
|
||
if (opts.microTrendCpu && opts.microTrendCpu.length) {
|
||
tr.push(
|
||
' cpu ' +
|
||
bareTopSparklineOverview(
|
||
opts.microTrendCpu,
|
||
Math.min(28, opts.sparkW),
|
||
opts.sparkAscii,
|
||
opts.logSpark,
|
||
opts.braille
|
||
)
|
||
)
|
||
}
|
||
if (opts.microTrendMem && opts.microTrendMem.length) {
|
||
tr.push(
|
||
' mem ' +
|
||
bareTopSparklineOverview(
|
||
opts.microTrendMem,
|
||
Math.min(28, opts.sparkW),
|
||
opts.sparkAscii,
|
||
opts.logSpark,
|
||
opts.braille
|
||
)
|
||
)
|
||
}
|
||
if (opts.microTrendNet && opts.microTrendNet.length) {
|
||
tr.push(
|
||
' net ' +
|
||
bareTopSparklineOverview(
|
||
opts.microTrendNet,
|
||
Math.min(28, opts.sparkW),
|
||
opts.sparkAscii,
|
||
opts.logSpark,
|
||
opts.braille
|
||
)
|
||
)
|
||
}
|
||
if (tr.length) pushSec('Micro trends', 'microtrends', tr)
|
||
}
|
||
|
||
if (want('protomux')) {
|
||
const cs =
|
||
repLive &&
|
||
typeof repLive === 'object' &&
|
||
repLive.collaborationSession &&
|
||
typeof repLive.collaborationSession === 'object'
|
||
? repLive.collaborationSession
|
||
: null
|
||
/** @type {string[]} */
|
||
const pl = []
|
||
const cl = bareTopProtomuxCollabLine(cs)
|
||
if (cl) pl.push(cl)
|
||
if (opts.protomuxSpark && opts.ringProto && opts.ringProto.length && opts.sparkW > 0) {
|
||
const sp = bareTopSparklineOverview(
|
||
opts.ringProto,
|
||
opts.sparkW,
|
||
opts.sparkAscii,
|
||
opts.logSpark,
|
||
opts.braille
|
||
)
|
||
pl.push(' appRx spark ' + sp)
|
||
}
|
||
if (pl.length) pushSec('Protomux / collab', 'protomux', pl)
|
||
}
|
||
|
||
if (want('meta')) {
|
||
const ml = bareTopMetaCoalesceLine(m, opts.nowMs, na)
|
||
if (ml) pushSec('Refresh meta', 'meta', [ml])
|
||
}
|
||
|
||
if (want('worker')) {
|
||
/** @type {string[]} */
|
||
const wbod = []
|
||
const wl = bareTopWorkerBudgetCompactLine(m && m.workerBudgetHints)
|
||
if (wl) wbod.push(wl)
|
||
const ws = bareTopWorkerSandboxOverviewLines(
|
||
snap.extra && snap.extra.workerBudget,
|
||
snap.extra && snap.extra.sandboxProfile
|
||
)
|
||
for (const x of ws) wbod.push(x)
|
||
if (wbod.length) pushSec('Worker budget', 'worker', wbod)
|
||
}
|
||
|
||
if (want('initd')) {
|
||
pushSec('Initd readiness', 'initd', bareTopInitdReadinessSummaryLines(m && m.initdReadiness))
|
||
}
|
||
|
||
if (want('fdagg')) {
|
||
const fl = bareTopLogicalFdAggregateLine(pt)
|
||
if (fl) pushSec('FD summary', 'fdagg', [fl])
|
||
}
|
||
|
||
if (want('delegate_rates') && m && m.delegateRateBuckets != null) {
|
||
pushSec('Delegate rate buckets', 'delegate_rates', bareTopDelegateRateBucketLines(m.delegateRateBuckets, cols))
|
||
}
|
||
|
||
if (want('fairness') && snap.fairnessSnapshot && fc) {
|
||
const fl = fc(snap.fairnessSnapshot, 14, 32)
|
||
const expl = [
|
||
' Fairness = delegate / job scheduling hints (not host CPU share).'
|
||
]
|
||
pushSec('Fairness', 'fairness', expl.concat(fl))
|
||
}
|
||
|
||
if (want('subprocess') && snap.subprocessBridge && fc) {
|
||
pushSec('Subprocess bridge', 'subprocess', fc(snap.subprocessBridge, 12, 24))
|
||
}
|
||
|
||
if (want('limits')) {
|
||
const ll = bareTopLimitsMergedLines(
|
||
/** @type {Record<string, unknown>} */ (snap)
|
||
)
|
||
if (ll.length) {
|
||
ll.push(
|
||
' note: quotas = operator pipeline caps; rlimits = per-process limits mirror.'
|
||
)
|
||
pushSec('Limits (quotas + rlimits)', 'limits', ll)
|
||
}
|
||
}
|
||
|
||
if (want('p2p')) {
|
||
const pl = bareTopP2PStackLines(/** @type {Record<string, unknown>} */ (snap))
|
||
if (pl.length) pushSec('P2P / HDMS / DHT', 'p2p', pl)
|
||
}
|
||
|
||
if (want('diskio') && snap.diskstatsLine) {
|
||
const dl = bareTopDiskstatsOverviewLines(String(snap.diskstatsLine))
|
||
if (dl.length) pushSec('Disk', 'diskio', dl)
|
||
}
|
||
|
||
if (want('syscallsum')) {
|
||
const sy = snap.extra && snap.extra.syscalls
|
||
const sl = bareTopSyscallsOverviewLines(
|
||
sy && typeof sy === 'object' ? sy : null
|
||
)
|
||
if (sl.length) pushSec('Syscalls proc', 'syscallsum', sl)
|
||
}
|
||
|
||
if (want('delegate_red')) {
|
||
const dr = snap.extra && snap.extra.delegateRed
|
||
const dl = bareTopDelegateRedOverviewLines(dr)
|
||
if (dl.length) pushSec('Delegate red', 'delegate_red', dl)
|
||
}
|
||
|
||
if (want('security')) {
|
||
const sp = snap.extra && snap.extra.securityPosture
|
||
const sl = bareTopSecurityPostureOverviewLines(sp)
|
||
if (sl.length) pushSec('Security posture', 'security', sl)
|
||
}
|
||
|
||
if (want('bootsum')) {
|
||
const bb = snap.extra && snap.extra.bootBudgetSummary
|
||
const bl = bareTopBootBudgetSummaryOverviewLines(bb)
|
||
if (bl.length) pushSec('Boot budget file', 'bootsum', bl)
|
||
}
|
||
|
||
if (want('badpeer')) {
|
||
const ph = snap.extra && snap.extra.peerHealth
|
||
const wl = bareTopWorstPeerOverviewLines(ph)
|
||
if (wl.length) pushSec('Peer health', 'badpeer', wl)
|
||
}
|
||
|
||
if (want('capdiff')) {
|
||
const cj = snap.extra && snap.extra.capabilitiesJson
|
||
const cn = snap.extra && snap.extra.capabilitiesNode
|
||
const cl = bareTopCapabilitiesDiffOverviewLines(cj, cn)
|
||
if (cl.length) pushSec('Capabilities diff', 'capdiff', cl)
|
||
}
|
||
|
||
if (want('extsum')) {
|
||
const ex = snap.extra && snap.extra.extensions
|
||
const el = bareTopExtensionsOverviewLines(ex)
|
||
if (el.length) pushSec('Extensions', 'extsum', el)
|
||
}
|
||
|
||
if (want('gitcounters') && opts.deltaMode && opts.prevSnap) {
|
||
const gl = bareTopGitStatsDeltaOverviewLines(
|
||
snap.extra && snap.extra.gitDelegateStats,
|
||
snap.extra && snap.extra.gitLfsPointerStats,
|
||
opts.prevSnap.extra && opts.prevSnap.extra.gitDelegateStats,
|
||
opts.prevSnap.extra && opts.prevSnap.extra.gitLfsPointerStats
|
||
)
|
||
if (gl.length) pushSec('Git stats delta', 'gitcounters', gl)
|
||
}
|
||
|
||
if (want('host') && snap.hostStats) {
|
||
const hl = bareTopHostStatsSummaryLine(snap.hostStats)
|
||
if (hl) pushSec('Host stats', 'host', [hl])
|
||
}
|
||
|
||
if (want('resources') && snap.resources) {
|
||
const full = !opts.compact && fc
|
||
if (full) {
|
||
pushSec('Resources', 'resources', fc(snap.resources, 20, 40))
|
||
} else {
|
||
pushSec('Resources', 'resources', bareTopResourcesSummaryLines(snap.resources, 10))
|
||
}
|
||
}
|
||
|
||
if (
|
||
want('readerr') &&
|
||
(snap.readErr ||
|
||
(Array.isArray(snap.readErrPaths) && snap.readErrPaths.length))
|
||
) {
|
||
/** @type {string[]} */
|
||
const erl = []
|
||
if (snap.readErr) {
|
||
const err = String(snap.readErr)
|
||
const wrapped = err.split('\n').slice(0, 3)
|
||
for (const w of wrapped) erl.push(' warn ' + w)
|
||
if (err.split('\n').length > 3) erl.push(' … (truncated)')
|
||
}
|
||
const paths = Array.isArray(snap.readErrPaths) ? snap.readErrPaths : []
|
||
for (const p of paths.slice(0, 16)) {
|
||
erl.push(' read fail ' + bareTopSanitizeVisible(String(p)))
|
||
}
|
||
if (erl.length) pushSec('Read errors', 'readerr', erl)
|
||
}
|
||
|
||
const mismatch = bareTopPeerMismatchWarningLine(Number(m && m.peers) || 0, repLive)
|
||
if (mismatch && want('replication')) {
|
||
lines.push(bareTopSanitizeVisible(mismatch))
|
||
}
|
||
|
||
if (opts.healthDetail && opts.healthBreakdown) {
|
||
sectionRawLineIndex.health_detail = lines.length
|
||
lines.push('')
|
||
lines.push(bareTopSanitizeVisible(' health detail ' + opts.healthBreakdown))
|
||
active.push('health_detail')
|
||
}
|
||
|
||
return { lines, activeSections: active, sectionRawLineIndex }
|
||
}
|
||
|
||
function bareTopSectionTitleFor(id) {
|
||
const map = {
|
||
session: 'session',
|
||
pipeline: 'pipeline',
|
||
delegates: 'delegates',
|
||
replication: 'replication',
|
||
kernel: 'kernel',
|
||
warm: 'warm',
|
||
ipc: 'ipc',
|
||
boot: 'boot',
|
||
swarm: 'swarm',
|
||
prochist: 'process',
|
||
protomux: 'protomux',
|
||
meta: 'meta',
|
||
worker: 'worker',
|
||
initd: 'initd',
|
||
initd_phases: 'initd',
|
||
fdagg: 'fd',
|
||
delegate_rates: 'delegate',
|
||
fairness: 'fairness',
|
||
subprocess: 'subprocess',
|
||
host: 'host',
|
||
resources: 'resources',
|
||
readerr: 'error',
|
||
limits: 'limits',
|
||
p2p: 'p2p',
|
||
nethealth: 'network',
|
||
diskio: 'disk',
|
||
syscallsum: 'syscall',
|
||
delegate_red: 'delegate',
|
||
security: 'security',
|
||
bootsum: 'boot',
|
||
badpeer: 'peer',
|
||
capdiff: 'cap',
|
||
extsum: 'ext',
|
||
gitcounters: 'git'
|
||
}
|
||
return map[id] || id
|
||
}
|
||
|
||
/**
|
||
* Pipeline gauges string (shared with TUI).
|
||
* Duplicates bareTopPipelineGauges from tui when only helpers are tested; TUI keeps full color version.
|
||
*/
|
||
function bareTopPipelineGaugesFromMetrics(pl, cols) {
|
||
if (!pl || typeof pl !== 'object') return ''
|
||
const o = /** @type {Record<string, unknown>} */ (pl)
|
||
const parts = []
|
||
for (const k of Object.keys(o).slice(0, 8)) {
|
||
const v = o[k]
|
||
if (typeof v === 'number' && Number.isFinite(v)) {
|
||
const cap = v > 1000 ? v : 100
|
||
const pct = Math.min(100, Math.round((v / cap) * 100))
|
||
parts.push(k.slice(0, 10) + '=' + pct + '%')
|
||
}
|
||
}
|
||
return parts.join(' ').slice(0, Math.max(20, cols - 2))
|
||
}
|
||
|
||
/**
|
||
* @param {number[]} values
|
||
* @param {number} width
|
||
* @param {boolean} ascii
|
||
* @param {boolean} logScale
|
||
* @param {boolean} braille
|
||
*/
|
||
function bareTopSparklineOverview(values, width, ascii, logScale, braille) {
|
||
if (width <= 0) return ''
|
||
const slice =
|
||
values.length > width ? values.slice(values.length - width) : values.slice()
|
||
if (slice.length === 0) return ' '.repeat(width)
|
||
const vmap = logScale
|
||
? slice.map((v) => Math.log1p(Math.max(0, v)))
|
||
: slice.slice()
|
||
let min = vmap[0]
|
||
let max = vmap[0]
|
||
for (let i = 1; i < vmap.length; i++) {
|
||
if (vmap[i] < min) min = vmap[i]
|
||
if (vmap[i] > max) max = vmap[i]
|
||
}
|
||
const span = max - min || 1
|
||
const uni = '\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588'
|
||
const br = '\u2800\u2840\u28c0\u28e0\u28f0\u28f4\u28f6\u28ff'
|
||
const asc = ' .:-=+*#'
|
||
let out = ''
|
||
const pad = width - slice.length
|
||
for (let i = 0; i < pad; i++) out += ascii ? ' ' : '\u2581'
|
||
for (let i = 0; i < vmap.length; i++) {
|
||
const v = vmap[i]
|
||
const t = (v - min) / span
|
||
let idx = Math.min(7, Math.floor(t * 8))
|
||
if (idx < 0) idx = 0
|
||
if (ascii) out += asc.charAt(idx)
|
||
else if (braille) out += br.charAt(idx)
|
||
else out += uni.charAt(idx)
|
||
}
|
||
return out.slice(0, width)
|
||
}
|