Two-socket machines: Hardware and Top show each CPU, not one with every core

Hardware counts CPUs, cores and threads per CPU. Top groups threads under a row
per socket, reads each core's temperature from its own socket's coretemp chip,
and the CPU temperature is the hotter package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-11-10 00:04:47 -08:00
co-authored by Claude Opus 5.5
parent 4b238ea93e
commit 1e2dcacb2c
6 changed files with 133 additions and 27 deletions
+16
View File
@@ -0,0 +1,16 @@
package metrics
import "testing"
// a two-socket board: two coretemp chips, and the CPU's temperature is the hotter package
func TestCPUTempHottestPackage(t *testing.T) {
temps := []Temp{
{Chip: "coretemp", Label: "Core 0", C: 31},
{Chip: "coretemp", Label: "Core 0", C: 44, Pkg: 1},
{Chip: "coretemp", Label: "Package id 0", C: 35},
{Chip: "coretemp", Label: "Package id 1", C: 41, Pkg: 1},
}
if got := cpuTemp(temps); got != 41 {
t.Fatalf("cpuTemp = %v, want 41", got)
}
}
+63 -6
View File
@@ -60,6 +60,7 @@ type Temp struct {
C float64 `json:"c"` C float64 `json:"c"`
Max float64 `json:"max,omitempty"` Max float64 `json:"max,omitempty"`
Crit float64 `json:"crit,omitempty"` Crit float64 `json:"crit,omitempty"`
Pkg int `json:"pkg,omitempty"` // coretemp: the CPU package the chip belongs to — a two-socket board has two chips, both with a "Core 0"
} }
type Fan struct { type Fan struct {
@@ -544,6 +545,19 @@ func hwmonDir(d string) (chip string, temps []Temp, fans []Fan) {
fans = append(fans, Fan{Chip: chip, Label: label, RPM: int(raw)}) fans = append(fans, Fan{Chip: chip, Label: label, RPM: int(raw)})
} }
} }
// coretemp is one chip per CPU package, and only its "Package id N" says which
if chip == "coretemp" {
for _, t := range temps {
if n, ok := strings.CutPrefix(t.Label, "Package id "); ok {
if pkg, err := strconv.Atoi(n); err == nil {
for i := range temps {
temps[i].Pkg = pkg
}
}
break
}
}
}
return return
} }
@@ -559,7 +573,10 @@ func hwmon() ([]Temp, []Fan) {
if temps[i].Chip != temps[j].Chip { if temps[i].Chip != temps[j].Chip {
return temps[i].Chip < temps[j].Chip return temps[i].Chip < temps[j].Chip
} }
return temps[i].Label < temps[j].Label if temps[i].Label != temps[j].Label {
return temps[i].Label < temps[j].Label
}
return temps[i].Pkg < temps[j].Pkg
}) })
return temps, fans return temps, fans
} }
@@ -1112,6 +1129,38 @@ func coreIDs(n int) []int {
return coreIDsVal[:n] return coreIDsVal[:n]
} }
// corePkgs maps logical CPUs to the CPU package (socket) each is on, and is nil on a
// one-socket machine: there is nothing to tell apart.
var (
corePkgsOnce sync.Once
corePkgsVal []int
)
func corePkgs(n int) []int {
corePkgsOnce.Do(func() {
seen := map[int]bool{}
var out []int
for i := 0; ; i++ {
b, err := os.ReadFile("/sys/devices/system/cpu/cpu" + strconv.Itoa(i) + "/topology/physical_package_id")
if err != nil {
break
}
id, err := strconv.Atoi(strings.TrimSpace(string(b)))
if err != nil {
break
}
out, seen[id] = append(out, id), true
}
if len(seen) > 1 {
corePkgsVal = out
}
})
if len(corePkgsVal) < n {
return nil
}
return corePkgsVal[:n]
}
// ProcTable is the Processes tab payload. // ProcTable is the Processes tab payload.
type ProcTable struct { type ProcTable struct {
T int64 `json:"t"` T int64 `json:"t"`
@@ -1123,7 +1172,8 @@ type ProcTable struct {
CPUMaxMHz float64 `json:"cpu_max_mhz,omitempty"` CPUMaxMHz float64 `json:"cpu_max_mhz,omitempty"`
CPUPowerW float64 `json:"cpu_power_w,omitempty"` CPUPowerW float64 `json:"cpu_power_w,omitempty"`
CPUTemp float64 `json:"cpu_temp,omitempty"` CPUTemp float64 `json:"cpu_temp,omitempty"`
CoreIDs []int `json:"core_ids,omitempty"` // for each logical CPU, the physical core it is a thread of (what the temperature sensors are named by) CoreIDs []int `json:"core_ids,omitempty"` // for each logical CPU, the physical core it is a thread of (what the temperature sensors are named by)
CorePkgs []int `json:"core_pkgs,omitempty"` // for each logical CPU, the socket it is on; only on a machine with more than one
Load [3]float64 `json:"load"` Load [3]float64 `json:"load"`
Mem Mem `json:"mem"` Mem Mem `json:"mem"`
Swap Mem `json:"swap"` Swap Mem `json:"swap"`
@@ -1182,18 +1232,24 @@ func cpuMHz() float64 {
// cpuTemp picks the sensor that best represents the CPU package. // cpuTemp picks the sensor that best represents the CPU package.
func cpuTemp(temps []Temp) float64 { func cpuTemp(temps []Temp) float64 {
best, max := 0.0, 0.0 // the hottest package: a two-socket board has two, and the hotter one is the one to watch
pkg, max := 0.0, 0.0
for _, t := range temps { for _, t := range temps {
l := strings.ToLower(t.Label) l := strings.ToLower(t.Label)
if t.Chip == "coretemp" && strings.HasPrefix(l, "package") || t.Chip == "k10temp" && (l == "tctl" || l == "tdie") || if t.Chip == "coretemp" && strings.HasPrefix(l, "package") || t.Chip == "k10temp" && (l == "tctl" || l == "tdie") ||
t.Chip == "thermal" && strings.Contains(l, "cpu") || t.Chip == "cpu_thermal" || t.Chip == "soc_thermal" { t.Chip == "thermal" && strings.Contains(l, "cpu") || t.Chip == "cpu_thermal" || t.Chip == "soc_thermal" {
return t.C if t.C > pkg {
pkg = t.C
}
} }
if (t.Chip == "coretemp" || t.Chip == "k10temp") && t.C > max { if (t.Chip == "coretemp" || t.Chip == "k10temp") && t.C > max {
max, best = t.C, t.C max = t.C
} }
} }
return best if pkg > 0 {
return pkg
}
return max
} }
var bootTime = func() int64 { var bootTime = func() int64 {
@@ -1323,6 +1379,7 @@ func (c *Collector) Processes() ProcTable {
last := c.ring[n-1] last := c.ring[n-1]
t.CPU, t.Cores, t.Net, t.CPUTemp, t.CPUPowerW = last.CPU, last.Cores, last.Net, cpuTemp(last.Temps), last.CPUPower t.CPU, t.Cores, t.Net, t.CPUTemp, t.CPUPowerW = last.CPU, last.Cores, last.Net, cpuTemp(last.Temps), last.CPUPower
t.CoreIDs = coreIDs(len(last.Cores)) t.CoreIDs = coreIDs(len(last.Cores))
t.CorePkgs = corePkgs(len(last.Cores))
from := n - 90 from := n - 90
if from < 0 { if from < 0 {
from = 0 from = 0
+13 -4
View File
@@ -158,6 +158,10 @@ export default function Hardware() {
const capacity = drives.reduce((n, d) => n + d.size, 0) const capacity = drives.reduce((n, d) => n + d.size, 0)
const threads = cpu.threads ?? 0 const threads = cpu.threads ?? 0
const cores = cpu.cores ?? 0 const cores = cpu.cores ?? 0
// a two-socket board is two processors, not one with twice the cores: count and name each
const sockets = Math.max(1, cpu.sockets ?? 1)
const multi = sockets > 1
const per = (n: number) => Math.round(n / sockets)
return ( return (
<div className="dash"> <div className="dash">
@@ -171,8 +175,11 @@ export default function Hardware() {
<div className="kpis"> <div className="kpis">
<div className="tile"> <div className="tile">
<div className="label">Processor</div> <div className="label">Processor</div>
<div className="value" title={cpu.model} style={(cpu.model?.length ?? 0) > 20 ? { fontSize: 16 } : undefined}>{cpu.model}</div> <div className="value" title={cpu.model} style={(cpu.model?.length ?? 0) > (multi ? 16 : 20) ? { fontSize: 16 } : undefined}>{multi ? `${sockets} × ${cpu.model}` : cpu.model}</div>
<div className="sub">{[cores ? `${cores} core${cores === 1 ? '' : 's'}` : '', threads && threads !== cores ? `${threads} threads` : '', cpu.max_mhz ? `up to ${ghz(cpu.max_mhz)}` : ''].filter(Boolean).join(' · ')}</div> <div className="sub">{(multi
? [`${sockets} CPUs`, cores ? `${per(cores)} cores${threads && threads !== cores ? ` / ${per(threads)} threads` : ''} each` : '', cpu.max_mhz ? `up to ${ghz(cpu.max_mhz)}` : '']
: [cores ? `${cores} core${cores === 1 ? '' : 's'}` : '', threads && threads !== cores ? `${threads} threads` : '', cpu.max_mhz ? `up to ${ghz(cpu.max_mhz)}` : '']
).filter(Boolean).join(' · ')}</div>
</div> </div>
<div className="tile"> <div className="tile">
<div className="label">Memory</div> <div className="label">Memory</div>
@@ -199,9 +206,11 @@ export default function Hardware() {
<div className="props"> <div className="props">
<P k="Model" v={cpu.model} /> <P k="Model" v={cpu.model} />
<P k="Vendor" v={cpu.vendor} /> <P k="Vendor" v={cpu.vendor} />
<P k="Cores" v={cores ? `${cores}${cpu.threads_per_core && cpu.threads_per_core > 1 ? ` · ${cpu.threads_per_core} threads each` : ''}` : ''} /> {multi && <P k="CPUs" v={`${sockets}`} />}
<P k="Cores" v={cores ? `${multi ? `${per(cores)} per CPU · ${cores} in all` : cores}${cpu.threads_per_core && cpu.threads_per_core > 1 ? ` · ${cpu.threads_per_core} threads each` : ''}` : ''} />
{multi && threads > 0 && <P k="Threads" v={`${per(threads)} per CPU · ${threads} in all`} />}
<P k="Clock" v={cpu.min_mhz && cpu.max_mhz ? `${ghz(cpu.min_mhz)} – ${ghz(cpu.max_mhz)}` : ghz(cpu.max_mhz)} /> <P k="Clock" v={cpu.min_mhz && cpu.max_mhz ? `${ghz(cpu.min_mhz)} – ${ghz(cpu.max_mhz)}` : ghz(cpu.max_mhz)} />
<P k="Socket" v={(cpu.socket_names ?? []).join(', ')} /> <P k={multi ? 'Sockets' : 'Socket'} v={(cpu.socket_names ?? []).join(', ')} />
<P k="Arch" v={cpu.arch} /> <P k="Arch" v={cpu.arch} />
<P k="Family" v={[cpu.family && `family ${cpu.family}`, cpu.model_id && `model ${cpu.model_id}`, cpu.stepping && `stepping ${cpu.stepping}`].filter(Boolean).join(' · ')} /> <P k="Family" v={[cpu.family && `family ${cpu.family}`, cpu.model_id && `model ${cpu.model_id}`, cpu.stepping && `stepping ${cpu.stepping}`].filter(Boolean).join(' · ')} />
<P k="Microcode" v={cpu.microcode} mono /> <P k="Microcode" v={cpu.microcode} mono />
+37 -15
View File
@@ -3,7 +3,7 @@
// as they climb. The numbers are Ham Lab's own — the sensor stream for everything measured and // as they climb. The numbers are Ham Lab's own — the sensor stream for everything measured and
// /api/procs for the table — so this works on any machine the console can see, whether or not // /api/procs for the table — so this works on any machine the console can see, whether or not
// btop is installed on it. // btop is installed on it.
import { createContext, memo, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import { Fragment, createContext, memo, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { api, GPU, ProcTable, Proc, Sample } from './api' import { api, GPU, ProcTable, Proc, Sample } from './api'
import { Catalog, MONITOR_SHAPE, Values, mergeValues, sampleOf, stale } from './sampleAdapter' import { Catalog, MONITOR_SHAPE, Values, mergeValues, sampleOf, stale } from './sampleAdapter'
import { fetchCatalog, fetchHistories, useSensorStream } from './sensors' import { fetchCatalog, fetchHistories, useSensorStream } from './sensors'
@@ -360,7 +360,13 @@ export default function Top() {
const model = (pt?.cpu_model ?? '').replace(/\(R\)|\(TM\)|CPU|Processor/g, '').replace(/@.*$/, '').replace(/^\d+th Gen\s*/i, '').replace(/\s+/g, ' ').trim() const model = (pt?.cpu_model ?? '').replace(/\(R\)|\(TM\)|CPU|Processor/g, '').replace(/@.*$/, '').replace(/^\d+th Gen\s*/i, '').replace(/\s+/g, ' ').trim()
// the hottest thing that looks like a CPU sensor, and one per core where the chip names them; // the hottest thing that looks like a CPU sensor, and one per core where the chip names them;
// a second thread of a core (CPU 8 on an 8-core chip with hyper-threading) wears its core's // a second thread of a core (CPU 8 on an 8-core chip with hyper-threading) wears its core's
const coreTemp = (i: number) => { const id = pt?.core_ids?.[i] ?? i; return s?.temps.find((t) => new RegExp(`core ${id}\\b`, 'i').test(t.label))?.c } // On a two-socket board each CPU has its own sensor chip, both counting from "Core 0", so a
// thread's core is looked up on its own socket's chip.
const pkgs = pt?.core_pkgs?.length === cores.length ? pt.core_pkgs : undefined
const sockets = pkgs ? [...new Set(pkgs)].sort((a, b) => a - b) : []
const coreTemp = (i: number) => { const id = pt?.core_ids?.[i] ?? i, pk = pkgs?.[i]; return s?.temps.find((t) => new RegExp(`core ${id}\\b`, 'i').test(t.label) && (pk === undefined || (t.pkg ?? 0) === pk))?.c }
const sockTemp = (pk: number) => s?.temps.find((t) => t.chip === 'coretemp' && new RegExp(`^package id ${pk}$`, 'i').test(t.label))?.c
const sockUse = (pk: number) => { const v = cores.filter((_, i) => pkgs?.[i] === pk); return v.length ? v.reduce((a, b) => a + b, 0) / v.length : 0 }
const cpuTemp = pt?.cpu_temp ?? s?.temps.find((t) => /package|tctl|tdie|cpu/i.test(t.label))?.c ?? coreTemp(0) const cpuTemp = pt?.cpu_temp ?? s?.temps.find((t) => /package|tctl|tdie|cpu/i.test(t.label))?.c ?? coreTemp(0)
// the temperature columns are there for every row once any row has one, so the % column lines up // the temperature columns are there for every row once any row has one, so the % column lines up
const anyTemp = cpuTemp !== undefined || cores.some((_, i) => coreTemp(i) !== undefined) const anyTemp = cpuTemp !== undefined || cores.some((_, i) => coreTemp(i) !== undefined)
@@ -403,8 +409,8 @@ export default function Top() {
<Graph values={curves.get('cpu.usage') ?? []} tick={tick} advance={advance} now={now} cap={cap} max={100} grad="cpu" /> <Graph values={curves.get('cpu.usage') ?? []} tick={tick} advance={advance} now={now} cap={cap} max={100} grad="cpu" />
<div className="t-up">up {upfor(uptime)}</div> <div className="t-up">up {upfor(uptime)}</div>
</div> </div>
<div className="t-cores"> <div className={'t-cores' + (sockets.length > 1 ? ' multi' : '')}>
<div className="t-cores-head"><b>{model || 'cpu'}</b><span className="spacer" />{pt?.cpu_mhz ? `${Math.round(pt.cpu_mhz)} MHz` : ''} <div className="t-cores-head"><b>{sockets.length > 1 ? `${sockets.length} × ` : ''}{model || 'cpu'}</b><span className="spacer" />{pt?.cpu_mhz ? `${Math.round(pt.cpu_mhz)} MHz` : ''}
{pt?.cpu_power_w ? <span title="the CPU's own power reading: cores, built-in graphics and memory controller, not the whole machine">{pt.cpu_mhz ? ' · ' : ''}{pt.cpu_power_w < 10 ? pt.cpu_power_w.toFixed(1) : Math.round(pt.cpu_power_w)} W</span> : null}</div> {pt?.cpu_power_w ? <span title="the CPU's own power reading: cores, built-in graphics and memory controller, not the whole machine">{pt.cpu_mhz ? ' · ' : ''}{pt.cpu_power_w < 10 ? pt.cpu_power_w.toFixed(1) : Math.round(pt.cpu_power_w)} W</span> : null}</div>
<div className="t-core"> <div className="t-core">
<span className="t-core-n">CPU</span> <span className="t-core-n">CPU</span>
@@ -412,17 +418,33 @@ export default function Top() {
<span className="t-core-p">{Math.round(cpu)}%</span> <span className="t-core-p">{Math.round(cpu)}%</span>
{anyTemp && <><span className="t-core-t">{cpuTemp !== undefined && <Meter pct={Math.min(100, (cpuTemp / 100) * 100)} grad="temp" />}</span><span className="t-core-c">{cpuTemp !== undefined ? `${Math.round(cpuTemp)}°C` : ''}</span></>} {anyTemp && <><span className="t-core-t">{cpuTemp !== undefined && <Meter pct={Math.min(100, (cpuTemp / 100) * 100)} grad="temp" />}</span><span className="t-core-c">{cpuTemp !== undefined ? `${Math.round(cpuTemp)}°C` : ''}</span></>}
</div> </div>
{cores.map((v, i) => { {(sockets.length > 1 ? sockets : [undefined]).map((pk) => (
const t = coreTemp(i) <Fragment key={pk ?? 'all'}>
return ( {pk !== undefined && (() => {
<div className="t-core" key={i}> const u = sockUse(pk), t = sockTemp(pk)
<span className="t-core-n">C{i}</span> return (
<span className="t-core-g" title={pt?.core_ids && pt.core_ids[i] !== i ? `cpu ${i} · a thread of core ${pt.core_ids[i]}` : `core ${i}`}>{braille(recent.get(`cpu.core.${i}.usage`) ?? [], 9)}</span> <div className="t-core t-sock">
<span className="t-core-p">{Math.round(v)}%</span> <span className="t-core-n">CPU{pk + 1}</span>
{anyTemp && <><span className="t-core-t">{t !== undefined && <Meter pct={Math.min(100, t)} grad="temp" />}</span><span className="t-core-c">{t !== undefined ? `${Math.round(t)}°C` : ''}</span></>} <Meter pct={u} grad="cpu" />
</div> <span className="t-core-p">{Math.round(u)}%</span>
) {anyTemp && <><span className="t-core-t">{t !== undefined && <Meter pct={Math.min(100, t)} grad="temp" />}</span><span className="t-core-c">{t !== undefined ? `${Math.round(t)}°C` : ''}</span></>}
})} </div>
)
})()}
{cores.map((v, i) => {
if (pk !== undefined && pkgs?.[i] !== pk) return null
const t = coreTemp(i)
return (
<div className="t-core" key={i}>
<span className="t-core-n">C{i}</span>
<span className="t-core-g" title={(pk !== undefined ? `CPU ${pk + 1} · ` : '') + (pt?.core_ids && pt.core_ids[i] !== i ? `cpu ${i} · a thread of core ${pt.core_ids[i]}` : `core ${i}`)}>{braille(recent.get(`cpu.core.${i}.usage`) ?? [], 9)}</span>
<span className="t-core-p">{Math.round(v)}%</span>
{anyTemp && <><span className="t-core-t">{t !== undefined && <Meter pct={Math.min(100, t)} grad="temp" />}</span><span className="t-core-c">{t !== undefined ? `${Math.round(t)}°C` : ''}</span></>}
</div>
)
})}
</Fragment>
))}
<div className="t-load">Load AVG:<span className="spacer" />{load.map((x) => x.toFixed(2)).join(' ')}</div> <div className="t-load">Load AVG:<span className="spacer" />{load.map((x) => x.toFixed(2)).join(' ')}</div>
</div> </div>
</div> </div>
+2 -2
View File
@@ -71,7 +71,7 @@ export const titleOf = (name: string | null, machines: MachineStatus[], hubHost:
export interface MachineStatus { name: string; url: string; fingerprint?: string; added: string; online: boolean; host?: string; version?: string; load?: string; uptime?: number; reboot_needed?: boolean; last_seen?: string; error?: string } export interface MachineStatus { name: string; url: string; fingerprint?: string; added: string; online: boolean; host?: string; version?: string; load?: string; uptime?: number; reboot_needed?: boolean; last_seen?: string; error?: string }
export interface Term { id: string; name: string; running: boolean; exit_code: number; created: string; clients: number } export interface Term { id: string; name: string; running: boolean; exit_code: number; created: string; clients: number }
export interface Temp { chip: string; label: string; c: number; max?: number; crit?: number } export interface Temp { chip: string; label: string; c: number; max?: number; crit?: number; pkg?: number }
export interface Fan { chip: string; label: string; rpm: number } export interface Fan { chip: string; label: string; rpm: number }
export interface NetIf { iface: string; up: boolean; rx_bps: number; tx_bps: number; rx_total: number; tx_total: number } export interface NetIf { iface: string; up: boolean; rx_bps: number; tx_bps: number; rx_total: number; tx_total: number }
export interface Disk { name: string; model?: string; size: number; read_bps: number; write_bps: number; util_pct: number; temp?: number } export interface Disk { name: string; model?: string; size: number; read_bps: number; write_bps: number; util_pct: number; temp?: number }
@@ -90,7 +90,7 @@ export interface GPU {
mhz?: number; req_mhz?: number; max_mhz?: number; mem_used?: number; mem_total?: number mhz?: number; req_mhz?: number; max_mhz?: number; mem_used?: number; mem_total?: number
clients: GPUClient[]; clients_ok: boolean; stalled?: boolean clients: GPUClient[]; clients_ok: boolean; stalled?: boolean
} }
export interface ProcTable { t: number; cpu: number; cores: number[]; cpu_hist: number[]; cpu_model?: string; cpu_mhz?: number; cpu_max_mhz?: number; cpu_power_w?: number; cpu_temp?: number; core_ids?: number[]; load: [number, number, number]; mem: MemInfo; swap: MemInfo; net: NetIf[]; gpus?: GPU[]; uptime: number; procs: Proc[]; total: number; running: number } export interface ProcTable { t: number; cpu: number; cores: number[]; cpu_hist: number[]; cpu_model?: string; cpu_mhz?: number; cpu_max_mhz?: number; cpu_power_w?: number; cpu_temp?: number; core_ids?: number[]; core_pkgs?: number[]; load: [number, number, number]; mem: MemInfo; swap: MemInfo; net: NetIf[]; gpus?: GPU[]; uptime: number; procs: Proc[]; total: number; running: number }
export interface Sample { export interface Sample {
t: number; interval_ms: number; cpu: number; cores: number[]; load: [number, number, number] t: number; interval_ms: number; cpu: number; cores: number[]; load: [number, number, number]
mem: { total: number; used: number; avail: number }; swap: { total: number; used: number; avail: number } mem: { total: number; used: number; avail: number }; swap: { total: number; used: number; avail: number }
+2
View File
@@ -794,6 +794,8 @@ pre.diff .add { color: var(--ok); background: color-mix(in srgb, var(--ok) 12%,
.t-cores-head b { color: var(--fg); font-weight: 400; overflow: hidden; text-overflow: ellipsis; } .t-cores-head b { color: var(--fg); font-weight: 400; overflow: hidden; text-overflow: ellipsis; }
.t-core { display: flex; align-items: center; gap: 6px; } .t-core { display: flex; align-items: center; gap: 6px; }
.t-core-n { width: 30px; } .t-core-p { width: 34px; text-align: right; } .t-core-n { width: 30px; } .t-core-p { width: 34px; text-align: right; }
.t-cores.multi .t-core-n { width: 38px; } /* "CPU2" — a two-socket board has a row per socket above its threads */
.t-sock { color: var(--fg); margin-top: 2px; }
.t-core-g { flex: 1; color: var(--ok); letter-spacing: 0; overflow: hidden; white-space: nowrap; } .t-core-g { flex: 1; color: var(--ok); letter-spacing: 0; overflow: hidden; white-space: nowrap; }
.t-core .tm { flex: 1; } .t-core-t { flex: 0 0 62px; display: flex; } .t-core-c { width: 42px; text-align: right; color: var(--t-blue); } .t-core .tm { flex: 1; } .t-core-t { flex: 0 0 62px; display: flex; } .t-core-c { width: 42px; text-align: right; color: var(--t-blue); }
.t-load { display: flex; margin-top: 2px; } .t-load { display: flex; margin-top: 2px; }