diff --git a/public/assets/app.js b/public/assets/app.js
index 6741882..f339d05 100644
--- a/public/assets/app.js
+++ b/public/assets/app.js
@@ -85,6 +85,11 @@
systemStatus: 'System Status',
loadAvg: 'Load Avg',
activeUsers: 'Active Users',
+ diskRoot: 'Disk /',
+ memory: 'Memory',
+ uptime: 'Uptime',
+ batteryV: 'Battery V',
+ batterySoc: 'Battery SOC',
sensorHistory: 'Sensor History',
temperature: 'Temperature',
rp1Temp: 'RP1 Temp',
@@ -131,6 +136,23 @@
average: 'Average',
current: 'Current',
causeCandidate: 'Candidate',
+ hostLabel: 'Host',
+ ipLabel: 'IP',
+ created: 'Created',
+ encoding: 'Encoding',
+ endpoint: 'Endpoint',
+ userAgent: 'UA',
+ hash: 'Hash',
+ na: 'N/A',
+ max: 'MAX',
+ avg: 'AVG',
+ min: 'MIN',
+ minuteAgo: ({ minutes }) => `${minutes}m ago`,
+ health_healthy: 'Healthy',
+ health_watch: 'Watch',
+ health_stale: 'Stale',
+ health_failed: 'Failed',
+ health_pending: 'Pending',
pushSummary: ({ total, healthy, watch, stale, failed, pending }) => `Push devices: ${total} total / ${healthy} healthy / ${watch} watch / ${stale} stale / ${failed} failed / ${pending} pending`,
statusUnsupported: 'Push status: browser unsupported',
statusDenied: 'Push status: permission denied',
@@ -198,6 +220,11 @@
systemStatus: '시스템 상태',
loadAvg: '부하 평균',
activeUsers: '활성 사용자',
+ diskRoot: 'Disk /',
+ memory: '메모리',
+ uptime: '가동 시간',
+ batteryV: '배터리 전압',
+ batterySoc: '배터리 SOC',
sensorHistory: '센서 이력',
temperature: '온도',
rp1Temp: 'RP1 온도',
@@ -244,7 +271,24 @@
average: '평균',
current: '현재',
causeCandidate: '원인 후보',
- pushSummary: ({ total, healthy, watch, stale, failed, pending }) => `Push devices: ${total} total / ${healthy} healthy / ${watch} watch / ${stale} stale / ${failed} failed / ${pending} pending`,
+ hostLabel: '호스트',
+ ipLabel: 'IP',
+ created: '생성',
+ encoding: '인코딩',
+ endpoint: 'Endpoint',
+ userAgent: 'UA',
+ hash: 'Hash',
+ na: 'N/A',
+ max: '최대',
+ avg: '평균',
+ min: '최소',
+ minuteAgo: ({ minutes }) => `${minutes}분 전`,
+ health_healthy: '정상',
+ health_watch: '주의',
+ health_stale: '장기 미수신',
+ health_failed: '실패',
+ health_pending: '수신 대기',
+ pushSummary: ({ total, healthy, watch, stale, failed, pending }) => `Push 기기: 총 ${total} / 정상 ${healthy} / 주의 ${watch} / 장기 미수신 ${stale} / 실패 ${failed} / 대기 ${pending}`,
statusUnsupported: 'Push 상태: 브라우저 미지원',
statusDenied: 'Push 상태: 권한 꺼짐',
statusUngrant: 'Push 상태: 권한 미허용',
@@ -311,6 +355,11 @@
}
window.controlT = t;
+ function pushHealthText(status, fallback = '') {
+ const key = `health_${status || 'pending'}`;
+ return messages[state.lang]?.[key] ?? messages.en[key] ?? fallback ?? t('pendingReceive');
+ }
+
function applyTheme(theme) {
state.theme = theme === 'light' ? 'light' : 'dark';
localStorage.setItem(storageKeys.theme, state.theme);
@@ -692,10 +741,17 @@
const secs = totalSeconds % 60;
const parts = [];
- if (days > 0) parts.push(`${days}d`);
- if (days > 0 || hours > 0) parts.push(`${hours}h`);
- if (days > 0 || hours > 0 || minutes > 0) parts.push(`${minutes}m`);
- parts.push(`${secs}s`);
+ if (state.lang === 'ko') {
+ if (days > 0) parts.push(`${days}일`);
+ if (days > 0 || hours > 0) parts.push(`${hours}시`);
+ if (days > 0 || hours > 0 || minutes > 0) parts.push(`${minutes}분`);
+ parts.push(`${secs}초`);
+ } else {
+ if (days > 0) parts.push(`${days}d`);
+ if (days > 0 || hours > 0) parts.push(`${hours}h`);
+ if (days > 0 || hours > 0 || minutes > 0) parts.push(`${minutes}m`);
+ parts.push(`${secs}s`);
+ }
return parts.join(' ');
}
@@ -921,27 +977,32 @@
}
els.pushDeviceList.innerHTML = devices.length
- ? devices.map(device => `
-
+ ? devices.map(device => {
+ const healthStatus = device.health_status || 'pending';
+ const healthText = pushHealthText(healthStatus, device.health_text);
+
+ return `
+
${escapeHtml(device.device_name || t('unnamed'))}
- ${escapeHtml(device.health_text || t('pendingReceive'))}
- Host: ${escapeHtml(device.host || 'unknown')}
- IP: ${escapeHtml(device.actor_ip || '-')}
- Created: ${escapeHtml(device.created_at || '-')}
+ ${escapeHtml(healthText)}
+ ${escapeHtml(t('hostLabel'))}: ${escapeHtml(device.host || 'unknown')}
+ ${escapeHtml(t('ipLabel'))}: ${escapeHtml(device.actor_ip || '-')}
+ ${escapeHtml(t('created'))}: ${escapeHtml(device.created_at || '-')}
${escapeHtml(t('registeredRefresh'))}: ${escapeHtml(device.last_seen_at || '-')} (${escapeHtml(timeAgo(device.last_seen_seconds))})
${escapeHtml(t('lastSendSuccess'))}: ${escapeHtml(device.last_send_success_at || '-')} (${escapeHtml(timeAgo(device.last_send_success_seconds))})
${escapeHtml(t('lastReceived'))}: ${escapeHtml(device.last_received_at || '-')} (${escapeHtml(timeAgo(device.last_received_seconds))})
${escapeHtml(t('lastShown'))}: ${escapeHtml(device.last_notification_at || '-')} (${escapeHtml(timeAgo(device.last_notification_seconds))})
${escapeHtml(t('lastClick'))}: ${escapeHtml(device.last_click_at || '-')}
${escapeHtml(t('failures'))}: ${Number(device.failure_count || 0).toLocaleString()}${device.last_failure_reason ? ` / ${escapeHtml(device.last_failure_reason)}` : ''}
- Encoding: ${escapeHtml(device.content_encoding || '-')}
- Hash: ${escapeHtml(device.hash || '-')}
- Endpoint: ${escapeHtml(device.endpoint || '-')}
- UA: ${escapeHtml(device.user_agent || t('unknownDevice'))}
+ ${escapeHtml(t('encoding'))}: ${escapeHtml(device.content_encoding || '-')}
+ ${escapeHtml(t('hash'))}: ${escapeHtml(device.hash || '-')}
+ ${escapeHtml(t('endpoint'))}: ${escapeHtml(device.endpoint || '-')}
+ ${escapeHtml(t('userAgent'))}: ${escapeHtml(device.user_agent || t('unknownDevice'))}
- `).join('')
+ `;
+ }).join('')
: `
${escapeHtml(t('noPushDevices'))}
`;
}
@@ -1082,15 +1143,27 @@
const minutes = Math.floor((totalSeconds % 3600) / 60);
const parts = [];
- if (days > 0) {
- parts.push(`${days}일`);
- }
+ if (state.lang === 'ko') {
+ if (days > 0) {
+ parts.push(`${days}일`);
+ }
- if (days > 0 || hours > 0) {
- parts.push(`${hours}시`);
- }
+ if (days > 0 || hours > 0) {
+ parts.push(`${hours}시`);
+ }
- parts.push(`${minutes}분`);
+ parts.push(`${minutes}분`);
+ } else {
+ if (days > 0) {
+ parts.push(`${days}d`);
+ }
+
+ if (days > 0 || hours > 0) {
+ parts.push(`${hours}h`);
+ }
+
+ parts.push(`${minutes}m`);
+ }
return parts.join(' ');
}
@@ -1125,7 +1198,7 @@
}
});
if (bestIndex >= 0 && bestDistance <= 15) {
- xTickMarkers.set(bestIndex, `${targetAge / 60}m ago`);
+ xTickMarkers.set(bestIndex, t('minuteAgo', { minutes: targetAge / 60 }));
}
});
const isXGridTick = index => {
@@ -1206,9 +1279,9 @@
? '-'
: `${Number(value).toLocaleString(undefined, { maximumFractionDigits: 2 })}${suffix}`;
return [
- `MAX: ${format(max)}`,
- `AVG: ${format(avg)}`,
- `MIN: ${format(min)}`,
+ `${t('max')}: ${format(max)}`,
+ `${t('avg')}: ${format(avg)}`,
+ `${t('min')}: ${format(min)}`,
];
},
label: () => null,
@@ -1538,6 +1611,7 @@
els.translateBtn?.addEventListener('click', () => {
applyLang(state.lang === 'ko' ? 'en' : 'ko');
refreshPushDevices(true);
+ refreshStatus();
});
els.themeBtn?.addEventListener('click', () => {
applyTheme(state.theme === 'dark' ? 'light' : 'dark');
diff --git a/public/index.php b/public/index.php
index c45b63e..deff843 100644
--- a/public/index.php
+++ b/public/index.php
@@ -86,13 +86,15 @@ try {
:root{
--bg:#0f1115;--card:#171a21;--card2:#202633;--line:#2e3747;
--text:#edf1f7;--sub:#a9b4c7;--blue:#3b82f6;--green:#35c46b;
- --input:#11151d;--table-head:#1d2330;--code-bg:#070a10;
+ --input:#11151d;--table-head:#1d2330;--code-bg:#070a10;--mono-text:#d9e2f2;
+ --notice-bg:rgba(138,109,27,.16);--notice-bg-strong:rgba(138,109,27,.28);--notice-border:rgba(255,204,0,.35);--notice-border-strong:rgba(255,204,0,.58);--notice-title:#f3e3a3;--notice-copy:#d7c995;
--red:#ff5f57;--yellow:#ffcc00;--shadow:0 12px 28px rgba(0,0,0,.20);
}
html[data-theme="light"]{
--bg:#f4f7fb;--card:#ffffff;--card2:#eef3f9;--line:#cfd8e5;
--text:#172033;--sub:#657189;--blue:#2563eb;--green:#198754;
- --input:#ffffff;--table-head:#e6edf6;--code-bg:#f8fafc;
+ --input:#ffffff;--table-head:#e6edf6;--code-bg:#f8fafc;--mono-text:#334155;
+ --notice-bg:#fff8db;--notice-bg-strong:#fff0b8;--notice-border:#d69e00;--notice-border-strong:#b7791f;--notice-title:#7a4f00;--notice-copy:#5f4b18;
--red:#c62828;--yellow:#9a6a00;--shadow:0 12px 28px rgba(36,52,78,.13);
}
*{box-sizing:border-box}
@@ -120,9 +122,9 @@ html[data-theme="light"] .btn.secondary{background:#d9e2ef;color:#172033}html[da
.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}
.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}
-.resource-card{margin-top:18px}.resource-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:16px}.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:#c6d3e6;font-size:14px;font-weight:500}.spike-log-list{display:grid;gap:8px;max-height:260px;overflow:auto;padding-right:4px}.spike-log-item{display:grid;gap:4px;padding:10px 12px;border:1px solid rgba(255,204,0,.35);background:rgba(138,109,27,.16);border-radius:12px}.spike-log-item.latest{border:1px solid rgba(255,204,0,.58);background:rgba(138,109,27,.28);box-shadow:0 0 0 1px rgba(255,204,0,.14) inset;}.spike-log-item strong{font-size:13px;font-weight:500;color:#f3e3a3}.spike-log-item span{font-size:12px;color:var(--sub);font-variant-numeric:tabular-nums}.spike-log-empty{padding:10px 12px;border-radius:12px;background:var(--card2);color:var(--sub);font-size:13px}.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:#070a10;color:#d9e2f2;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}
+.resource-card{margin-top:18px}.resource-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:16px}.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-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 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}.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}
.resource-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.resource-box{min-width:0}.resource-box h3{margin:0 0 9px;color:var(--sub);font-size:14px;font-weight:500}.resource-table{width:100%;min-width:620px;table-layout:fixed;border-collapse:collapse;border:1px solid var(--line);border-radius:14px;overflow:hidden}.resource-table th,.resource-table td{padding:9px 10px;border-bottom:1px solid rgba(128,145,170,.18);background:var(--input);text-align:left;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.resource-table th{background:var(--table-head);color:var(--sub);font-weight:500}.resource-table .pid{width:72px}.resource-table .metric{width:82px}.resource-table .service{width:150px}.resource-table .cmd{width:auto}.push-device-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:12px}.push-device-head h3{margin:0}.push-device-actions{display:flex;gap:8px;flex-wrap:wrap}.push-device-list{display:grid;gap:8px;margin-top:8px}.push-device-row{padding:10px 12px;border:1px solid rgba(84,101,128,.58);background:var(--input);border-radius:12px}.push-device-row.healthy{border-color:rgba(53,196,107,.45)}.push-device-row.watch{border-color:rgba(255,204,0,.48)}.push-device-row.stale,.push-device-row.failed{border-color:rgba(255,95,87,.55)}.push-device-main{min-width:0;color:var(--sub);font-size:12px;line-height:1.45;display:grid;gap:2px;word-break:break-all}.push-device-main strong{color:var(--text);font-size:13px}.push-device-badge{display:inline-flex;align-items:center;width:max-content;border-radius:999px;padding:2px 8px;background:#2b3342;color:#edf1f7;font-size:11px}.push-device-badge.healthy{background:#198754}.push-device-badge.watch{background:#8a6d1b}.push-device-badge.stale,.push-device-badge.failed{background:#c62828}
-.notice{position:fixed;right:20px;bottom:20px;min-width:220px;max-width:420px;padding:14px 16px;border-radius:14px;font-weight:500;background:#1d2330;border:1px solid var(--line);box-shadow:var(--shadow);z-index:99}.notice:empty{display:none}.notice[data-type=success]{border-color:rgba(53,196,107,.5)}.notice[data-type=error]{border-color:rgba(255,95,87,.5)}
+.notice{position:fixed;right:20px;bottom:20px;min-width:220px;max-width:420px;padding:14px 16px;border-radius:14px;font-weight:500;background:var(--table-head);color:var(--text);border:1px solid var(--line);box-shadow:var(--shadow);z-index:99}.notice:empty{display:none}.notice[data-type=success]{border-color:rgba(53,196,107,.5)}.notice[data-type=error]{border-color:rgba(255,95,87,.5)}
.dialog-layer{position:fixed;inset:0;display:none;align-items:center;justify-content:center;padding:22px;background:rgba(4,7,12,.68);backdrop-filter:blur(8px);z-index:120}.dialog-layer[data-open="1"]{display:flex}.dialog-box{width:min(430px,100%);border:1px solid rgba(84,101,128,.78);border-radius:20px;background:var(--card);box-shadow:0 24px 70px rgba(0,0,0,.42);padding:20px}.dialog-box h3{margin:0 0 8px;font-size:19px;font-weight:500}.dialog-message{margin:0 0 16px;color:var(--sub);line-height:1.5;white-space:pre-wrap}.dialog-input{width:100%;height:48px;margin-bottom:14px;border-radius:14px;border:1px solid var(--line);background:var(--input);color:var(--text);padding:0 14px;outline:none}.dialog-input:focus{outline:2px solid rgba(59,130,246,.65);outline-offset:2px}.dialog-actions{display:flex;justify-content:flex-end;gap:10px}.dialog-actions .btn{min-width:84px}.dialog-actions .btn.red{background:#c62828}
@media(max-width:1320px){.chart-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media(max-width:1100px){.layout{grid-template-columns:1fr}.chart-grid,.resource-grid{grid-template-columns:1fr}.topbar{align-items:flex-start;flex-direction:column}.topbar-right{width:100%}.topbar-right .btn{flex:1}.stat-grid{grid-template-columns:1fr}.resource-head{align-items:flex-start;flex-direction:column}.baseline-pill{text-align:left;white-space:normal}}
@@ -192,14 +194,14 @@ html[data-theme="light"] .btn.secondary{background:#d9e2ef;color:#172033}html[da
@@ -215,7 +217,7 @@ html[data-theme="light"] .btn.secondary{background:#d9e2ef;color:#172033}html[da
-
+