WiFi 표시값과 잔여 시간 설명 강화
This commit is contained in:
+185
-7
@@ -1982,6 +1982,56 @@ function dnsmasq_leases(): array
|
||||
return $map;
|
||||
}
|
||||
|
||||
function wifi_client_aliases(): array
|
||||
{
|
||||
try {
|
||||
$rows = db()->query("SELECT mac, hostname FROM wifi_client_aliases")->fetchAll();
|
||||
} catch (Throwable) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$aliases = [];
|
||||
foreach ($rows as $row) {
|
||||
$mac = strtolower((string)($row['mac'] ?? ''));
|
||||
$hostname = trim((string)($row['hostname'] ?? ''));
|
||||
if (wifi_valid_mac($mac) && $hostname !== '') {
|
||||
$aliases[$mac] = $hostname;
|
||||
}
|
||||
}
|
||||
|
||||
return $aliases;
|
||||
}
|
||||
|
||||
function save_wifi_client_alias(string $mac, string $hostname): array
|
||||
{
|
||||
$mac = strtolower(trim($mac));
|
||||
$hostname = trim(preg_replace('/\s+/', ' ', $hostname) ?? '');
|
||||
|
||||
if (!wifi_valid_mac($mac)) {
|
||||
throw new InvalidArgumentException('bad_mac');
|
||||
}
|
||||
if ($hostname === '' || mb_strlen($hostname) > 80) {
|
||||
throw new InvalidArgumentException('bad_hostname');
|
||||
}
|
||||
|
||||
$stmt = db()->prepare("
|
||||
INSERT INTO wifi_client_aliases (mac, hostname)
|
||||
VALUES (:mac, :hostname)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
hostname = VALUES(hostname),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
");
|
||||
$stmt->execute([
|
||||
':mac' => $mac,
|
||||
':hostname' => $hostname,
|
||||
]);
|
||||
|
||||
return [
|
||||
'mac' => $mac,
|
||||
'hostname' => $hostname,
|
||||
];
|
||||
}
|
||||
|
||||
function iw_station_dump(string $iface): string
|
||||
{
|
||||
if (!preg_match('/^[A-Za-z0-9_.:-]+$/', $iface)) {
|
||||
@@ -1991,7 +2041,7 @@ function iw_station_dump(string $iface): string
|
||||
return sh(['/usr/sbin/iw', 'dev', $iface, 'station', 'dump'], true, 4)['out'];
|
||||
}
|
||||
|
||||
function parse_live_wifi_rows(string $iface, string $band, string $text, array $leases): array
|
||||
function parse_live_wifi_rows(string $iface, string $band, string $text, array $leases, array $aliases): array
|
||||
{
|
||||
$rows = [];
|
||||
$cur = null;
|
||||
@@ -2006,16 +2056,24 @@ function parse_live_wifi_rows(string $iface, string $band, string $text, array $
|
||||
|
||||
$mac = strtolower($m[1]);
|
||||
$lease = $leases[$mac] ?? [];
|
||||
$leaseHostname = (string)($lease['hostname'] ?? 'N/A');
|
||||
$aliasHostname = $aliases[$mac] ?? null;
|
||||
$hostname = $aliasHostname ?: $leaseHostname;
|
||||
$hostnameSource = $aliasHostname ? 'alias' : ($leaseHostname === 'N/A' ? 'generated' : 'lease');
|
||||
$cur = [
|
||||
'band' => $band,
|
||||
'iface' => $iface,
|
||||
'mac' => $mac,
|
||||
'ip' => $lease['ip'] ?? 'N/A',
|
||||
'hostname' => $lease['hostname'] ?? 'N/A',
|
||||
'name' => $lease['hostname'] ?? $mac,
|
||||
'hostname' => $hostname === 'N/A' ? '기기-' . strtoupper(substr(str_replace(':', '', $mac), -6)) : $hostname,
|
||||
'hostname_source' => $hostnameSource,
|
||||
'name' => $hostname === 'N/A' ? $mac : $hostname,
|
||||
'signal' => 'N/A',
|
||||
'tx_bitrate' => 'N/A',
|
||||
'rx_bitrate' => 'N/A',
|
||||
'signal_source' => 'missing',
|
||||
'tx_bitrate_source' => 'missing',
|
||||
'rx_bitrate_source' => 'missing',
|
||||
'connected_time' => 'N/A',
|
||||
'inactive_time' => 'N/A',
|
||||
'rx_bytes' => 0,
|
||||
@@ -2031,9 +2089,16 @@ function parse_live_wifi_rows(string $iface, string $band, string $text, array $
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preg_match('/^signal:\s+(.+)/', $t, $m)) $cur['signal'] = $m[1];
|
||||
elseif (preg_match('/^tx bitrate:\s+(.+)/', $t, $m)) $cur['tx_bitrate'] = $m[1];
|
||||
elseif (preg_match('/^rx bitrate:\s+(.+)/', $t, $m)) $cur['rx_bitrate'] = $m[1];
|
||||
if (preg_match('/^signal:\s+(.+)/', $t, $m)) {
|
||||
$cur['signal'] = $m[1];
|
||||
$cur['signal_source'] = 'iw';
|
||||
} elseif (preg_match('/^tx bitrate:\s+(.+)/', $t, $m)) {
|
||||
$cur['tx_bitrate'] = $m[1];
|
||||
$cur['tx_bitrate_source'] = 'iw';
|
||||
} elseif (preg_match('/^rx bitrate:\s+(.+)/', $t, $m)) {
|
||||
$cur['rx_bitrate'] = $m[1];
|
||||
$cur['rx_bitrate_source'] = 'iw';
|
||||
}
|
||||
elseif (preg_match('/^connected time:\s+(.+)/', $t, $m)) $cur['connected_time'] = $m[1];
|
||||
elseif (preg_match('/^inactive time:\s+(.+)/', $t, $m)) $cur['inactive_time'] = $m[1];
|
||||
elseif (preg_match('/^rx bytes:\s+(\d+)/', $t, $m)) $cur['rx_bytes'] = (int)$m[1];
|
||||
@@ -2059,6 +2124,21 @@ function wifi_time_ms(string $value): int
|
||||
return PHP_INT_MAX;
|
||||
}
|
||||
|
||||
function wifi_unknown(mixed $value): bool
|
||||
{
|
||||
$text = strtoupper(trim((string)$value));
|
||||
return $text === '' || $text === 'N/A' || $text === '-';
|
||||
}
|
||||
|
||||
function wifi_bitrate_mbps(mixed $value): ?float
|
||||
{
|
||||
if (preg_match('/([0-9]+(?:\.[0-9]+)?)/', (string)$value, $m)) {
|
||||
return (float)$m[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function wifi_signal_dbm(string $value): int
|
||||
{
|
||||
if (preg_match('/-?\d+/', $value, $m)) {
|
||||
@@ -2068,6 +2148,87 @@ function wifi_signal_dbm(string $value): int
|
||||
return -999;
|
||||
}
|
||||
|
||||
function estimate_wifi_rate_from_signal(string $band, int $dbm): float
|
||||
{
|
||||
if ($band === '5G') {
|
||||
if ($dbm >= -50) return 1300.0;
|
||||
if ($dbm >= -58) return 866.7;
|
||||
if ($dbm >= -66) return 585.0;
|
||||
if ($dbm >= -73) return 390.0;
|
||||
if ($dbm >= -80) return 173.3;
|
||||
return 54.0;
|
||||
}
|
||||
|
||||
if ($dbm >= -50) return 300.0;
|
||||
if ($dbm >= -58) return 144.4;
|
||||
if ($dbm >= -66) return 72.2;
|
||||
if ($dbm >= -73) return 39.0;
|
||||
if ($dbm >= -80) return 19.5;
|
||||
return 6.5;
|
||||
}
|
||||
|
||||
function estimate_wifi_signal_from_rate(string $band, float $mbps): int
|
||||
{
|
||||
if ($band === '5G') {
|
||||
if ($mbps >= 900) return -48;
|
||||
if ($mbps >= 600) return -55;
|
||||
if ($mbps >= 350) return -63;
|
||||
if ($mbps >= 150) return -71;
|
||||
if ($mbps >= 54) return -79;
|
||||
return -84;
|
||||
}
|
||||
|
||||
if ($mbps >= 250) return -48;
|
||||
if ($mbps >= 120) return -56;
|
||||
if ($mbps >= 65) return -64;
|
||||
if ($mbps >= 30) return -72;
|
||||
if ($mbps >= 10) return -80;
|
||||
return -86;
|
||||
}
|
||||
|
||||
function format_estimated_mbps(float $mbps): string
|
||||
{
|
||||
$value = abs($mbps - round($mbps)) < 0.05
|
||||
? (string)(int)round($mbps)
|
||||
: number_format($mbps, 1);
|
||||
|
||||
return $value . ' MBit/s (추정)';
|
||||
}
|
||||
|
||||
function apply_wifi_estimates(array $clients): array
|
||||
{
|
||||
foreach ($clients as &$client) {
|
||||
$band = (string)($client['band'] ?? '2.4G');
|
||||
$signalDbm = wifi_signal_dbm((string)($client['signal'] ?? ''));
|
||||
$txMbps = wifi_bitrate_mbps($client['tx_bitrate'] ?? null);
|
||||
$rxMbps = wifi_bitrate_mbps($client['rx_bitrate'] ?? null);
|
||||
|
||||
if (wifi_unknown($client['signal'] ?? null)) {
|
||||
$sourceRate = max($txMbps ?? 0.0, $rxMbps ?? 0.0);
|
||||
$signalDbm = $sourceRate > 0
|
||||
? estimate_wifi_signal_from_rate($band, $sourceRate)
|
||||
: ($band === '5G' ? -67 : -70);
|
||||
$client['signal'] = $signalDbm . ' dBm (추정)';
|
||||
$client['signal_source'] = $sourceRate > 0 ? 'estimated_from_rate' : 'estimated_default';
|
||||
}
|
||||
|
||||
if (wifi_unknown($client['tx_bitrate'] ?? null)) {
|
||||
$base = $rxMbps ?? estimate_wifi_rate_from_signal($band, $signalDbm);
|
||||
$client['tx_bitrate'] = format_estimated_mbps($base);
|
||||
$client['tx_bitrate_source'] = $rxMbps === null ? 'estimated_from_signal' : 'estimated_from_rx';
|
||||
}
|
||||
|
||||
if (wifi_unknown($client['rx_bitrate'] ?? null)) {
|
||||
$base = $txMbps ?? estimate_wifi_rate_from_signal($band, $signalDbm);
|
||||
$client['rx_bitrate'] = format_estimated_mbps($base);
|
||||
$client['rx_bitrate_source'] = $txMbps === null ? 'estimated_from_signal' : 'estimated_from_tx';
|
||||
}
|
||||
}
|
||||
unset($client);
|
||||
|
||||
return $clients;
|
||||
}
|
||||
|
||||
function dedupe_wifi_clients(array $clients): array
|
||||
{
|
||||
$deduped = [];
|
||||
@@ -2246,16 +2407,18 @@ function apply_observed_wifi_connected_time(array $clients): array
|
||||
function wifi_data(): array
|
||||
{
|
||||
$leases = dnsmasq_leases();
|
||||
$aliases = wifi_client_aliases();
|
||||
$clients = [];
|
||||
|
||||
foreach (['wlan0' => '2.4G', 'wlan1' => '5G'] as $iface => $band) {
|
||||
$clients = array_merge(
|
||||
$clients,
|
||||
parse_live_wifi_rows($iface, $band, iw_station_dump($iface), $leases)
|
||||
parse_live_wifi_rows($iface, $band, iw_station_dump($iface), $leases, $aliases)
|
||||
);
|
||||
}
|
||||
|
||||
$clients = dedupe_wifi_clients($clients);
|
||||
$clients = apply_wifi_estimates($clients);
|
||||
$clients = apply_observed_wifi_connected_time($clients);
|
||||
|
||||
return [
|
||||
@@ -2723,6 +2886,21 @@ function control_api_dispatch(): void
|
||||
]);
|
||||
}
|
||||
|
||||
if ($action === 'wifi_alias') {
|
||||
$alias = save_wifi_client_alias(
|
||||
(string)($_POST['mac'] ?? ''),
|
||||
(string)($_POST['hostname'] ?? '')
|
||||
);
|
||||
|
||||
json_out([
|
||||
'ok' => true,
|
||||
'data' => [
|
||||
'alias' => $alias,
|
||||
'status' => collect_snapshot(false),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
if ($action === 'collect') {
|
||||
json_out([
|
||||
'ok' => true,
|
||||
|
||||
+81
-4
@@ -156,6 +156,9 @@
|
||||
serviceEnabled: 'enabled',
|
||||
servicePid: 'pid',
|
||||
serviceRestarts: 'restarts',
|
||||
wifiAliasTitle: 'WiFi Hostname',
|
||||
wifiAliasPrompt: 'Enter a name to keep for this MAC address.',
|
||||
wifiAliasSaved: 'WiFi hostname saved.',
|
||||
notifySuccess: 'success',
|
||||
notifyFailed: 'failed',
|
||||
show: 'Show',
|
||||
@@ -287,6 +290,9 @@
|
||||
serviceEnabled: '활성화',
|
||||
servicePid: 'PID',
|
||||
serviceRestarts: '재시작',
|
||||
wifiAliasTitle: 'WiFi 호스트명',
|
||||
wifiAliasPrompt: '이 MAC 주소에 계속 표시할 이름을 입력하세요.',
|
||||
wifiAliasSaved: 'WiFi 호스트명을 저장했습니다.',
|
||||
notifySuccess: '성공',
|
||||
notifyFailed: '실패',
|
||||
show: '보기',
|
||||
@@ -691,6 +697,9 @@
|
||||
};
|
||||
const lines = [sourceMap[remaining.source] || remaining.source || '-'];
|
||||
|
||||
if (remaining.seconds !== undefined && remaining.seconds !== null) {
|
||||
lines.push(`계산 잔여: ${formatDurationSeconds(remaining.seconds)}`);
|
||||
}
|
||||
if (remaining.drop_per_hour !== undefined && remaining.drop_per_hour !== null) {
|
||||
lines.push(`시간당 SOC 감소: ${Number(remaining.drop_per_hour).toFixed(3)}%`);
|
||||
}
|
||||
@@ -716,6 +725,16 @@
|
||||
}
|
||||
return `${row.minutes}분`;
|
||||
}).join(', ')}`);
|
||||
lines.push('');
|
||||
lines.push('세부 후보');
|
||||
remaining.windows.forEach(row => {
|
||||
if (row.source === 'learned_profile') {
|
||||
lines.push(`- 장기 학습: ${row.intervals || 0}구간 / SOC대 ${row.soc_buckets || 0}개 / 누적감소 ${Number(row.drop_percent || 0).toFixed(3)}% / 신뢰도 ${(Number(row.confidence || 0) * 100).toFixed(0)}%`);
|
||||
} else {
|
||||
const load = row.load_factor === null || row.load_factor === undefined ? '-' : `${Number(row.load_factor).toFixed(3)}x`;
|
||||
lines.push(`- 최근 ${row.minutes}분: 감소 ${Number(row.drop_percent || 0).toFixed(3)}% / 신뢰도 ${(Number(row.confidence || 0) * 100).toFixed(0)}% / 부하보정 ${load}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
@@ -802,6 +821,59 @@
|
||||
return `<td>${escapeHtml(v)}</td>`;
|
||||
}
|
||||
|
||||
function attr(v) {
|
||||
return escapeHtml(v).replaceAll("'", ''');
|
||||
}
|
||||
|
||||
function wifiValueTitle(row, key) {
|
||||
const source = row?.[`${key}_source`];
|
||||
if (!source || source === 'iw' || source === 'lease') return '';
|
||||
const labels = {
|
||||
alias: 'DB 저장 이름',
|
||||
generated: 'MAC 기반 임시 이름',
|
||||
estimated_from_rate: '속도 기반 추정',
|
||||
estimated_from_signal: '신호 기반 추정',
|
||||
estimated_from_rx: '수신 속도 기반 추정',
|
||||
estimated_from_tx: '송신 속도 기반 추정',
|
||||
estimated_default: '대역 기본 추정',
|
||||
};
|
||||
return labels[source] || source;
|
||||
}
|
||||
|
||||
function wifiHostCell(row) {
|
||||
const title = wifiValueTitle(row, 'hostname') || t('wifiAliasPrompt');
|
||||
return `<td><button type="button" class="wifi-host-edit" data-mac="${attr(row.mac || '')}" data-hostname="${attr(row.hostname || '')}" title="${attr(title)}">${escapeHtml(row.hostname || '-')}</button></td>`;
|
||||
}
|
||||
|
||||
function wifiMetricCell(row, key) {
|
||||
const title = wifiValueTitle(row, key);
|
||||
const className = title ? ' class="wifi-estimated"' : '';
|
||||
const titleAttr = title ? ` title="${attr(title)}"` : '';
|
||||
return `<td${className}${titleAttr}>${escapeHtml(row[key] || '-')}</td>`;
|
||||
}
|
||||
|
||||
async function editWifiHostname(mac, currentName) {
|
||||
if (!mac) return;
|
||||
const value = await window.customPrompt(`${t('wifiAliasPrompt')}\n${mac}`, {
|
||||
title: t('wifiAliasTitle'),
|
||||
defaultValue: currentName || '',
|
||||
okText: t('dialogOk'),
|
||||
});
|
||||
if (value === null) return;
|
||||
|
||||
const hostname = String(value || '').trim();
|
||||
if (!hostname) return;
|
||||
|
||||
try {
|
||||
const data = await api('wifi_alias', { mac, hostname });
|
||||
notice(t('wifiAliasSaved'), 'success');
|
||||
render(data.status || await api('status'));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
notice(e.message || 'WiFi alias failed', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function parseWifiDurationSeconds(value) {
|
||||
const text = String(value ?? '').trim();
|
||||
if (text === '' || text.toUpperCase() === 'N/A') {
|
||||
@@ -930,12 +1002,12 @@
|
||||
? rows.map(row => `
|
||||
<tr>
|
||||
${td(row.band)}
|
||||
${td(row.hostname)}
|
||||
${wifiHostCell(row)}
|
||||
${td(row.ip)}
|
||||
${td(row.mac)}
|
||||
${td(row.signal)}
|
||||
${td(row.tx_bitrate)}
|
||||
${td(row.rx_bitrate)}
|
||||
${wifiMetricCell(row, 'signal')}
|
||||
${wifiMetricCell(row, 'tx_bitrate')}
|
||||
${wifiMetricCell(row, 'rx_bitrate')}
|
||||
${td(wifiConnectedTime(row.connected_time))}
|
||||
${td(row.inactive_time)}
|
||||
</tr>
|
||||
@@ -1744,6 +1816,11 @@
|
||||
els.dmesgToggle?.addEventListener('click', () => {
|
||||
setDmesgOpen(!state.dmesgOpen);
|
||||
});
|
||||
els.wifiTable?.addEventListener('click', event => {
|
||||
const button = event.target?.closest?.('.wifi-host-edit');
|
||||
if (!button) return;
|
||||
editWifiHostname(button.dataset.mac || '', button.dataset.hostname || '');
|
||||
});
|
||||
els.statusBatteryRemaining?.addEventListener('mouseenter', showBatteryTooltip);
|
||||
els.statusBatteryRemaining?.addEventListener('mouseleave', hideBatteryTooltip);
|
||||
els.statusBatteryRemaining?.addEventListener('focus', showBatteryTooltip);
|
||||
|
||||
@@ -104,6 +104,7 @@ html[data-theme="light"] .btn.secondary{background:#d9e2ef;color:#172033}html[da
|
||||
.btn:disabled{opacity:.55;cursor:not-allowed}
|
||||
.status-list{display:grid;gap:10px}.status-row{display:grid;grid-template-columns:112px minmax(0,1fr);gap:12px;align-items:baseline;padding:10px 12px;background:var(--card2);border-radius:14px}.status-key{color:var(--sub);font-size:13px}.status-value{overflow:visible;white-space:normal;word-break:break-word;font-variant-numeric:tabular-nums}
|
||||
.table-wrap{max-width:100%;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;border-radius:16px;border:1px solid var(--line)}.wifi-table{width:100%;min-width:1080px;table-layout:fixed;border-collapse:collapse}.wifi-table th,.wifi-table td{padding:11px 13px;border-bottom:1px solid rgba(128,145,170,.18);text-align:left;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.wifi-table th{position:sticky;top:0;background:var(--table-head);z-index:1;font-weight:500}.wifi-table tr:hover{background:rgba(128,145,170,.09)}.wifi-table .col-band{width:72px}.wifi-table .col-host{width:240px}.wifi-table .col-ip{width:132px}.wifi-table .col-mac{width:170px}.wifi-table .col-signal{width:100px}.wifi-table .col-rate{width:120px}.wifi-table .col-time{width:130px}
|
||||
.wifi-host-edit{appearance:none;border:0;background:transparent;color:inherit;font:inherit;max-width:100%;padding:0;margin:0;cursor:pointer;text-align:left;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-decoration:underline;text-decoration-style:dotted;text-underline-offset:3px}.wifi-host-edit:hover{color:var(--blue)}.wifi-estimated{color:var(--yellow)}
|
||||
.chart-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px}.chart-box{height:280px;background:var(--input);border:1px solid rgba(84,101,128,.58);border-radius:16px;padding:12px 12px 18px}.chart-box h3{margin:0 0 8px;color:var(--sub);font-size:13px}.chart-canvas{position:relative;height:calc(100% - 27px);min-height:0}.chart-canvas canvas{width:100%!important;height:100%!important}.chart-secondary{margin-top:14px}
|
||||
.details-panel{border:1px solid rgba(84,101,128,.52);border-radius:16px;background:rgba(128,145,170,.06);padding:0;margin-top:14px;overflow:hidden}.details-panel>summary{cursor:pointer;list-style:none;display:flex;align-items:center;justify-content:space-between;gap:12px;min-height:48px;padding:0 14px;color:var(--text);font-weight:500}.details-panel>summary::-webkit-details-marker{display:none}.details-panel>summary::after{content:"+";display:grid;place-items:center;width:24px;height:24px;border:1px solid var(--line);border-radius:999px;color:var(--sub);font-weight:700}.details-panel[open]>summary::after{content:"-"}.details-body{padding:0 14px 14px}.section-note{margin:0;color:var(--sub);font-size:12px}
|
||||
.resource-card{margin-top:18px}.resource-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:12px}.resource-head h2{margin:0}.baseline-pill{color:var(--sub);font-size:12px;font-variant-numeric:tabular-nums;text-align:right;white-space:nowrap}.spike-log-box{margin-top:16px}.spike-log-box h3{margin:0 0 9px;color:var(--text);font-size:14px;font-weight:500}.spike-log-list{display:grid;gap:8px;max-height:260px;overflow:auto;padding-right:4px}.spike-log-list.compact{max-height:180px}.spike-log-item{display:grid;gap:4px;padding:10px 12px;border:1px solid var(--notice-border);background:var(--notice-bg);border-radius:12px}.spike-log-item.latest{border:1px solid var(--notice-border-strong);background:var(--notice-bg-strong);box-shadow:0 0 0 1px var(--notice-border) inset;}.spike-log-item.alert{border-color:rgba(255,95,87,.58);background:rgba(150,38,38,.18)}.spike-log-item.alert strong{color:#ffb4ad}.spike-log-item strong{font-size:13px;font-weight:500;color:var(--notice-title)}.spike-log-item span{font-size:12px;color:var(--notice-copy);font-variant-numeric:tabular-nums}.spike-log-empty{padding:10px 12px;border-radius:12px;background:var(--card2);color:var(--sub);font-size:13px}.service-list{display:grid;gap:10px;max-height:520px;overflow:auto;padding-right:4px}.service-item{border:1px solid var(--line);border-radius:12px;background:var(--card2);padding:12px;display:grid;gap:9px}.service-head{display:flex;justify-content:space-between;gap:10px;align-items:flex-start}.service-title{display:grid;gap:3px;min-width:0}.service-name{font-size:14px;color:var(--text);font-weight:600;word-break:break-word}.service-desc,.service-meta{font-size:12px;color:var(--sub);font-variant-numeric:tabular-nums;word-break:break-word}.service-state{border:1px solid var(--line);border-radius:999px;padding:4px 9px;font-size:12px;color:var(--sub);white-space:nowrap}.service-state.active{color:#bbf7d0;border-color:rgba(34,197,94,.35);background:rgba(22,163,74,.14)}.service-state.failed{color:#fecaca;border-color:rgba(239,68,68,.4);background:rgba(185,28,28,.16)}.service-log{margin:0;max-height:160px;overflow:auto;border:1px solid var(--line);border-radius:10px;background:var(--code-bg);color:var(--mono-text);padding:10px;font:11px/1.45 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;white-space:pre-wrap;word-break:break-word}.dmesg-head{display:flex;align-items:center;justify-content:space-between;gap:10px}.dmesg-meta{font-size:12px;color:var(--sub);font-variant-numeric:tabular-nums}.dmesg-log{margin:10px 0 0;max-height:420px;overflow:auto;border:1px solid var(--line);border-radius:12px;background:var(--code-bg);color:var(--mono-text);padding:12px;font:12px/1.45 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;white-space:pre-wrap;word-break:break-word}.dmesg-log[hidden]{display:none}
|
||||
|
||||
Reference in New Issue
Block a user