네트워크 오류 드롭 누적 표시 정리
This commit is contained in:
@@ -76,7 +76,8 @@ Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를
|
||||
- `control_state`: 팬 모드와 PWM 상태
|
||||
- `presence_fan_policy_state`: 외출/귀가 기반 팬 정책의 마지막 확인/적용 상태
|
||||
- `sensor_logs`: 온도, RPM, PWM, load, memory, disk, uptime. 과거 배터리 컬럼은 호환을 위해 유지하지만 UPS 제거 상태에서는 새 배터리값을 기록하지 않습니다.
|
||||
- `network_usage_logs`: WAN, LAN, 2.4G WiFi, 5G WiFi 기준 누적 byte/packet/error/drop 카운터와 초당 다운로드/업로드 속도, RX/TX 패킷, 오류/드롭 이력
|
||||
- `network_usage_logs`: WAN, LAN, 2.4G WiFi, 5G WiFi 기준 byte/packet/error/drop 커널 카운터와 초당 다운로드/업로드 속도, RX/TX 패킷 이력
|
||||
- `network_quality_totals`: 인터페이스별 RX/TX 오류와 드롭 누적 개수. 커널 카운터가 재부팅으로 초기화되면 현재 값을 새 증가분으로 보고 DB 누적값에 더합니다.
|
||||
- `power_event_states`: 저전압/스로틀링 episode 상태, 최근 지속시간, 최근 이벤트 이력
|
||||
- `battery_profile_cache`: 배터리 잔여시간 계산용 장기 프로파일 캐시
|
||||
- `systemd_service_cache`: 사용자 생성 systemd 서비스 목록과 최근 journal 로그의 짧은 TTL 캐시
|
||||
|
||||
@@ -622,6 +622,28 @@ function bootstrap_db(): void
|
||||
COLLATE=utf8mb4_unicode_ci
|
||||
");
|
||||
|
||||
$pdo->exec("
|
||||
CREATE TABLE IF NOT EXISTS network_quality_totals (
|
||||
iface VARCHAR(32) NOT NULL PRIMARY KEY,
|
||||
|
||||
rx_errors_total BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
tx_errors_total BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
rx_dropped_total BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
tx_dropped_total BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
|
||||
last_rx_errors BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
last_tx_errors BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
last_rx_dropped BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
last_tx_dropped BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
|
||||
ON UPDATE CURRENT_TIMESTAMP(3)
|
||||
) ENGINE=InnoDB
|
||||
DEFAULT CHARSET=utf8mb4
|
||||
COLLATE=utf8mb4_unicode_ci
|
||||
");
|
||||
|
||||
$pdo->exec("
|
||||
CREATE TABLE IF NOT EXISTS remember_tokens (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
|
||||
+143
@@ -1057,6 +1057,8 @@ function add_network_usage_log_for_interface(array $iface): void
|
||||
}
|
||||
|
||||
$pdo = db();
|
||||
update_network_quality_totals($name, $rxErrors, $txErrors, $rxDropped, $txDropped);
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT
|
||||
recorded_at,
|
||||
@@ -1179,6 +1181,103 @@ function add_network_usage_log_for_interface(array $iface): void
|
||||
]);
|
||||
}
|
||||
|
||||
function update_network_quality_totals(string $iface, int $rxErrors, int $txErrors, int $rxDropped, int $txDropped): void
|
||||
{
|
||||
$pdo = db();
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT
|
||||
last_rx_errors,
|
||||
last_tx_errors,
|
||||
last_rx_dropped,
|
||||
last_tx_dropped
|
||||
FROM network_quality_totals
|
||||
WHERE iface = :iface
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute([':iface' => $iface]);
|
||||
$previous = $stmt->fetch() ?: null;
|
||||
|
||||
if (!$previous) {
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO network_quality_totals
|
||||
(
|
||||
iface,
|
||||
rx_errors_total,
|
||||
tx_errors_total,
|
||||
rx_dropped_total,
|
||||
tx_dropped_total,
|
||||
last_rx_errors,
|
||||
last_tx_errors,
|
||||
last_rx_dropped,
|
||||
last_tx_dropped
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
:iface,
|
||||
:rx_errors_total,
|
||||
:tx_errors_total,
|
||||
:rx_dropped_total,
|
||||
:tx_dropped_total,
|
||||
:last_rx_errors,
|
||||
:last_tx_errors,
|
||||
:last_rx_dropped,
|
||||
:last_tx_dropped
|
||||
)
|
||||
");
|
||||
$stmt->execute([
|
||||
':iface' => $iface,
|
||||
':rx_errors_total' => $rxErrors,
|
||||
':tx_errors_total' => $txErrors,
|
||||
':rx_dropped_total' => $rxDropped,
|
||||
':tx_dropped_total' => $txDropped,
|
||||
':last_rx_errors' => $rxErrors,
|
||||
':last_tx_errors' => $txErrors,
|
||||
':last_rx_dropped' => $rxDropped,
|
||||
':last_tx_dropped' => $txDropped,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$rxErrorDelta = $rxErrors >= (int)$previous['last_rx_errors']
|
||||
? $rxErrors - (int)$previous['last_rx_errors']
|
||||
: $rxErrors;
|
||||
$txErrorDelta = $txErrors >= (int)$previous['last_tx_errors']
|
||||
? $txErrors - (int)$previous['last_tx_errors']
|
||||
: $txErrors;
|
||||
$rxDroppedDelta = $rxDropped >= (int)$previous['last_rx_dropped']
|
||||
? $rxDropped - (int)$previous['last_rx_dropped']
|
||||
: $rxDropped;
|
||||
$txDroppedDelta = $txDropped >= (int)$previous['last_tx_dropped']
|
||||
? $txDropped - (int)$previous['last_tx_dropped']
|
||||
: $txDropped;
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE network_quality_totals
|
||||
SET
|
||||
rx_errors_total = rx_errors_total + :rx_error_delta,
|
||||
tx_errors_total = tx_errors_total + :tx_error_delta,
|
||||
rx_dropped_total = rx_dropped_total + :rx_dropped_delta,
|
||||
tx_dropped_total = tx_dropped_total + :tx_dropped_delta,
|
||||
last_rx_errors = :last_rx_errors,
|
||||
last_tx_errors = :last_tx_errors,
|
||||
last_rx_dropped = :last_rx_dropped,
|
||||
last_tx_dropped = :last_tx_dropped,
|
||||
updated_at = NOW(3)
|
||||
WHERE iface = :iface
|
||||
");
|
||||
$stmt->execute([
|
||||
':rx_error_delta' => $rxErrorDelta,
|
||||
':tx_error_delta' => $txErrorDelta,
|
||||
':rx_dropped_delta' => $rxDroppedDelta,
|
||||
':tx_dropped_delta' => $txDroppedDelta,
|
||||
':last_rx_errors' => $rxErrors,
|
||||
':last_tx_errors' => $txErrors,
|
||||
':last_rx_dropped' => $rxDropped,
|
||||
':last_tx_dropped' => $txDropped,
|
||||
':iface' => $iface,
|
||||
]);
|
||||
}
|
||||
|
||||
function add_network_usage_log(array $network): void
|
||||
{
|
||||
foreach (network_log_interfaces($network) as $iface) {
|
||||
@@ -1186,6 +1285,48 @@ function add_network_usage_log(array $network): void
|
||||
}
|
||||
}
|
||||
|
||||
function network_quality_totals(): array
|
||||
{
|
||||
$stmt = db()->query("
|
||||
SELECT
|
||||
iface,
|
||||
rx_errors_total,
|
||||
tx_errors_total,
|
||||
rx_dropped_total,
|
||||
tx_dropped_total,
|
||||
last_rx_errors,
|
||||
last_tx_errors,
|
||||
last_rx_dropped,
|
||||
last_tx_dropped,
|
||||
updated_at
|
||||
FROM network_quality_totals
|
||||
ORDER BY iface
|
||||
");
|
||||
|
||||
$totals = [];
|
||||
foreach ($stmt->fetchAll() as $row) {
|
||||
$iface = (string)($row['iface'] ?? '');
|
||||
if ($iface === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$totals[$iface] = [
|
||||
'iface' => $iface,
|
||||
'rx_errors_total' => (int)$row['rx_errors_total'],
|
||||
'tx_errors_total' => (int)$row['tx_errors_total'],
|
||||
'rx_dropped_total' => (int)$row['rx_dropped_total'],
|
||||
'tx_dropped_total' => (int)$row['tx_dropped_total'],
|
||||
'last_rx_errors' => (int)$row['last_rx_errors'],
|
||||
'last_tx_errors' => (int)$row['last_tx_errors'],
|
||||
'last_rx_dropped' => (int)$row['last_rx_dropped'],
|
||||
'last_tx_dropped' => (int)$row['last_tx_dropped'],
|
||||
'updated_at' => $row['updated_at'],
|
||||
];
|
||||
}
|
||||
|
||||
return $totals;
|
||||
}
|
||||
|
||||
function network_usage_history(int $limit = 240): array
|
||||
{
|
||||
$limit = max(1, min(1500, $limit));
|
||||
@@ -4604,6 +4745,7 @@ function collect_snapshot(bool $applyFan = true): array
|
||||
$network = network_info();
|
||||
add_network_usage_log($network);
|
||||
$networkHistory = network_usage_history(1000);
|
||||
$networkQualityTotals = network_quality_totals();
|
||||
|
||||
$shouldLogNotice = !empty($fanSpike['alert_started'])
|
||||
|| (!empty($fanSpike['active']) && (bool)setting_value('notice.log_during_alert'));
|
||||
@@ -4626,6 +4768,7 @@ function collect_snapshot(bool $applyFan = true): array
|
||||
'wifi' => wifi_data(),
|
||||
'network' => $network,
|
||||
'network_history' => $networkHistory,
|
||||
'network_quality_totals' => $networkQualityTotals,
|
||||
'history' => $history,
|
||||
'processes' => $processes,
|
||||
'custom_services' => custom_systemd_services(),
|
||||
|
||||
+30
-116
@@ -2507,6 +2507,27 @@
|
||||
return rows.filter(row => String(row.iface || '') === iface).slice(-180);
|
||||
}
|
||||
|
||||
function formatCount(value) {
|
||||
const numeric = Number(value);
|
||||
if (!Number.isFinite(numeric)) return '0';
|
||||
return Math.max(0, Math.round(numeric)).toLocaleString();
|
||||
}
|
||||
|
||||
function networkQualitySummary(elementId, totals, direction) {
|
||||
const el = $('#' + elementId);
|
||||
if (!el) return;
|
||||
|
||||
const errorKey = direction === 'rx' ? 'rx_errors_total' : 'tx_errors_total';
|
||||
const dropKey = direction === 'rx' ? 'rx_dropped_total' : 'tx_dropped_total';
|
||||
const title = direction === 'rx' ? t('rxErrorsDrops') : t('txErrorsDrops');
|
||||
const row = totals || {};
|
||||
|
||||
el.innerHTML = `
|
||||
<span>${escapeHtml(title)}</span>
|
||||
<span><strong>${escapeHtml(formatCount(row[errorKey]))}</strong> ${escapeHtml(t('errorsLine'))} / <strong>${escapeHtml(formatCount(row[dropKey]))}</strong> ${escapeHtml(t('dropsLine'))}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
function networkChart(canvasId, rows, speedKey, speedLabel, packetKey, packetLabel, speedColor = '#0ea5e9') {
|
||||
const canvas = $('#' + canvasId);
|
||||
if (!canvas || typeof Chart === 'undefined') return;
|
||||
@@ -2634,114 +2655,6 @@
|
||||
c.update('none');
|
||||
}
|
||||
|
||||
function networkQualityChart(canvasId, rows, errorKey, dropKey) {
|
||||
const canvas = $('#' + canvasId);
|
||||
if (!canvas || typeof Chart === 'undefined') return;
|
||||
|
||||
const { labels, xTickCallback, xGridColor } = timeAxis(rows);
|
||||
const errorValues = rows.map(row => {
|
||||
const value = Number(row[errorKey]);
|
||||
return Number.isFinite(value) ? value : null;
|
||||
});
|
||||
const dropValues = rows.map(row => {
|
||||
const value = Number(row[dropKey]);
|
||||
return Number.isFinite(value) ? value : null;
|
||||
});
|
||||
const scale = dynamicScaleForValues([...errorValues, ...dropValues], { minSpan: 1, padding: 0.2 });
|
||||
const datasets = [
|
||||
{
|
||||
label: t('errorsLine'),
|
||||
data: errorValues,
|
||||
borderColor: '#ef4444',
|
||||
backgroundColor: 'transparent',
|
||||
borderWidth: 2.1,
|
||||
pointRadius: 0,
|
||||
tension: 0.2,
|
||||
fill: false,
|
||||
},
|
||||
{
|
||||
label: t('dropsLine'),
|
||||
data: dropValues,
|
||||
borderColor: '#f59e0b',
|
||||
backgroundColor: 'transparent',
|
||||
borderWidth: 2,
|
||||
borderDash: [5, 4],
|
||||
pointRadius: 0,
|
||||
tension: 0.2,
|
||||
fill: false,
|
||||
},
|
||||
];
|
||||
const options = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: false,
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
boxWidth: 18,
|
||||
boxHeight: 2,
|
||||
color: '#8d98aa',
|
||||
padding: 10,
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
displayColors: true,
|
||||
padding: 8,
|
||||
callbacks: {
|
||||
label: item => `${item.dataset.label}: ${formatChartNumber(item.parsed.y, '/s')}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: {
|
||||
color: '#64748b',
|
||||
maxRotation: 0,
|
||||
autoSkip: false,
|
||||
callback: xTickCallback,
|
||||
},
|
||||
grid: {
|
||||
color: xGridColor,
|
||||
drawTicks: false,
|
||||
},
|
||||
border: { display: false },
|
||||
},
|
||||
y: {
|
||||
min: scale.min,
|
||||
max: scale.max,
|
||||
ticks: {
|
||||
color: '#8d98aa',
|
||||
maxTicksLimit: 4,
|
||||
callback: value => formatChartNumber(value, '/s'),
|
||||
},
|
||||
grid: { color: 'rgba(255,255,255,.045)', drawTicks: false },
|
||||
border: { display: false },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (!state.charts[canvasId]) {
|
||||
state.charts[canvasId] = new Chart(canvas, {
|
||||
type: 'line',
|
||||
data: { labels, datasets },
|
||||
options,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const c = state.charts[canvasId];
|
||||
c.data.labels = labels;
|
||||
c.data.datasets = datasets;
|
||||
c.options = options;
|
||||
c.update('none');
|
||||
}
|
||||
|
||||
function dynamicScaleForValues(values, options = {}) {
|
||||
const floor = Number(options.floor ?? 0);
|
||||
const ceiling = Number.isFinite(Number(options.ceiling)) ? Number(options.ceiling) : null;
|
||||
@@ -2804,22 +2717,23 @@
|
||||
const lanRows = networkRowsForIface(networkRows, 'eth1');
|
||||
const wifi24Rows = networkRowsForIface(networkRows, 'wlan0');
|
||||
const wifi5Rows = networkRowsForIface(networkRows, 'wlan1');
|
||||
const networkQualityTotals = data.network_quality_totals || {};
|
||||
networkChart('netWanRxChart', wanRows, 'rx_mbytes', t('downloadLine'), 'rx_pps', t('rxPacketsLine'), '#0ea5e9');
|
||||
networkChart('netWanTxChart', wanRows, 'tx_mbytes', t('uploadLine'), 'tx_pps', t('txPacketsLine'), '#22c55e');
|
||||
networkQualityChart('netWanRxQualityChart', wanRows, 'rx_errors_per_sec', 'rx_dropped_per_sec');
|
||||
networkQualityChart('netWanTxQualityChart', wanRows, 'tx_errors_per_sec', 'tx_dropped_per_sec');
|
||||
networkQualitySummary('netWanRxQualitySummary', networkQualityTotals[wanIface], 'rx');
|
||||
networkQualitySummary('netWanTxQualitySummary', networkQualityTotals[wanIface], 'tx');
|
||||
networkChart('netLanRxChart', lanRows, 'rx_mbytes', t('downloadLine'), 'rx_pps', t('rxPacketsLine'), '#0ea5e9');
|
||||
networkChart('netLanTxChart', lanRows, 'tx_mbytes', t('uploadLine'), 'tx_pps', t('txPacketsLine'), '#22c55e');
|
||||
networkQualityChart('netLanRxQualityChart', lanRows, 'rx_errors_per_sec', 'rx_dropped_per_sec');
|
||||
networkQualityChart('netLanTxQualityChart', lanRows, 'tx_errors_per_sec', 'tx_dropped_per_sec');
|
||||
networkQualitySummary('netLanRxQualitySummary', networkQualityTotals.eth1, 'rx');
|
||||
networkQualitySummary('netLanTxQualitySummary', networkQualityTotals.eth1, 'tx');
|
||||
networkChart('netWifi24RxChart', wifi24Rows, 'rx_mbytes', t('downloadLine'), 'rx_pps', t('rxPacketsLine'), '#0ea5e9');
|
||||
networkChart('netWifi24TxChart', wifi24Rows, 'tx_mbytes', t('uploadLine'), 'tx_pps', t('txPacketsLine'), '#22c55e');
|
||||
networkQualityChart('netWifi24RxQualityChart', wifi24Rows, 'rx_errors_per_sec', 'rx_dropped_per_sec');
|
||||
networkQualityChart('netWifi24TxQualityChart', wifi24Rows, 'tx_errors_per_sec', 'tx_dropped_per_sec');
|
||||
networkQualitySummary('netWifi24RxQualitySummary', networkQualityTotals.wlan0, 'rx');
|
||||
networkQualitySummary('netWifi24TxQualitySummary', networkQualityTotals.wlan0, 'tx');
|
||||
networkChart('netWifi5RxChart', wifi5Rows, 'rx_mbytes', t('downloadLine'), 'rx_pps', t('rxPacketsLine'), '#0ea5e9');
|
||||
networkChart('netWifi5TxChart', wifi5Rows, 'tx_mbytes', t('uploadLine'), 'tx_pps', t('txPacketsLine'), '#22c55e');
|
||||
networkQualityChart('netWifi5RxQualityChart', wifi5Rows, 'rx_errors_per_sec', 'rx_dropped_per_sec');
|
||||
networkQualityChart('netWifi5TxQualityChart', wifi5Rows, 'tx_errors_per_sec', 'tx_dropped_per_sec');
|
||||
networkQualitySummary('netWifi5RxQualitySummary', networkQualityTotals.wlan1, 'rx');
|
||||
networkQualitySummary('netWifi5TxQualitySummary', networkQualityTotals.wlan1, 'tx');
|
||||
}
|
||||
|
||||
function resizeChartsSoon() {
|
||||
|
||||
+9
-9
@@ -150,7 +150,7 @@ html[data-theme="light"] .btn.secondary{background:#d9e2ef;color:#172033}html[da
|
||||
.table-wrap{max-width:100%;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;border-radius:16px;border:1px solid var(--line)}.table-wrap.compact{border-radius:12px}.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 .lan-link-speed{text-align:center}.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,.lan-wake-btn{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,.lan-wake-btn:hover{color:var(--blue)}.lan-wake-btn{font-weight:700}
|
||||
.wifi-signal{font-weight:700;font-variant-numeric:tabular-nums;cursor:help;text-decoration:underline;text-decoration-style:dotted;text-decoration-thickness:1px;text-underline-offset:3px}.wifi-signal.best{color:#16a34a}.wifi-signal.excellent{color:#22c55e}.wifi-signal.good{color:#38bdf8}.wifi-signal.fine{color:#0ea5e9}.wifi-signal.normal{color:#f59e0b}.wifi-signal.weak{color:#fb7185}.wifi-signal.bad{color:#ef4444}.wifi-signal.unknown{color:var(--sub);text-decoration:none;cursor:default}
|
||||
.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.wide{grid-column:span 2}.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}
|
||||
.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.wide{grid-column:span 2}.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}.quality-summary{height:48px;padding:0 12px;display:flex;align-items:center}.quality-pill{display:grid;grid-template-columns:auto 1fr;gap:8px 18px;width:100%;font-size:13px;color:var(--sub);font-variant-numeric:tabular-nums}.quality-pill strong{color:var(--text);font-size:15px;font-weight:500}.quality-pill span:last-child{justify-self:end}
|
||||
.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-list.remote-log-list{grid-template-columns:repeat(auto-fit,minmax(230px,1fr));max-height:none}.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.warning{color:#fde68a;border-color:rgba(245,158,11,.42);background:rgba(180,83,9,.16)}.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}
|
||||
html[data-theme="light"] .details-panel{background:#edf3fa;border-color:#b8c7da}html[data-theme="light"] .service-item{background:#e8f0f8;border-color:#b7c8dd}html[data-theme="light"] .service-name{color:#0f1f35}html[data-theme="light"] .service-desc,html[data-theme="light"] .service-meta{color:#42526a}html[data-theme="light"] .service-state{background:#f8fbff;border-color:#aebed2;color:#304057}html[data-theme="light"] .service-state.active{background:#d8f3e3;border-color:#7fc99c;color:#146c39}html[data-theme="light"] .service-state.warning{background:#fff2c7;border-color:#dca642;color:#8a4d08}html[data-theme="light"] .service-state.failed{background:#ffe2e2;border-color:#e38b8b;color:#9f1d1d}html[data-theme="light"] .service-log,html[data-theme="light"] .dmesg-log{background:#f7fafc;border-color:#b9c8da;color:#0f2742}
|
||||
@@ -289,8 +289,8 @@ html[data-theme="light"] .details-panel{background:#edf3fa;border-color:#b8c7da}
|
||||
<div class="chart-grid">
|
||||
<div class="chart-box wide"><h3 data-i18n="downloadRxPackets">Download / RX Packets</h3><div class="chart-canvas"><canvas id="netWanRxChart"></canvas></div></div>
|
||||
<div class="chart-box wide"><h3 data-i18n="uploadTxPackets">Upload / TX Packets</h3><div class="chart-canvas"><canvas id="netWanTxChart"></canvas></div></div>
|
||||
<div class="chart-box wide"><h3 data-i18n="rxErrorsDrops">RX Errors / Drops</h3><div class="chart-canvas"><canvas id="netWanRxQualityChart"></canvas></div></div>
|
||||
<div class="chart-box wide"><h3 data-i18n="txErrorsDrops">TX Errors / Drops</h3><div class="chart-canvas"><canvas id="netWanTxQualityChart"></canvas></div></div>
|
||||
<div class="chart-box wide quality-summary"><div id="netWanRxQualitySummary" class="quality-pill"></div></div>
|
||||
<div class="chart-box wide quality-summary"><div id="netWanTxQualitySummary" class="quality-pill"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -299,8 +299,8 @@ html[data-theme="light"] .details-panel{background:#edf3fa;border-color:#b8c7da}
|
||||
<div class="chart-grid">
|
||||
<div class="chart-box wide"><h3 data-i18n="downloadRxPackets">Download / RX Packets</h3><div class="chart-canvas"><canvas id="netLanRxChart"></canvas></div></div>
|
||||
<div class="chart-box wide"><h3 data-i18n="uploadTxPackets">Upload / TX Packets</h3><div class="chart-canvas"><canvas id="netLanTxChart"></canvas></div></div>
|
||||
<div class="chart-box wide"><h3 data-i18n="rxErrorsDrops">RX Errors / Drops</h3><div class="chart-canvas"><canvas id="netLanRxQualityChart"></canvas></div></div>
|
||||
<div class="chart-box wide"><h3 data-i18n="txErrorsDrops">TX Errors / Drops</h3><div class="chart-canvas"><canvas id="netLanTxQualityChart"></canvas></div></div>
|
||||
<div class="chart-box wide quality-summary"><div id="netLanRxQualitySummary" class="quality-pill"></div></div>
|
||||
<div class="chart-box wide quality-summary"><div id="netLanTxQualitySummary" class="quality-pill"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -309,8 +309,8 @@ html[data-theme="light"] .details-panel{background:#edf3fa;border-color:#b8c7da}
|
||||
<div class="chart-grid">
|
||||
<div class="chart-box wide"><h3 data-i18n="downloadRxPackets">Download / RX Packets</h3><div class="chart-canvas"><canvas id="netWifi24RxChart"></canvas></div></div>
|
||||
<div class="chart-box wide"><h3 data-i18n="uploadTxPackets">Upload / TX Packets</h3><div class="chart-canvas"><canvas id="netWifi24TxChart"></canvas></div></div>
|
||||
<div class="chart-box wide"><h3 data-i18n="rxErrorsDrops">RX Errors / Drops</h3><div class="chart-canvas"><canvas id="netWifi24RxQualityChart"></canvas></div></div>
|
||||
<div class="chart-box wide"><h3 data-i18n="txErrorsDrops">TX Errors / Drops</h3><div class="chart-canvas"><canvas id="netWifi24TxQualityChart"></canvas></div></div>
|
||||
<div class="chart-box wide quality-summary"><div id="netWifi24RxQualitySummary" class="quality-pill"></div></div>
|
||||
<div class="chart-box wide quality-summary"><div id="netWifi24TxQualitySummary" class="quality-pill"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -319,8 +319,8 @@ html[data-theme="light"] .details-panel{background:#edf3fa;border-color:#b8c7da}
|
||||
<div class="chart-grid">
|
||||
<div class="chart-box wide"><h3 data-i18n="downloadRxPackets">Download / RX Packets</h3><div class="chart-canvas"><canvas id="netWifi5RxChart"></canvas></div></div>
|
||||
<div class="chart-box wide"><h3 data-i18n="uploadTxPackets">Upload / TX Packets</h3><div class="chart-canvas"><canvas id="netWifi5TxChart"></canvas></div></div>
|
||||
<div class="chart-box wide"><h3 data-i18n="rxErrorsDrops">RX Errors / Drops</h3><div class="chart-canvas"><canvas id="netWifi5RxQualityChart"></canvas></div></div>
|
||||
<div class="chart-box wide"><h3 data-i18n="txErrorsDrops">TX Errors / Drops</h3><div class="chart-canvas"><canvas id="netWifi5TxQualityChart"></canvas></div></div>
|
||||
<div class="chart-box wide quality-summary"><div id="netWifi5RxQualitySummary" class="quality-pill"></div></div>
|
||||
<div class="chart-box wide quality-summary"><div id="netWifi5TxQualitySummary" class="quality-pill"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user