commit 6ebf8e8df714b8b378b8b0280d7d33ba13717394 Author: 변정훈 Date: Tue Jan 27 16:55:03 2026 +0900 최초 배포 diff --git a/README.md b/README.md new file mode 100644 index 0000000..1cfb293 --- /dev/null +++ b/README.md @@ -0,0 +1,107 @@ +# snStatus - Synology NAS System Monitor + +> 🖥️ **실시간 시스템 모니터링 & Docker 컨테이너 관리 웹 애플리케이션** + +Synology NAS를 위한 현대적이고 직관적인 시스템 모니터링 대시보드입니다. PWA(Progressive Web App)를 지원하여 앱처럼 설치할 수 있으며, 시스템 리소스와 Docker 컨테이너를 언제 어디서나 손쉽게 관리할 수 있습니다. + +** 해당 프로젝트는 바이브 코딩을 활용하여 제작 되었습니다.** + +![License](https://img.shields.io/badge/license-ISC-blue.svg) +![Node](https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen) +![React](https://img.shields.io/badge/react-18.2.0-61dafb) + +--- + +## ✨ 주요 기능 + +### 📊 실시간 시스템 모니터링 +- **리소스 대시보드** : CPU, 메모리, 디스크 사용량 및 네트워크 트래픽 실시간 시각화 +- **인터랙티브 차트** : 다양한 기간(1h, 6h, 12h, 24h) 선택 및 상세 툴팁 제공 +- **직관적인 UI** : 글래스모피즘(Glassmorphism) 디자인으로 세련된 정보 표시 + +### 🐳 강력한 Docker 컨테이너 관리 +- **실시간 리소스 모니터링** +- **컨테이너 제어** : 시작, 중지, 재시작, 강제 종료 등 원격 제어 지원 +- **웹 터미널** : 브라우저에서 컨테이너 내부 쉘(bash/sh) 직접 접속 +- **실시간 로그** : 컨테이너 로그 스트리밍 확인 + +### 🔔 스마트 알림 시스템 +- **임계치 알림** : CPU, 메모리, 디스크 사용률이 설정된 한계 초과 시 알림 +- **Docker 이벤트** : 컨테이너 비정상 종료 시 즉시 알림 +- **PWA 푸시 알림** : 앱을 닫아도 백그라운드에서 시스템 경고 수신 + +--- + +## 📦 설치 및 실행 + +### 사전 요구사항 +- **Docker** & **Docker Compose** +- **HTTPS 환경 (PWA 설치를 위한 필수 조건)** + +### 설치 방법 +git clone을 수행하지 않고 직접 docker-compose.yaml을 작성하여 실행하여도 됩니다. +만약 직접 작성하여 실행을 하실 경우 3 부터 시작하세요. + +1. **프로젝트 클론** + ```bash + git clone https://github.com/ElppaDev/snStatus.git + cd snStatus + ``` + +2. **Docker Compose 직접 작성** + ```yaml + services: + backend: + image: sa8001/snstatus-backend:v0.1.5 + container_name: snstatus-backend + restart: always + environment: + - NODE_ENV=production + privileged: true + volumes: + - /:/host:ro + - /proc:/host/proc:ro + - /sys:/host/sys:ro + - /var/run/docker.sock:/var/run/docker.sock + - ./data:/app/data + ports: + - "8001:8001" + + frontend: + image: sa8001/snstatus-frontend:v0.1.5 + container_name: snstatus-frontend + restart: always + ports: + - "3005:80" + depends_on: + - backend + ``` + +3. **Docker Compose 실행** + ```bash + mkdir data + docker-compose up -d + ``` + +4. **접속** + - 브라우저에서 `http://:3005` 접속 + - PWA 앱 설치 및 푸시 알림을 사용하기 위해서는 HTTPS를 통하여 접속하여야 합니다. + + +--- + +### 모바일 화면 +![홈](/img/1.jpg) +![히스토리](/img/2.jpg) +![도커잠금](/img/3.jpg) +![도커](/img/4.jpg) +![설정](/img/5.jpg) +![갤럭시](/img/6.jpg) +![아이폰](/img/7.jpg) + +### PC 화면 +![홈](/img/8.jpg) +![히스토리](/img/9.jpg) +![도커잠금](/img/10.jpg) +![도커](/img/11.jpg) +![설정](/img/12.jpg) diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..4935625 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,12 @@ +FROM node:18-alpine + +WORKDIR /app + +COPY package*.json ./ +RUN npm install + +COPY . . + +EXPOSE 8001 + +CMD ["node", "server.js"] diff --git a/backend/convert_icons.js b/backend/convert_icons.js new file mode 100644 index 0000000..81c4981 --- /dev/null +++ b/backend/convert_icons.js @@ -0,0 +1,30 @@ +const sharp = require('sharp'); +const fs = require('fs'); +const path = require('path'); + +// /app/data 디렉토리 사용 +const DATA_DIR = path.join(__dirname, 'data'); +const svgPath = path.join(DATA_DIR, 'icon.svg'); + +// icon.svg가 존재하는지 확인 +if (!fs.existsSync(svgPath)) { + console.error(`File not found: ${svgPath}`); + process.exit(1); +} + +const svgBuffer = fs.readFileSync(svgPath); + +async function convert() { + try { + console.log('Converting icons from:', svgPath); + await sharp(svgBuffer).resize(64, 64).png().toFile(path.join(DATA_DIR, 'favicon.png')); + await sharp(svgBuffer).resize(192, 192).png().toFile(path.join(DATA_DIR, 'pwa-192x192.png')); + await sharp(svgBuffer).resize(512, 512).png().toFile(path.join(DATA_DIR, 'pwa-512x512.png')); + await sharp(svgBuffer).resize(180, 180).png().toFile(path.join(DATA_DIR, 'apple-touch-icon.png')); + console.log('Conversion complete. Files saved to:', DATA_DIR); + } catch (err) { + console.error('Error converting icons:', err); + process.exit(1); + } +} +convert(); diff --git a/backend/data/apple-touch-icon.png b/backend/data/apple-touch-icon.png new file mode 100644 index 0000000..5b34bf8 Binary files /dev/null and b/backend/data/apple-touch-icon.png differ diff --git a/backend/data/config.json b/backend/data/config.json new file mode 100644 index 0000000..51839bb --- /dev/null +++ b/backend/data/config.json @@ -0,0 +1,13 @@ +{ + "enabled": true, + "passwordHash": "d7fe5080df575511eb7b3c5e50a99c0402952461f5e202e8c90461c1e52d208a9c14329317d5b39ec85de62caca369114295d559466e044d9f8b3a5e94b5ebd3", + "salt": "ced784250b4ed141397cc2115e864dac", + "retentionHours": 24, + "alertThresholds": { + "cpu": 80, + "memory": 80, + "disk": 90 + }, + "containerAlertEnabled": false, + "alertCooldownSeconds": 300 +} \ No newline at end of file diff --git a/backend/data/convert_icons_exec.cjs b/backend/data/convert_icons_exec.cjs new file mode 100644 index 0000000..af19a6d --- /dev/null +++ b/backend/data/convert_icons_exec.cjs @@ -0,0 +1,30 @@ +const sharp = require('sharp'); +const fs = require('fs'); +const path = require('path'); + +// 이미 /app/data 내에서 실행되므로 현재 디렉토리가 data 폴더임 +const DATA_DIR = __dirname; +const svgPath = path.join(DATA_DIR, 'icon.svg'); + +// icon.svg가 존재하는지 확인 +if (!fs.existsSync(svgPath)) { + console.error(`File not found: ${svgPath}`); + process.exit(1); +} + +const svgBuffer = fs.readFileSync(svgPath); + +async function convert() { + try { + console.log('Converting icons from:', svgPath); + await sharp(svgBuffer).resize(64, 64).png().toFile(path.join(DATA_DIR, 'favicon.png')); + await sharp(svgBuffer).resize(192, 192).png().toFile(path.join(DATA_DIR, 'pwa-192x192.png')); + await sharp(svgBuffer).resize(512, 512).png().toFile(path.join(DATA_DIR, 'pwa-512x512.png')); + await sharp(svgBuffer).resize(180, 180).png().toFile(path.join(DATA_DIR, 'apple-touch-icon.png')); + console.log('Conversion complete. Files saved to:', DATA_DIR); + } catch (err) { + console.error('Error converting icons:', err); + process.exit(1); + } +} +convert(); diff --git a/backend/data/favicon.png b/backend/data/favicon.png new file mode 100644 index 0000000..9a8c30a Binary files /dev/null and b/backend/data/favicon.png differ diff --git a/backend/data/history.json b/backend/data/history.json new file mode 100644 index 0000000..1bfaba7 --- /dev/null +++ b/backend/data/history.json @@ -0,0 +1 @@ +[{"timestamp":1769232137824,"cpu":0,"memory":13,"network":{"rx":0,"tx":0}},{"timestamp":1769232197803,"cpu":1,"memory":13,"network":{"rx":17526.239067055394,"tx":17622.448979591834}},{"timestamp":1769232257797,"cpu":2,"memory":13,"network":{"rx":16948.979591836734,"tx":17045.18950437318}},{"timestamp":1769274730671,"cpu":0,"memory":13,"network":{"rx":7.1127441453073414,"tx":7.266378455621464}},{"timestamp":1769274790689,"cpu":0,"memory":13,"network":{"rx":887.4174614820249,"tx":897.1019809244314}},{"timestamp":1769274850712,"cpu":0,"memory":13,"network":{"rx":882.2515584891823,"tx":891.932526585992}},{"timestamp":1769274910711,"cpu":0,"memory":13,"network":{"rx":887.0269873863303,"tx":896.7072455265474}},{"timestamp":1769274970730,"cpu":0,"memory":13,"network":{"rx":891.1155889357025,"tx":900.7876900531232}},{"timestamp":1769275030753,"cpu":0,"memory":13,"network":{"rx":886.8969057046488,"tx":896.575744244024}},{"timestamp":1769275090769,"cpu":0,"memory":13,"network":{"rx":897.4800380924474,"tx":907.1496593656142}},{"timestamp":1769275150791,"cpu":0,"memory":13,"network":{"rx":907.6574104562252,"tx":917.316064830059}},{"timestamp":1769275210808,"cpu":0,"memory":13,"network":{"rx":914.7006108489704,"tx":924.3571454698416}},{"timestamp":1769275270826,"cpu":0,"memory":13,"network":{"rx":902.1349711193975,"tx":911.7862104262631}},{"timestamp":1769275330843,"cpu":0,"memory":13,"network":{"rx":903.721830600095,"tx":913.3642572774754}},{"timestamp":1769275390864,"cpu":0,"memory":13,"network":{"rx":905.5371026024748,"tx":915.1731941453445}},{"timestamp":1769275450878,"cpu":5,"memory":16,"network":{"rx":1230115.1207764724,"tx":36951.98131795957}},{"timestamp":1769275492446,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769275493594,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769275494850,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769275496235,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769275498028,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769275500604,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769275504749,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769275512194,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769275526088,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769275552727,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769275604947,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769275666057,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769275726999,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769275787999,"cpu":0,"memory":13,"network":{"rx":0,"tx":0}},{"timestamp":1769275848920,"cpu":0,"memory":13,"network":{"rx":0,"tx":0}},{"timestamp":1769275909920,"cpu":0,"memory":13,"network":{"rx":0,"tx":0}},{"timestamp":1769275970956,"cpu":0,"memory":13,"network":{"rx":0,"tx":0}},{"timestamp":1769276031990,"cpu":0,"memory":13,"network":{"rx":0,"tx":0}},{"timestamp":1769276289922,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769276349893,"cpu":1,"memory":14,"network":{"rx":3891.0401048111216,"tx":3943.882378630177}},{"timestamp":1769276409890,"cpu":0,"memory":14,"network":{"rx":2505.7718045951374,"tx":2542.5775150568816}},{"timestamp":1769276469909,"cpu":0,"memory":14,"network":{"rx":2380.862726803179,"tx":2420.450190772922}},{"timestamp":1769276529920,"cpu":3,"memory":15,"network":{"rx":10144.390195130893,"tx":10226.15853760144}},{"timestamp":1769276543922,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769276603923,"cpu":0,"memory":14,"network":{"rx":2854.71424523742,"tx":2905.0150835847267}},{"timestamp":1769276663936,"cpu":0,"memory":14,"network":{"rx":2056.4896435653463,"tx":2087.2839979337123}},{"timestamp":1769276723928,"cpu":1,"memory":14,"network":{"rx":7216.576914071646,"tx":7293.842191524233}},{"timestamp":1769276783957,"cpu":0,"memory":14,"network":{"rx":2086.2430865596057,"tx":2117.028719930699}},{"timestamp":1769276843980,"cpu":0,"memory":14,"network":{"rx":2065.2583176449025,"tx":2096.046515502391}},{"timestamp":1769276904009,"cpu":0,"memory":14,"network":{"rx":2054.4403538289826,"tx":2085.225474354062}},{"timestamp":1769276964022,"cpu":0,"memory":13,"network":{"rx":2055.6703435864965,"tx":2086.4631585963275}},{"timestamp":1769277024051,"cpu":0,"memory":13,"network":{"rx":2041.3147416995685,"tx":2072.1008879337633}},{"timestamp":1769277060043,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769277119993,"cpu":1,"memory":14,"network":{"rx":8726.259778819369,"tx":8849.743957565344}},{"timestamp":1769277122327,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769277182299,"cpu":0,"memory":14,"network":{"rx":2220.456554001101,"tx":2254.9732370645816}},{"timestamp":1769277220513,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769277280460,"cpu":0,"memory":15,"network":{"rx":4046.659719810119,"tx":4092.50897302304}},{"timestamp":1769277340454,"cpu":0,"memory":15,"network":{"rx":4364.702697825498,"tx":4418.2982946855545}},{"timestamp":1769277400451,"cpu":0,"memory":14,"network":{"rx":2101.771755254429,"tx":2132.573295331433}},{"timestamp":1769277425190,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769277485133,"cpu":0,"memory":14,"network":{"rx":2560.590367685137,"tx":2589.0730191610564}},{"timestamp":1769277545138,"cpu":0,"memory":14,"network":{"rx":2463.8756650798096,"tx":2496.2195463455614}},{"timestamp":1769277605165,"cpu":0,"memory":14,"network":{"rx":3908.841021540307,"tx":3957.219251336898}},{"timestamp":1769277665157,"cpu":1,"memory":14,"network":{"rx":7629.432624113476,"tx":7668.439716312057}},{"timestamp":1769277725168,"cpu":0,"memory":14,"network":{"rx":2130.1562427985436,"tx":2157.533299534498}},{"timestamp":1769277785176,"cpu":0,"memory":14,"network":{"rx":2127.2584808259585,"tx":2154.636799410029}},{"timestamp":1769277845191,"cpu":0,"memory":14,"network":{"rx":2147.393648891552,"tx":2174.7707056275062}},{"timestamp":1769277905203,"cpu":0,"memory":14,"network":{"rx":2129.8802946593,"tx":2157.228360957643}},{"timestamp":1769277965227,"cpu":0,"memory":14,"network":{"rx":2124.327802546307,"tx":2151.6293606655327}},{"timestamp":1769278025242,"cpu":0,"memory":14,"network":{"rx":2156.425351853555,"tx":2183.745745561586}},{"timestamp":1769278085248,"cpu":0,"memory":14,"network":{"rx":2165.730854197349,"tx":2193.068851251841}},{"timestamp":1769278145259,"cpu":0,"memory":14,"network":{"rx":2166.3755057006256,"tx":2193.683339463038}},{"timestamp":1769278205279,"cpu":0,"memory":14,"network":{"rx":2162.3072687224667,"tx":2189.564977973568}},{"timestamp":1769278265287,"cpu":0,"memory":14,"network":{"rx":2890.8129211006863,"tx":2928.2161072980293}},{"timestamp":1769278325311,"cpu":0,"memory":14,"network":{"rx":3802.079168332667,"tx":3879.048380647741}},{"timestamp":1769278385342,"cpu":0,"memory":14,"network":{"rx":2077.6099015508653,"tx":2108.3939964351753}},{"timestamp":1769278437582,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769278497503,"cpu":4,"memory":20,"network":{"rx":15875.467289719625,"tx":16155.80774365821}},{"timestamp":1769278514492,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769278574453,"cpu":0,"memory":15,"network":{"rx":2393.2889711645903,"tx":2427.1109554543787}},{"timestamp":1769278634455,"cpu":0,"memory":15,"network":{"rx":1928.8190393653545,"tx":1957.4180860637978}},{"timestamp":1769278694487,"cpu":0,"memory":14,"network":{"rx":2036.0147257250421,"tx":2066.798820609352}},{"timestamp":1769278754508,"cpu":0,"memory":14,"network":{"rx":2030.272737875077,"tx":2061.061961646757}},{"timestamp":1769278814533,"cpu":0,"memory":14,"network":{"rx":2076.0862321737973,"tx":2101.7093162734905}},{"timestamp":1769278874552,"cpu":0,"memory":14,"network":{"rx":2050.4823311840855,"tx":2086.436413921794}},{"timestamp":1769278934584,"cpu":0,"memory":14,"network":{"rx":2047.4096285190737,"tx":2078.194236215226}},{"timestamp":1769278994606,"cpu":0,"memory":14,"network":{"rx":2055.4944688791147,"tx":2086.2821538051444}},{"timestamp":1769279054638,"cpu":0,"memory":14,"network":{"rx":2050.5730277185503,"tx":2081.3566098081023}},{"timestamp":1769279114651,"cpu":0,"memory":14,"network":{"rx":2071.4011964074452,"tx":2102.1945245196875}},{"timestamp":1769279174673,"cpu":0,"memory":14,"network":{"rx":2042.3511379160975,"tx":2073.1398487221354}},{"timestamp":1769279234696,"cpu":0,"memory":14,"network":{"rx":3868.4337670559617,"tx":3946.5038401945917}},{"timestamp":1769279294703,"cpu":3,"memory":14,"network":{"rx":14035.179815351798,"tx":14226.943972269439}},{"timestamp":1769279300955,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769279360949,"cpu":0,"memory":15,"network":{"rx":1980.6483873656139,"tx":2005.883823651971}},{"timestamp":1769279420956,"cpu":0,"memory":15,"network":{"rx":2056.844315568443,"tx":2087.6412358764123}},{"timestamp":1769304067845,"cpu":0,"memory":15,"network":{"rx":15.139030325490573,"tx":15.465724700590002}},{"timestamp":1769304127875,"cpu":0,"memory":14,"network":{"rx":2060.254210464942,"tx":2085.8751603391693}},{"timestamp":1769304187906,"cpu":0,"memory":14,"network":{"rx":2057.670203728074,"tx":2093.6182972131064}},{"timestamp":1769304249603,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769304309586,"cpu":0,"memory":15,"network":{"rx":1963.104983244694,"tx":1991.0304929894467}},{"timestamp":1769304369613,"cpu":0,"memory":15,"network":{"rx":2055.1251936628514,"tx":2088.4935112532694}},{"timestamp":1769304429628,"cpu":0,"memory":14,"network":{"rx":2054.8344635686553,"tx":2085.625739373844}},{"timestamp":1769304487720,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769304525387,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769304562281,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769304622241,"cpu":0,"memory":15,"network":{"rx":2246.201571073567,"tx":2280.024683533748}},{"timestamp":1769304682258,"cpu":0,"memory":15,"network":{"rx":2522.243327001899,"tx":2564.030790762771}},{"timestamp":1769304696312,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769304733402,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769304793353,"cpu":0,"memory":15,"network":{"rx":2312.850414891231,"tx":2345.7426926814683}},{"timestamp":1769304853381,"cpu":0,"memory":15,"network":{"rx":2037.9489571533286,"tx":2069.8340774305325}},{"timestamp":1769304885038,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769304944975,"cpu":2,"memory":15,"network":{"rx":31627.53036437247,"tx":31761.133603238864}},{"timestamp":1769339780907,"cpu":4,"memory":15,"network":{"rx":7.990280764204416,"tx":8.194001493002265}},{"timestamp":1769339840925,"cpu":0,"memory":15,"network":{"rx":2779.704029841619,"tx":2820.063596893536}},{"timestamp":1769339900949,"cpu":0,"memory":15,"network":{"rx":2384.305674061434,"tx":2419.501919795222}},{"timestamp":1769339960981,"cpu":0,"memory":15,"network":{"rx":2385.367803837953,"tx":2420.549040511727}},{"timestamp":1769340021001,"cpu":0,"memory":15,"network":{"rx":2382.1421912042833,"tx":2417.3037479022937}},{"timestamp":1769340080999,"cpu":1,"memory":14,"network":{"rx":3005.1579626047715,"tx":3047.7111540941332}},{"timestamp":1769340140994,"cpu":1,"memory":15,"network":{"rx":5995.863336908227,"tx":6076.758081814003}},{"timestamp":1769340201020,"cpu":0,"memory":15,"network":{"rx":2374.1847898421483,"tx":2409.3220113397397}},{"timestamp":1769340261049,"cpu":0,"memory":14,"network":{"rx":2370.435383919785,"tx":2405.54270060374}},{"timestamp":1769340321076,"cpu":0,"memory":14,"network":{"rx":2378.200186095972,"tx":2413.2925694536752}},{"timestamp":1769340381096,"cpu":0,"memory":14,"network":{"rx":2376.844736352275,"tx":2411.9445847847473}},{"timestamp":1769340419449,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769340479426,"cpu":0,"memory":15,"network":{"rx":3213.3116476917303,"tx":3254.59777305142}},{"timestamp":1769340539443,"cpu":0,"memory":15,"network":{"rx":2427.586458485335,"tx":2461.2943236538745}},{"timestamp":1769340599445,"cpu":0,"memory":15,"network":{"rx":2962.559715587157,"tx":2999.2223086323743}},{"timestamp":1769340659454,"cpu":0,"memory":15,"network":{"rx":2151.054562541794,"tx":2182.0679185266845}},{"timestamp":1769340684696,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769340732016,"cpu":0,"memory":16,"network":{"rx":0,"tx":0}},{"timestamp":1769340791978,"cpu":0,"memory":16,"network":{"rx":6402.012248468942,"tx":6474.19072615923}},{"timestamp":1769340852004,"cpu":0,"memory":15,"network":{"rx":2322.1877941637904,"tx":2355.840131785378}},{"timestamp":1769340912000,"cpu":0,"memory":16,"network":{"rx":8487.974398719936,"tx":8559.077953897695}},{"timestamp":1769340931295,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769340991276,"cpu":0,"memory":15,"network":{"rx":1936.9466997882662,"tx":1967.4563611810404}},{"timestamp":1769341052398,"cpu":0,"memory":15,"network":{"rx":2040.149209777167,"tx":2072.5434377147344}},{"timestamp":1769341112428,"cpu":0,"memory":15,"network":{"rx":2048.2933248929685,"tx":2079.078445418048}},{"timestamp":1769341173488,"cpu":0,"memory":15,"network":{"rx":2074.6654874793235,"tx":2099.7559737303263}},{"timestamp":1769343788695,"cpu":0,"memory":15,"network":{"rx":93.54705247154337,"tx":95.68531451418013}},{"timestamp":1769343848723,"cpu":0,"memory":15,"network":{"rx":2038.3654294662492,"tx":2069.1510628373426}},{"timestamp":1769343908752,"cpu":0,"memory":15,"network":{"rx":2044.4285262123306,"tx":2075.2136467374103}},{"timestamp":1769343968776,"cpu":0,"memory":15,"network":{"rx":2027.9055044648808,"tx":2058.6931893909104}},{"timestamp":1769344028802,"cpu":0,"memory":15,"network":{"rx":2043.2645853463498,"tx":2074.0512444607334}},{"timestamp":1769344088828,"cpu":0,"memory":15,"network":{"rx":2042.6974528129008,"tx":2073.4835990470956}},{"timestamp":1769344148854,"cpu":0,"memory":15,"network":{"rx":2039.966680549771,"tx":2070.7538525614327}},{"timestamp":1769344208862,"cpu":1,"memory":15,"network":{"rx":12919.177443007598,"tx":13107.63564858019}},{"timestamp":1769344218270,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769344278213,"cpu":3,"memory":16,"network":{"rx":15367.766044408856,"tx":15546.769430959412}},{"timestamp":1769344290057,"cpu":0,"memory":16,"network":{"rx":0,"tx":0}},{"timestamp":1769344350054,"cpu":0,"memory":16,"network":{"rx":465.16550551685054,"tx":476.36587886262873}},{"timestamp":1769344410073,"cpu":0,"memory":16,"network":{"rx":465.13604025391965,"tx":469.5346473616688}},{"timestamp":1769344470108,"cpu":0,"memory":14,"network":{"rx":466.352400306493,"tx":470.7499083852484}},{"timestamp":1769344530092,"cpu":0,"memory":14,"network":{"rx":1511.0692722196975,"tx":1528.208790495358}},{"timestamp":1769344590116,"cpu":0,"memory":14,"network":{"rx":529.0457256461233,"tx":534.2942345924454}},{"timestamp":1769344650143,"cpu":0,"memory":14,"network":{"rx":996.0850298204111,"tx":1000.4831239796088}},{"timestamp":1769344680889,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769344740867,"cpu":0,"memory":15,"network":{"rx":475.44974074259324,"tx":480.21807632671437}},{"timestamp":1769344800894,"cpu":0,"memory":15,"network":{"rx":473.47027171106333,"tx":477.8682926016626}},{"timestamp":1769344860892,"cpu":1,"memory":15,"network":{"rx":8793.404461687682,"tx":8889.427740058196}},{"timestamp":1769344920933,"cpu":0,"memory":14,"network":{"rx":490.5561951359908,"tx":495.3050798676068}},{"timestamp":1769344946925,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769344993180,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769345053167,"cpu":0,"memory":15,"network":{"rx":458.364591147787,"tx":465.76644161040264}},{"timestamp":1769345113186,"cpu":0,"memory":15,"network":{"rx":469.92669110296566,"tx":474.32522492502494}},{"timestamp":1769345173213,"cpu":0,"memory":15,"network":{"rx":467.1064687557266,"tx":471.50448964632585}},{"timestamp":1769345233233,"cpu":0,"memory":14,"network":{"rx":475.2582472509164,"tx":479.6567810729756}},{"timestamp":1769345293272,"cpu":0,"memory":14,"network":{"rx":474.70810639750823,"tx":479.1052482553007}},{"timestamp":1769345353290,"cpu":0,"memory":14,"network":{"rx":467.184297782295,"tx":471.5830514687505}},{"timestamp":1769345413312,"cpu":0,"memory":14,"network":{"rx":471.5359112340269,"tx":475.93422521366807}},{"timestamp":1769345473337,"cpu":0,"memory":14,"network":{"rx":468.22157434402334,"tx":472.6197417742607}},{"timestamp":1769345533362,"cpu":0,"memory":14,"network":{"rx":624.6230911389943,"tx":631.0427001264468}},{"timestamp":1769345593386,"cpu":0,"memory":14,"network":{"rx":2582.9834732773556,"tx":2646.757963481274}},{"timestamp":1769345653411,"cpu":0,"memory":14,"network":{"rx":469.7542690545606,"tx":474.15243648479805}},{"timestamp":1769345713435,"cpu":0,"memory":14,"network":{"rx":469.76209516193524,"tx":474.16033586565374}},{"timestamp":1769345773463,"cpu":0,"memory":14,"network":{"rx":475.92790031318714,"tx":480.3258479376291}},{"timestamp":1769345833488,"cpu":0,"memory":14,"network":{"rx":466.45564348188253,"tx":470.85381091211997}},{"timestamp":1769345893517,"cpu":0,"memory":14,"network":{"rx":469.3153423288355,"tx":473.71314342828583}},{"timestamp":1769345953547,"cpu":0,"memory":14,"network":{"rx":472.9292996601586,"tx":477.3272472846005}},{"timestamp":1769346013565,"cpu":0,"memory":14,"network":{"rx":470.10113464069707,"tx":474.49974174844635}},{"timestamp":1769346073590,"cpu":0,"memory":14,"network":{"rx":468.2548937942524,"tx":472.6530612244898}},{"timestamp":1769346133617,"cpu":0,"memory":14,"network":{"rx":468.2392923184567,"tx":472.63731320905595}},{"timestamp":1769346193643,"cpu":0,"memory":14,"network":{"rx":469.74644320794323,"tx":474.14453736714086}},{"timestamp":1769346253671,"cpu":0,"memory":14,"network":{"rx":466.7477426448539,"tx":471.14583680405156}},{"timestamp":1769346313673,"cpu":3,"memory":15,"network":{"rx":30155.572961802543,"tx":30314.345710285983}},{"timestamp":1769346317787,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769346377782,"cpu":0,"memory":15,"network":{"rx":481.931526485982,"tx":485.59855985598557}},{"timestamp":1769346437791,"cpu":0,"memory":15,"network":{"rx":465.61349130963686,"tx":470.01283140862205}},{"timestamp":1769346497806,"cpu":0,"memory":15,"network":{"rx":971.3904857119054,"tx":975.7893859868366}},{"timestamp":1769346557816,"cpu":0,"memory":14,"network":{"rx":474.9454248529387,"tx":479.34476495192393}},{"timestamp":1769346617805,"cpu":0,"memory":14,"network":{"rx":472.18749479088535,"tx":476.5881548899001}},{"timestamp":1769346677808,"cpu":3,"memory":15,"network":{"rx":25012.54937253137,"tx":25168.341582920853}},{"timestamp":1769346680555,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769346740543,"cpu":0,"memory":15,"network":{"rx":471.5443088617723,"tx":476.745349069814}},{"timestamp":1769346800558,"cpu":0,"memory":15,"network":{"rx":455.6694159793385,"tx":460.0683162542698}},{"timestamp":1769346860583,"cpu":0,"memory":15,"network":{"rx":465.71485689534535,"tx":470.11295105454303}},{"timestamp":1769346920609,"cpu":0,"memory":14,"network":{"rx":471.22817539650805,"tx":475.6264161002266}},{"timestamp":1769346980633,"cpu":0,"memory":14,"network":{"rx":475.61849229487717,"tx":478.9171178675552}},{"timestamp":1769347040660,"cpu":0,"memory":14,"network":{"rx":464.6075932497043,"tx":469.0056141403035}},{"timestamp":1769347100683,"cpu":0,"memory":14,"network":{"rx":473.7350682238475,"tx":478.1333822034886}},{"timestamp":1769347160709,"cpu":0,"memory":14,"network":{"rx":468.22937491669995,"tx":472.6276156204185}},{"timestamp":1769347220735,"cpu":0,"memory":14,"network":{"rx":467.8139472895079,"tx":472.2120414487056}},{"timestamp":1769347280754,"cpu":0,"memory":14,"network":{"rx":476.0167274787158,"tx":479.2489295413272}},{"timestamp":1769347340788,"cpu":0,"memory":14,"network":{"rx":473.9814105340307,"tx":478.37891861278615}},{"timestamp":1769347400810,"cpu":0,"memory":14,"network":{"rx":2622.132253711201,"tx":2690.308392062778}},{"timestamp":1769347460828,"cpu":0,"memory":14,"network":{"rx":473.8078576427072,"tx":478.2065380385884}},{"timestamp":1769347520849,"cpu":0,"memory":14,"network":{"rx":472.2847003548758,"tx":476.68316089368716}},{"timestamp":1769347580879,"cpu":0,"memory":14,"network":{"rx":464.9175412293853,"tx":469.3153423288356}},{"timestamp":1769347640905,"cpu":0,"memory":14,"network":{"rx":472.64518708559626,"tx":477.0432812447939}},{"timestamp":1769347700906,"cpu":2,"memory":20,"network":{"rx":22155.544815172827,"tx":22203.226559114693}},{"timestamp":1769347720239,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769347780234,"cpu":0,"memory":15,"network":{"rx":478.2057906755788,"tx":484.5065257613388}},{"timestamp":1769347840243,"cpu":0,"memory":15,"network":{"rx":456.8148111116666,"tx":461.2141512106517}},{"timestamp":1769347900268,"cpu":0,"memory":15,"network":{"rx":472.60399486897563,"tx":477.0020157595749}},{"timestamp":1769347960295,"cpu":0,"memory":14,"network":{"rx":464.91520341185486,"tx":469.3132975710525}},{"timestamp":1769348020319,"cpu":0,"memory":14,"network":{"rx":465.7148568953453,"tx":470.11295105454303}},{"timestamp":1769348080343,"cpu":0,"memory":14,"network":{"rx":471.9278955084633,"tx":476.3261362121818}},{"timestamp":1769348140368,"cpu":0,"memory":14,"network":{"rx":466.8299346927895,"tx":471.22817539650805}},{"timestamp":1769348200368,"cpu":0,"memory":14,"network":{"rx":470.232341078036,"tx":474.6324877495917}},{"timestamp":1769348260376,"cpu":0,"memory":14,"network":{"rx":1004.4825862356274,"tx":1008.881853024496}},{"timestamp":1769348320374,"cpu":0,"memory":14,"network":{"rx":471.73239107970267,"tx":476.1325377512584}},{"timestamp":1769348380369,"cpu":0,"memory":14,"network":{"rx":466.6966696669667,"tx":471.09710971097115}},{"timestamp":1769348440361,"cpu":0,"memory":14,"network":{"rx":470.84611281504203,"tx":475.24669955994136}},{"timestamp":1769348500364,"cpu":0,"memory":14,"network":{"rx":706.2215629739505,"tx":713.921434642756}},{"timestamp":1769348560352,"cpu":2,"memory":20,"network":{"rx":9038.88851846913,"tx":9114.081877583678}},{"timestamp":1769348580259,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769348631963,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769348691959,"cpu":0,"memory":15,"network":{"rx":482.67297851416,"tx":488.5736669278082}},{"timestamp":1769348751926,"cpu":3,"memory":15,"network":{"rx":8213.730656350053,"tx":8268.59324973319}},{"timestamp":1769348765691,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769348825659,"cpu":0,"memory":15,"network":{"rx":477.0457084729935,"tx":482.24857004685913}},{"timestamp":1769348885653,"cpu":1,"memory":15,"network":{"rx":15642.611683848798,"tx":15812.714776632303}},{"timestamp":1769348945643,"cpu":1,"memory":15,"network":{"rx":10519.658119658121,"tx":10632.478632478635}},{"timestamp":1769349005668,"cpu":0,"memory":14,"network":{"rx":654.1758073567676,"tx":660.9101576450181}},{"timestamp":1769349065694,"cpu":0,"memory":14,"network":{"rx":466.8066036950039,"tx":471.20462458560314}},{"timestamp":1769349093262,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769349153258,"cpu":0,"memory":15,"network":{"rx":2744.207754108744,"tx":2816.5149848318165}},{"timestamp":1769349213276,"cpu":0,"memory":15,"network":{"rx":469.23474233159504,"tx":473.6333494393442}},{"timestamp":1769349273308,"cpu":0,"memory":15,"network":{"rx":470.3893118326142,"tx":474.78718619333983}},{"timestamp":1769349333299,"cpu":2,"memory":14,"network":{"rx":15914.565826330534,"tx":16007.002801120449}},{"timestamp":1769349393338,"cpu":0,"memory":14,"network":{"rx":525.8427412154521,"tx":531.0807325251483}},{"timestamp":1769349453321,"cpu":1,"memory":14,"network":{"rx":9842.076798269334,"tx":9949.161709031909}},{"timestamp":1769349513316,"cpu":0,"memory":14,"network":{"rx":4741.532047941636,"tx":4793.121417404898}},{"timestamp":1769349573325,"cpu":0,"memory":14,"network":{"rx":1255.578523347644,"tx":1269.5712089892404}},{"timestamp":1769349633347,"cpu":0,"memory":14,"network":{"rx":538.2811560508278,"tx":543.5723734316751}},{"timestamp":1769349693342,"cpu":0,"memory":14,"network":{"rx":534.8235765838011,"tx":540.1162790697674}},{"timestamp":1769349753351,"cpu":3,"memory":15,"network":{"rx":32161.117121448893,"tx":32347.07697239251}},{"timestamp":1769349760861,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769349820864,"cpu":0,"memory":15,"network":{"rx":491.43704220439486,"tx":498.0292989187303}},{"timestamp":1769349880872,"cpu":0,"memory":15,"network":{"rx":480.1792064709574,"tx":484.78139599748977}},{"timestamp":1769349940891,"cpu":0,"memory":15,"network":{"rx":477.0193044811485,"tx":481.61892814830304}},{"timestamp":1769350000893,"cpu":1,"memory":14,"network":{"rx":11119.801980198019,"tx":11185.148514851484}},{"timestamp":1769350060884,"cpu":1,"memory":14,"network":{"rx":11431.166347992352,"tx":11494.263862332697}},{"timestamp":1769350120888,"cpu":0,"memory":14,"network":{"rx":1995.3606073386757,"tx":2016.2378743146353}},{"timestamp":1769350180915,"cpu":0,"memory":14,"network":{"rx":470.41282111085195,"tx":474.8109152700496}},{"timestamp":1769350240941,"cpu":0,"memory":14,"network":{"rx":587.052043542582,"tx":592.8812735984455}},{"timestamp":1769350271656,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769350331628,"cpu":0,"memory":15,"network":{"rx":467.10131394650836,"tx":472.30374174614815}},{"timestamp":1769350391621,"cpu":1,"memory":15,"network":{"rx":16216.740088105726,"tx":16391.189427312776}},{"timestamp":1769350451634,"cpu":0,"memory":15,"network":{"rx":860.9226835245232,"tx":870.2986823880385}},{"timestamp":1769350511657,"cpu":0,"memory":14,"network":{"rx":1364.9406918104978,"tx":1383.6920235812204}},{"timestamp":1769350558474,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769350618439,"cpu":1,"memory":15,"network":{"rx":6164.08876933423,"tx":6230.665770006724}},{"timestamp":1769350678431,"cpu":1,"memory":15,"network":{"rx":6154.362416107382,"tx":6220.805369127516}},{"timestamp":1769350738427,"cpu":0,"memory":15,"network":{"rx":2275.316852804618,"tx":2300.163132137031}},{"timestamp":1769350798433,"cpu":0,"memory":14,"network":{"rx":1576.782023071281,"tx":1594.3855437754219}},{"timestamp":1769350858437,"cpu":1,"memory":14,"network":{"rx":6121.992544900034,"tx":6189.088444595052}},{"timestamp":1769350911236,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769350971204,"cpu":0,"memory":15,"network":{"rx":2404.619776797301,"tx":2430.3140410070073}},{"timestamp":1769351031208,"cpu":0,"memory":15,"network":{"rx":6317.373461012312,"tx":6385.088919288646}},{"timestamp":1769351091228,"cpu":0,"memory":15,"network":{"rx":580.332894583382,"tx":586.9308052450058}},{"timestamp":1769351151252,"cpu":0,"memory":14,"network":{"rx":466.43009462881514,"tx":470.8283353325337}},{"timestamp":1769351211289,"cpu":0,"memory":14,"network":{"rx":464.89664706764165,"tx":468.12798774089316}},{"timestamp":1769351271280,"cpu":3,"memory":14,"network":{"rx":11804.87398319776,"tx":11969.49593279104}},{"timestamp":1769351273778,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769351333734,"cpu":0,"memory":15,"network":{"rx":592.0372285418821,"tx":600.5437502084932}},{"timestamp":1769351393733,"cpu":0,"memory":15,"network":{"rx":5673.218673218674,"tx":5734.029484029485}},{"timestamp":1769351453728,"cpu":0,"memory":15,"network":{"rx":1801.2895662368112,"tx":1820.63305978898}},{"timestamp":1769351513730,"cpu":0,"memory":14,"network":{"rx":1806.0239789453162,"tx":1825.3241056633199}},{"timestamp":1769351573732,"cpu":0,"memory":14,"network":{"rx":1830.6774855802132,"tx":1850.034216443445}},{"timestamp":1769351633730,"cpu":0,"memory":14,"network":{"rx":1861.3010353584687,"tx":1880.6407501465133}},{"timestamp":1769351693730,"cpu":0,"memory":14,"network":{"rx":1872.032822115854,"tx":1891.374426101397}},{"timestamp":1769351753731,"cpu":0,"memory":14,"network":{"rx":1848.046875,"tx":1867.3828125}},{"timestamp":1769351813728,"cpu":0,"memory":14,"network":{"rx":1821.9097832454597,"tx":1841.24194493263}},{"timestamp":1769351873732,"cpu":0,"memory":14,"network":{"rx":1875.146771037182,"tx":1894.5205479452054}},{"timestamp":1769351933735,"cpu":0,"memory":14,"network":{"rx":1839.6632732967894,"tx":1859.0446358653094}},{"timestamp":1769351993729,"cpu":0,"memory":14,"network":{"rx":1869.403714565005,"tx":1888.758553274682}},{"timestamp":1769352053731,"cpu":0,"memory":14,"network":{"rx":1845.9430361162767,"tx":1865.3225017128316}},{"timestamp":1769352113734,"cpu":0,"memory":14,"network":{"rx":1917.2682926829268,"tx":1930.1463414634147}},{"timestamp":1769352173737,"cpu":0,"memory":14,"network":{"rx":1933.002239750706,"tx":1952.2835719154737}},{"timestamp":1769352233737,"cpu":0,"memory":14,"network":{"rx":1903.0905722920932,"tx":1922.3944623184168}},{"timestamp":1769352293731,"cpu":0,"memory":14,"network":{"rx":1919.328059380799,"tx":1938.665885340365}},{"timestamp":1769352353737,"cpu":0,"memory":14,"network":{"rx":1919.0672153635119,"tx":1938.467568097198}},{"timestamp":1769352413740,"cpu":0,"memory":14,"network":{"rx":1937.4695626765365,"tx":1956.7546508230255}},{"timestamp":1769352473738,"cpu":0,"memory":14,"network":{"rx":1934.320795166634,"tx":1953.6152796725783}},{"timestamp":1769352533738,"cpu":0,"memory":14,"network":{"rx":1963.8318670576734,"tx":1983.186705767351}},{"timestamp":1769352576211,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769352618522,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769352678509,"cpu":0,"memory":15,"network":{"rx":759.611816754674,"tx":767.1471385757101}},{"timestamp":1769352738526,"cpu":0,"memory":15,"network":{"rx":739.1515359141258,"tx":746.6883635948384}},{"timestamp":1769352798551,"cpu":0,"memory":15,"network":{"rx":721.6659535016403,"tx":729.1969761802881}},{"timestamp":1769352858555,"cpu":1,"memory":14,"network":{"rx":5865.446371226719,"tx":5929.03018625562}},{"timestamp":1769352918551,"cpu":0,"memory":14,"network":{"rx":1754.8179871520342,"tx":1775.0076475986539}},{"timestamp":1769352978556,"cpu":0,"memory":14,"network":{"rx":3466.5432274604764,"tx":3513.5450790485693}},{"timestamp":1769353006454,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769353066411,"cpu":1,"memory":15,"network":{"rx":6359.189378057302,"tx":6428.371767994409}},{"timestamp":1769353126409,"cpu":1,"memory":15,"network":{"rx":6247.793618465716,"tx":6315.00339443313}},{"timestamp":1769353186406,"cpu":1,"memory":15,"network":{"rx":6312.776831345826,"tx":6380.238500851789}},{"timestamp":1769353246392,"cpu":1,"memory":14,"network":{"rx":6271.878242822552,"tx":6340.3666551366305}},{"timestamp":1769353306389,"cpu":1,"memory":14,"network":{"rx":6462.173314993122,"tx":6530.261348005502}},{"timestamp":1769353366381,"cpu":1,"memory":14,"network":{"rx":6416.149068322981,"tx":6484.472049689441}},{"timestamp":1769353426380,"cpu":1,"memory":14,"network":{"rx":6437.804030576789,"tx":6506.601806810285}},{"timestamp":1769383077987,"cpu":1,"memory":14,"network":{"rx":11.457266792927156,"tx":11.778134139499038}},{"timestamp":1769474461904,"cpu":6,"memory":11,"network":{"rx":0,"tx":0}},{"timestamp":1769474486342,"cpu":6,"memory":13,"network":{"rx":0,"tx":0}},{"timestamp":1769474546315,"cpu":0,"memory":13,"network":{"rx":6010.301109350237,"tx":6114.896988906497}},{"timestamp":1769474606334,"cpu":0,"memory":13,"network":{"rx":4474.491836054648,"tx":4534.905031656114}},{"timestamp":1769474666359,"cpu":0,"memory":13,"network":{"rx":2048.6630570595585,"tx":2080.5497709287797}},{"timestamp":1769474726388,"cpu":0,"memory":13,"network":{"rx":2057.888687134551,"tx":2089.7732762498126}},{"timestamp":1769474786400,"cpu":0,"memory":12,"network":{"rx":2049.9900019996003,"tx":2080.7171898953543}},{"timestamp":1769474846427,"cpu":0,"memory":12,"network":{"rx":2052.0099288653437,"tx":2083.8955803221884}},{"timestamp":1769474906454,"cpu":0,"memory":12,"network":{"rx":2053.82577839972,"tx":2085.7114298565643}},{"timestamp":1769474966478,"cpu":0,"memory":12,"network":{"rx":2060.8923097427696,"tx":2092.7795548447284}},{"timestamp":1769475026484,"cpu":0,"memory":12,"network":{"rx":2046.1953804619538,"tx":2076.9256407692565}},{"timestamp":1769475086507,"cpu":0,"memory":12,"network":{"rx":2049.664295353448,"tx":2081.552071705846}},{"timestamp":1769475146539,"cpu":0,"memory":12,"network":{"rx":2034.4816098081023,"tx":2066.3646055437102}},{"timestamp":1769475206570,"cpu":0,"memory":12,"network":{"rx":2048.3750062467725,"tx":2080.258533091236}},{"timestamp":1769475266568,"cpu":0,"memory":12,"network":{"rx":2062.50208340278,"tx":2094.403146771559}},{"timestamp":1769475326598,"cpu":0,"memory":12,"network":{"rx":2056.3727531693016,"tx":2088.2573422845626}},{"timestamp":1769475386625,"cpu":0,"memory":12,"network":{"rx":2557.5064969680816,"tx":2589.3916172452855}},{"timestamp":1769475446653,"cpu":0,"memory":12,"network":{"rx":2046.0784967015393,"tx":2077.963616978743}},{"timestamp":1769475506681,"cpu":0,"memory":12,"network":{"rx":2050.9604011528145,"tx":2082.8460526096587}},{"timestamp":1769475566702,"cpu":0,"memory":12,"network":{"rx":2054.38006064443,"tx":2085.1021292192863}},{"timestamp":1769475626707,"cpu":0,"memory":12,"network":{"rx":2052.345637863511,"tx":2084.242979751687}},{"timestamp":1769475686730,"cpu":0,"memory":12,"network":{"rx":4263.3823700914645,"tx":4321.66003032171}},{"timestamp":1769475746758,"cpu":0,"memory":12,"network":{"rx":2060.3718264809754,"tx":2092.2569467581793}},{"timestamp":1769475806787,"cpu":0,"memory":12,"network":{"rx":2065.534991420813,"tx":2097.419580536074}},{"timestamp":1769475866808,"cpu":0,"memory":12,"network":{"rx":2072.258043018277,"tx":2104.1468819246597}},{"timestamp":1769475926834,"cpu":0,"memory":12,"network":{"rx":2057.7759266972093,"tx":2089.662640566431}},{"timestamp":1769475986858,"cpu":0,"memory":12,"network":{"rx":2063.723448563099,"tx":2095.61016243232}},{"timestamp":1769476046885,"cpu":0,"memory":12,"network":{"rx":2069.1533668743546,"tx":2101.0395495285375}},{"timestamp":1769476106910,"cpu":0,"memory":12,"network":{"rx":2064.7885916103023,"tx":2096.674774264485}},{"timestamp":1769476166937,"cpu":0,"memory":12,"network":{"rx":2062.1720225898343,"tx":2094.057674046679}},{"timestamp":1769476226963,"cpu":0,"memory":12,"network":{"rx":2063.7067888379843,"tx":2095.5935027072055}},{"timestamp":1769476286989,"cpu":0,"memory":12,"network":{"rx":4575.05789061589,"tx":4683.90890765822}},{"timestamp":1769476347014,"cpu":0,"memory":12,"network":{"rx":2060.491461890879,"tx":2092.3781757601}},{"timestamp":1769476407041,"cpu":0,"memory":12,"network":{"rx":2054.9586019624503,"tx":2086.8442534192945}},{"timestamp":1769476467071,"cpu":0,"memory":12,"network":{"rx":2058.854886804711,"tx":2090.739475919972}},{"timestamp":1769476527075,"cpu":0,"memory":12,"network":{"rx":2058.9950837430215,"tx":2090.8924256311975}},{"timestamp":1769476587104,"cpu":0,"memory":12,"network":{"rx":2063.3027370104446,"tx":2095.1873261257056}},{"timestamp":1769476647131,"cpu":0,"memory":12,"network":{"rx":2063.8056842035116,"tx":2095.691866857695}},{"timestamp":1769476707159,"cpu":0,"memory":12,"network":{"rx":2069.21769840741,"tx":2101.102818684614}},{"timestamp":1769476767164,"cpu":0,"memory":12,"network":{"rx":4360.603283059745,"tx":4416.631947337722}},{"timestamp":1769476827193,"cpu":0,"memory":12,"network":{"rx":2062.0699995002415,"tx":2093.9545886155024}},{"timestamp":1769476887217,"cpu":0,"memory":12,"network":{"rx":2041.4160766347354,"tx":2073.3027905039567}},{"timestamp":1769476947250,"cpu":0,"memory":12,"network":{"rx":2052.804290973298,"tx":2084.6867556177435}},{"timestamp":1769477007273,"cpu":0,"memory":12,"network":{"rx":2052.1308853420414,"tx":2084.0191929625803}},{"timestamp":1769477067298,"cpu":0,"memory":12,"network":{"rx":2062.9893712724484,"tx":2094.8755539266317}},{"timestamp":1769477127328,"cpu":0,"memory":12,"network":{"rx":2080.2112312382346,"tx":2112.0958203534956}},{"timestamp":1769477187352,"cpu":0,"memory":12,"network":{"rx":2568.213244481466,"tx":2600.0999583506873}},{"timestamp":1769477247379,"cpu":0,"memory":12,"network":{"rx":2056.3246539057427,"tx":2088.2103053625865}},{"timestamp":1769477307421,"cpu":0,"memory":12,"network":{"rx":2050.364744678725,"tx":2082.2424302987906}},{"timestamp":1769477367433,"cpu":0,"memory":12,"network":{"rx":2081.25041658335,"tx":2113.144037859095}},{"timestamp":1769477427469,"cpu":0,"memory":12,"network":{"rx":2052.0029982510205,"tx":2083.8844007662196}},{"timestamp":1769477487476,"cpu":0,"memory":12,"network":{"rx":4192.307692307692,"tx":4247.300359952006}},{"timestamp":1769477547500,"cpu":0,"memory":12,"network":{"rx":2051.08041917265,"tx":2082.9681955250485}},{"timestamp":1769477607529,"cpu":0,"memory":12,"network":{"rx":2076.612970397641,"tx":2108.497559512902}},{"timestamp":1769477667552,"cpu":0,"memory":12,"network":{"rx":2055.877648940424,"tx":2087.7648940423833}},{"timestamp":1769477727579,"cpu":0,"memory":12,"network":{"rx":2065.0707181768203,"tx":2096.9563696336645}},{"timestamp":1769477787606,"cpu":0,"memory":12,"network":{"rx":2053.6267617365806,"tx":2085.512944390764}},{"timestamp":1769477847634,"cpu":0,"memory":12,"network":{"rx":2059.055773972146,"tx":2090.94089424935}},{"timestamp":1769477907661,"cpu":0,"memory":12,"network":{"rx":2055.3908176184445,"tx":2087.275937895649}},{"timestamp":1769477967691,"cpu":0,"memory":12,"network":{"rx":2071.182261906745,"tx":2103.066851022006}},{"timestamp":1769478027718,"cpu":0,"memory":12,"network":{"rx":2061.304724461918,"tx":2093.189844739122}},{"timestamp":1769478087748,"cpu":0,"memory":12,"network":{"rx":4571.207249829249,"tx":4678.955171667028}},{"timestamp":1769478147770,"cpu":0,"memory":12,"network":{"rx":2050.530629925195,"tx":2082.4184062775935}},{"timestamp":1769478207802,"cpu":0,"memory":12,"network":{"rx":2061.217350746269,"tx":2093.1003464818764}},{"timestamp":1769478267829,"cpu":0,"memory":12,"network":{"rx":2058.7911904841235,"tx":2090.6773731383064}},{"timestamp":1769478327855,"cpu":0,"memory":12,"network":{"rx":2060.339513885418,"tx":2092.2251653422627}},{"timestamp":1769478387879,"cpu":0,"memory":12,"network":{"rx":2048.330667732907,"tx":2080.217912834866}},{"timestamp":1769478447882,"cpu":0,"memory":12,"network":{"rx":4369.431528423578,"tx":4427.728613569322}},{"timestamp":1769478507909,"cpu":0,"memory":12,"network":{"rx":2064.837489796258,"tx":2096.7231412531028}},{"timestamp":1769478567926,"cpu":0,"memory":12,"network":{"rx":2055.3176599963344,"tx":2087.2086242231367}},{"timestamp":1769478627954,"cpu":0,"memory":12,"network":{"rx":2046.778170187246,"tx":2078.66329046445}},{"timestamp":1769478687960,"cpu":0,"memory":12,"network":{"rx":2054.112157320223,"tx":2086.009499208399}},{"timestamp":1769478747982,"cpu":0,"memory":12,"network":{"rx":2042.533695416757,"tx":2074.421471769155}},{"timestamp":1769478808007,"cpu":0,"memory":12,"network":{"rx":2058.9587671803415,"tx":2090.8454810495627}},{"timestamp":1769478868039,"cpu":0,"memory":12,"network":{"rx":2060.9841417910447,"tx":2091.701092750533}},{"timestamp":1769478928063,"cpu":0,"memory":12,"network":{"rx":2071.1393965646503,"tx":2103.027172917048}},{"timestamp":1769478988090,"cpu":0,"memory":12,"network":{"rx":4551.092823349104,"tx":4658.842540147931}},{"timestamp":1769479048120,"cpu":0,"memory":12,"network":{"rx":2052.173913043478,"tx":2084.0579710144925}},{"timestamp":1769479108145,"cpu":0,"memory":12,"network":{"rx":2057.2761349437733,"tx":2089.1628488129945}},{"timestamp":1769479168174,"cpu":0,"memory":12,"network":{"rx":2046.860684002732,"tx":2078.745273117993}},{"timestamp":1769479228204,"cpu":0,"memory":12,"network":{"rx":2044.6276861569213,"tx":2076.511744127936}},{"timestamp":1769479288231,"cpu":0,"memory":12,"network":{"rx":2044.6798940476785,"tx":2076.565545504523}},{"timestamp":1769479348255,"cpu":0,"memory":12,"network":{"rx":2050.9629481540715,"tx":2082.850193256031}},{"timestamp":1769479408285,"cpu":0,"memory":12,"network":{"rx":2069.5330590214726,"tx":2101.417648136734}},{"timestamp":1769479468313,"cpu":0,"memory":12,"network":{"rx":2058.9381798797244,"tx":2090.822768994986}},{"timestamp":1769479528310,"cpu":1,"memory":19,"network":{"rx":44763.250883392226,"tx":44849.6399759984}},{"timestamp":1769479542955,"cpu":0,"memory":13,"network":{"rx":0,"tx":0}},{"timestamp":1769479602957,"cpu":0,"memory":13,"network":{"rx":2080.9306356454786,"tx":2113.6295456818107}},{"timestamp":1769479662941,"cpu":0,"memory":13,"network":{"rx":3881.749973581317,"tx":3930.5717003064574}},{"timestamp":1769479722944,"cpu":0,"memory":13,"network":{"rx":2437.2875141657223,"tx":2475.7849476701554}},{"timestamp":1769479782967,"cpu":0,"memory":13,"network":{"rx":2144.6802838959047,"tx":2178.767785145447}},{"timestamp":1769479842997,"cpu":0,"memory":13,"network":{"rx":2040.2791890856392,"tx":2072.162715930103}},{"timestamp":1769479903022,"cpu":0,"memory":13,"network":{"rx":4594.08579758434,"tx":4705.139525197834}},{"timestamp":1769479963030,"cpu":0,"memory":13,"network":{"rx":2802.0097320357286,"tx":2848.2035728569526}},{"timestamp":1769480023056,"cpu":0,"memory":13,"network":{"rx":2052.661391087047,"tx":2084.548104956268}},{"timestamp":1769480083057,"cpu":0,"memory":13,"network":{"rx":5416.745570133911,"tx":5503.0434194508325}},{"timestamp":1769480143050,"cpu":0,"memory":13,"network":{"rx":4776.573933625589,"tx":4860.183688096945}},{"timestamp":1769480203076,"cpu":0,"memory":13,"network":{"rx":3509.42107455227,"tx":3568.796334860475}},{"timestamp":1769480225862,"cpu":0,"memory":13,"network":{"rx":0,"tx":0}},{"timestamp":1769480285844,"cpu":0,"memory":13,"network":{"rx":2191.97425894435,"tx":2229.085392284352}},{"timestamp":1769480345866,"cpu":0,"memory":13,"network":{"rx":2308.1536769851054,"tx":2344.44037186365}},{"timestamp":1769480405892,"cpu":0,"memory":13,"network":{"rx":2295.938426681771,"tx":2332.222703495152}},{"timestamp":1769480465918,"cpu":0,"memory":13,"network":{"rx":2309.1160497117917,"tx":2345.400326525172}},{"timestamp":1769480525945,"cpu":0,"memory":13,"network":{"rx":3567.7611741383043,"tx":3626.0349509387443}},{"timestamp":1769480585978,"cpu":0,"memory":13,"network":{"rx":2065.830459913714,"tx":2097.7129245581596}},{"timestamp":1769480646003,"cpu":0,"memory":13,"network":{"rx":2068.9712619741777,"tx":2100.8579758433984}},{"timestamp":1769480706029,"cpu":0,"memory":13,"network":{"rx":2072.668510312198,"tx":2104.554692966381}},{"timestamp":1769480766060,"cpu":0,"memory":13,"network":{"rx":2432.5265279605537,"tx":2462.2111908847096}},{"timestamp":1769480826084,"cpu":0,"memory":13,"network":{"rx":2208.9997334399573,"tx":2243.0860988937757}},{"timestamp":1769480886116,"cpu":0,"memory":13,"network":{"rx":1918.5420865885958,"tx":1948.2267495127517}},{"timestamp":1769480946140,"cpu":0,"memory":13,"network":{"rx":2078.401972544316,"tx":2110.2892176462747}},{"timestamp":1769481006166,"cpu":0,"memory":13,"network":{"rx":2063.3548236626852,"tx":2095.2404751195295}},{"timestamp":1769481066194,"cpu":0,"memory":13,"network":{"rx":2062.220963550343,"tx":2094.106083827547}},{"timestamp":1769481126222,"cpu":0,"memory":13,"network":{"rx":2060.705004331312,"tx":2092.590124608516}},{"timestamp":1769481186229,"cpu":0,"memory":13,"network":{"rx":154340.26597340268,"tx":6771.672832716728}},{"timestamp":1769481246254,"cpu":0,"memory":13,"network":{"rx":6104.506455643482,"tx":6201.266139108705}},{"timestamp":1769481306282,"cpu":0,"memory":13,"network":{"rx":2068.466907661297,"tx":2100.3514967765577}},{"timestamp":1769481366311,"cpu":0,"memory":13,"network":{"rx":2088.823735194656,"tx":2120.708324309917}},{"timestamp":1769481426342,"cpu":0,"memory":13,"network":{"rx":2084.506338391831,"tx":2116.3898652362946}},{"timestamp":1769481486369,"cpu":0,"memory":13,"network":{"rx":2063.3392196714753,"tx":2095.2254023256587}},{"timestamp":1769481546400,"cpu":0,"memory":13,"network":{"rx":2071.2619936034116,"tx":2103.1449893390195}},{"timestamp":1769481606422,"cpu":0,"memory":13,"network":{"rx":2075.6901751053797,"tx":2107.5790140117624}},{"timestamp":1769481666433,"cpu":0,"memory":13,"network":{"rx":4625.758181696994,"tx":4736.8359661401055}},{"timestamp":1769481726461,"cpu":0,"memory":13,"network":{"rx":2082.478176850803,"tx":2114.3632971280067}},{"timestamp":1769481786489,"cpu":0,"memory":13,"network":{"rx":2076.8974478576665,"tx":2108.7825681348704}},{"timestamp":1769481846516,"cpu":0,"memory":13,"network":{"rx":2081.4300231562465,"tx":2113.315674613091}},{"timestamp":1769481906545,"cpu":0,"memory":13,"network":{"rx":2076.9794599277016,"tx":2108.8640490429625}},{"timestamp":1769481966572,"cpu":0,"memory":13,"network":{"rx":2086.279278979109,"tx":2118.165461633292}},{"timestamp":1769482026578,"cpu":0,"memory":13,"network":{"rx":2884.813438432183,"tx":2936.5074074691283}},{"timestamp":1769482070677,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769482130682,"cpu":0,"memory":14,"network":{"rx":1977.3518873427215,"tx":2005.2162319806682}},{"timestamp":1769482190705,"cpu":0,"memory":14,"network":{"rx":2063.8421938257,"tx":2095.7299701780985}},{"timestamp":1769482250705,"cpu":0,"memory":13,"network":{"rx":2067.784463074385,"tx":2098.5183086384773}},{"timestamp":1769482310735,"cpu":0,"memory":14,"network":{"rx":2065.91704147926,"tx":2097.8010994502747}},{"timestamp":1769482370763,"cpu":0,"memory":13,"network":{"rx":2069.2165453364205,"tx":2101.1011344516814}},{"timestamp":1769482430789,"cpu":0,"memory":14,"network":{"rx":2060.5737513744043,"tx":2092.4599340285877}},{"timestamp":1769482490818,"cpu":0,"memory":13,"network":{"rx":2074.3807159872727,"tx":2106.2653051025336}},{"timestamp":1769482550846,"cpu":0,"memory":14,"network":{"rx":2068.134870393816,"tx":2100.0199906710204}},{"timestamp":1769482610844,"cpu":2,"memory":14,"network":{"rx":18048.80162672089,"tx":18344.744824827496}},{"timestamp":1769482619544,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769482679556,"cpu":0,"memory":14,"network":{"rx":1957.9084183163368,"tx":1988.4023195360928}},{"timestamp":1769482739554,"cpu":0,"memory":14,"network":{"rx":2885.912863762125,"tx":2937.614587152905}},{"timestamp":1769482799583,"cpu":0,"memory":14,"network":{"rx":2068.633493811324,"tx":2100.5180829265855}},{"timestamp":1769482859589,"cpu":0,"memory":14,"network":{"rx":154418.5914741859,"tx":6801.203213012032}},{"timestamp":1769482880007,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769482940015,"cpu":0,"memory":14,"network":{"rx":1975.9194760611263,"tx":2007.5157898245204}},{"timestamp":1769483000029,"cpu":0,"memory":14,"network":{"rx":2058.035491127218,"tx":2089.92751812047}},{"timestamp":1769483060060,"cpu":0,"memory":14,"network":{"rx":2062.3677766487317,"tx":2094.2513034931953}},{"timestamp":1769483120085,"cpu":0,"memory":14,"network":{"rx":2063.6578701852595,"tx":2095.5451152872183}},{"timestamp":1769483180115,"cpu":0,"memory":14,"network":{"rx":2055.7878429478105,"tx":2087.671369792274}},{"timestamp":1769483240141,"cpu":0,"memory":14,"network":{"rx":2062.5905872553103,"tx":2094.4773011245316}},{"timestamp":1769483300170,"cpu":0,"memory":14,"network":{"rx":2065.184074629352,"tx":2097.0681326003664}},{"timestamp":1769483360196,"cpu":0,"memory":14,"network":{"rx":2057.0586079365607,"tx":2088.944790590744}},{"timestamp":1769483420224,"cpu":0,"memory":14,"network":{"rx":2065.837039998667,"tx":2097.7226914555117}},{"timestamp":1769483480252,"cpu":0,"memory":14,"network":{"rx":2571.6332378223497,"tx":2603.5183580995536}},{"timestamp":1769483540282,"cpu":0,"memory":14,"network":{"rx":2057.8034682080925,"tx":2089.686995052556}},{"timestamp":1769483600276,"cpu":0,"memory":15,"network":{"rx":10299.0799079908,"tx":10383.821715504884}},{"timestamp":1769483618147,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769483678121,"cpu":0,"memory":15,"network":{"rx":17098.42995169082,"tx":17297.70531400966}},{"timestamp":1769483738119,"cpu":2,"memory":14,"network":{"rx":28958.163265306124,"tx":29294.897959183672}},{"timestamp":1769483798134,"cpu":0,"memory":14,"network":{"rx":2879.0529660094876,"tx":2922.139530835183}},{"timestamp":1769483858149,"cpu":0,"memory":14,"network":{"rx":2880.1164508560005,"tx":2923.133744677153}},{"timestamp":1769483918165,"cpu":0,"memory":14,"network":{"rx":2903.010033444816,"tx":2946.0105112279025}},{"timestamp":1769483978181,"cpu":0,"memory":14,"network":{"rx":2576.7878077373975,"tx":2614.0419434675005}},{"timestamp":1769484038190,"cpu":0,"memory":14,"network":{"rx":2559.6788194444443,"tx":2596.918402777778}},{"timestamp":1769484098206,"cpu":0,"memory":14,"network":{"rx":2465.8164150616,"tx":2503.0366128752385}},{"timestamp":1769484158221,"cpu":0,"memory":14,"network":{"rx":2444.8634590377114,"tx":2482.0546163849153}},{"timestamp":1769484218236,"cpu":0,"memory":14,"network":{"rx":2479.993071193487,"tx":2517.1487961198686}},{"timestamp":1769484278248,"cpu":0,"memory":14,"network":{"rx":2480.4885443284684,"tx":2517.6490969725846}},{"timestamp":1769484338259,"cpu":0,"memory":14,"network":{"rx":2466.2425344066473,"tx":2503.3757465593353}},{"timestamp":1769484398275,"cpu":0,"memory":14,"network":{"rx":2473.7434034085995,"tx":2510.857340600398}},{"timestamp":1769484458290,"cpu":0,"memory":14,"network":{"rx":2475.066977789301,"tx":2512.1424250280875}},{"timestamp":1769484518306,"cpu":0,"memory":14,"network":{"rx":2483.7414172820313,"tx":2520.7928488146135}},{"timestamp":1769484578323,"cpu":0,"memory":14,"network":{"rx":2453.20792164646,"tx":2490.227380592829}},{"timestamp":1769484638337,"cpu":0,"memory":14,"network":{"rx":2446.1830251303936,"tx":2483.16737790422}},{"timestamp":1769484698354,"cpu":0,"memory":14,"network":{"rx":2470.560344827586,"tx":2507.5431034482763}},{"timestamp":1769484758366,"cpu":0,"memory":14,"network":{"rx":2487.137501615892,"tx":2524.109105011419}},{"timestamp":1769484818381,"cpu":0,"memory":14,"network":{"rx":2482.133631823661,"tx":2519.071809884622}},{"timestamp":1769484878397,"cpu":0,"memory":14,"network":{"rx":2452.769290355898,"tx":2489.69316176787}},{"timestamp":1769484938414,"cpu":0,"memory":14,"network":{"rx":2475.9447955630076,"tx":2512.8337417773764}},{"timestamp":1769484998433,"cpu":0,"memory":14,"network":{"rx":2477.564515436472,"tx":2514.405942719739}},{"timestamp":1769485058446,"cpu":0,"memory":14,"network":{"rx":2465.4557945811325,"tx":2502.2972218643995}},{"timestamp":1769485118460,"cpu":0,"memory":14,"network":{"rx":2452.0559704695684,"tx":2488.8831659369907}},{"timestamp":1769485178474,"cpu":0,"memory":14,"network":{"rx":2443.9584941257185,"tx":2480.7477917845813}},{"timestamp":1769485238489,"cpu":0,"memory":14,"network":{"rx":2435.8710562414267,"tx":2472.6508916323733}},{"timestamp":1769485298503,"cpu":0,"memory":14,"network":{"rx":2450.85021630188,"tx":2487.6001199297552}},{"timestamp":1769485358522,"cpu":0,"memory":14,"network":{"rx":2451.80053032247,"tx":2488.4954238302967}},{"timestamp":1769485418534,"cpu":0,"memory":14,"network":{"rx":2452.53164556962,"tx":2489.2234006158055}},{"timestamp":1769485478551,"cpu":0,"memory":14,"network":{"rx":2427.246698859023,"tx":2463.911798641084}},{"timestamp":1769485538567,"cpu":0,"memory":14,"network":{"rx":2427.0277196429333,"tx":2463.6740272498187}},{"timestamp":1769485598581,"cpu":0,"memory":14,"network":{"rx":2426.8277068840425,"tx":2463.44586231915}},{"timestamp":1769485658597,"cpu":0,"memory":14,"network":{"rx":2471.0849539406345,"tx":2507.6765609007166}},{"timestamp":1769485718612,"cpu":0,"memory":14,"network":{"rx":2467.240717848161,"tx":2503.815166886909}},{"timestamp":1769485778630,"cpu":0,"memory":14,"network":{"rx":2441.108840061318,"tx":2477.6443536024526}},{"timestamp":1769485838641,"cpu":0,"memory":14,"network":{"rx":2452.1842799965934,"tx":2488.716682278804}},{"timestamp":1769485898656,"cpu":0,"memory":14,"network":{"rx":2446.5060856243085,"tx":2480.040854540812}},{"timestamp":1769485958674,"cpu":0,"memory":14,"network":{"rx":2440.556169742325,"tx":2477.0388638489667}},{"timestamp":1769486018691,"cpu":0,"memory":14,"network":{"rx":2430.7934214440525,"tx":2467.255960222685}},{"timestamp":1769486078703,"cpu":0,"memory":14,"network":{"rx":2422.5071104130407,"tx":2458.929405272318}},{"timestamp":1769486138718,"cpu":0,"memory":14,"network":{"rx":2418.6105691746543,"tx":2455.0004241241836}},{"timestamp":1769486198740,"cpu":0,"memory":14,"network":{"rx":2427.808619739797,"tx":2464.169174047548}},{"timestamp":1769486258750,"cpu":0,"memory":14,"network":{"rx":2417.6410777834267,"tx":2453.9908490086427}},{"timestamp":1769486318765,"cpu":0,"memory":14,"network":{"rx":2436.3544003725183,"tx":2472.6749354442704}},{"timestamp":1769486378781,"cpu":0,"memory":14,"network":{"rx":2429.4421400152373,"tx":2465.7580631507662}},{"timestamp":1769486438798,"cpu":0,"memory":14,"network":{"rx":2428.4626770987525,"tx":2464.7494184817087}},{"timestamp":1769486498810,"cpu":0,"memory":14,"network":{"rx":2446.1824638538938,"tx":2482.4553986640735}},{"timestamp":1769486558830,"cpu":0,"memory":14,"network":{"rx":2441.5952008787126,"tx":2477.842085251996}},{"timestamp":1769486618843,"cpu":0,"memory":14,"network":{"rx":2413.5455812185955,"tx":2449.774099565089}},{"timestamp":1769486678857,"cpu":0,"memory":14,"network":{"rx":2441.9104628614787,"tx":2478.0794199477277}},{"timestamp":1769486738874,"cpu":0,"memory":14,"network":{"rx":2429.263565891473,"tx":2465.41118975396}},{"timestamp":1769486798886,"cpu":0,"memory":14,"network":{"rx":2442.8812131423756,"tx":2479.0227464195455}},{"timestamp":1769486858902,"cpu":0,"memory":14,"network":{"rx":2451.822203496946,"tx":2487.971350326522}},{"timestamp":1769486918917,"cpu":0,"memory":14,"network":{"rx":2406.1144565629343,"tx":2442.2453362530005}},{"timestamp":1769486978932,"cpu":0,"memory":14,"network":{"rx":2404.640800369919,"tx":2440.7078902013536}},{"timestamp":1769487038946,"cpu":0,"memory":14,"network":{"rx":2409.090909090909,"tx":2445.1686149188463}},{"timestamp":1769487098963,"cpu":0,"memory":14,"network":{"rx":2418.3506280720917,"tx":2454.396504642272}},{"timestamp":1769487158980,"cpu":0,"memory":14,"network":{"rx":2435.257082896118,"tx":2471.269674711438}},{"timestamp":1769487218981,"cpu":0,"memory":14,"network":{"rx":2383.2536286601226,"tx":2419.2465810890176}},{"timestamp":1769487278998,"cpu":0,"memory":14,"network":{"rx":2413.6889783593356,"tx":2449.6728736789128}},{"timestamp":1769487339013,"cpu":0,"memory":14,"network":{"rx":2392.6071832697708,"tx":2428.5654415154436}},{"timestamp":1769487399013,"cpu":1,"memory":14,"network":{"rx":380788.59369761986,"tx":9325.972175662084}},{"timestamp":1769487459033,"cpu":0,"memory":15,"network":{"rx":2394.60681684951,"tx":2430.5334561594505}},{"timestamp":1769487482870,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769487542896,"cpu":0,"memory":15,"network":{"rx":2128.0724179116987,"tx":2159.880979821051}},{"timestamp":1769487595469,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769487655462,"cpu":0,"memory":15,"network":{"rx":2248.528644863294,"tx":2276.3912687178727}},{"timestamp":1769487715471,"cpu":0,"memory":15,"network":{"rx":2243.0834947598473,"tx":2277.504594446928}},{"timestamp":1769487775490,"cpu":0,"memory":15,"network":{"rx":2232.5021709465327,"tx":2266.8899640243144}},{"timestamp":1769487835520,"cpu":0,"memory":15,"network":{"rx":2242.2378732268626,"tx":2276.60946334689}},{"timestamp":1769487895545,"cpu":0,"memory":14,"network":{"rx":2420.0545364402574,"tx":2457.6846802181453}},{"timestamp":1769487955570,"cpu":0,"memory":14,"network":{"rx":2482.703117648515,"tx":2520.293192680088}},{"timestamp":1769488015595,"cpu":0,"memory":14,"network":{"rx":2508.6277325279134,"tx":2546.2085014730274}},{"timestamp":1769488075623,"cpu":0,"memory":14,"network":{"rx":2492.4907835811664,"tx":2530.049236707326}},{"timestamp":1769488135648,"cpu":0,"memory":14,"network":{"rx":2487.157301426417,"tx":2524.684185805048}},{"timestamp":1769488195672,"cpu":0,"memory":14,"network":{"rx":2492.7235440911227,"tx":2530.230029896474}},{"timestamp":1769488255697,"cpu":0,"memory":14,"network":{"rx":2470.672495122373,"tx":2508.16230767331}},{"timestamp":1769488315692,"cpu":0,"memory":15,"network":{"rx":6337.059064807218,"tx":6425.041017227235}},{"timestamp":1769488375690,"cpu":0,"memory":15,"network":{"rx":4887.212907096903,"tx":4971.915730524352}},{"timestamp":1769488435694,"cpu":0,"memory":15,"network":{"rx":4368.542097193521,"tx":4440.037330844611}},{"timestamp":1769488495690,"cpu":2,"memory":17,"network":{"rx":488701.43507175357,"tx":17339.200293348}},{"timestamp":1769488555736,"cpu":1,"memory":17,"network":{"rx":98691.13165126154,"tx":6176.017986510117}},{"timestamp":1769488615737,"cpu":4,"memory":15,"network":{"rx":427669.8221696305,"tx":16580.923651272482}},{"timestamp":1769488675758,"cpu":0,"memory":15,"network":{"rx":2088.0510479490854,"tx":2126.5369364566327}},{"timestamp":1769488735775,"cpu":0,"memory":15,"network":{"rx":2084.9592615425627,"tx":2124.5480447206623}},{"timestamp":1769488795802,"cpu":0,"memory":15,"network":{"rx":2098.9071402392296,"tx":2138.4899876720087}},{"timestamp":1769488855824,"cpu":0,"memory":15,"network":{"rx":2070.8728320810355,"tx":2110.457657897806}},{"timestamp":1769488915852,"cpu":0,"memory":15,"network":{"rx":2600.236556273739,"tx":2639.8180848937163}},{"timestamp":1769488975879,"cpu":0,"memory":15,"network":{"rx":2075.8504647985874,"tx":2115.4333122313665}},{"timestamp":1769489035904,"cpu":0,"memory":15,"network":{"rx":2083.9302968713555,"tx":2123.5131443041346}},{"timestamp":1769489095938,"cpu":0,"memory":15,"network":{"rx":2076.3746606033346,"tx":2115.95289257575}},{"timestamp":1769489155954,"cpu":0,"memory":15,"network":{"rx":2092.991868834977,"tx":2132.581311650227}},{"timestamp":1769489215979,"cpu":0,"memory":15,"network":{"rx":2084.9979175343606,"tx":2124.5814244064973}},{"timestamp":1769489276005,"cpu":0,"memory":15,"network":{"rx":2093.376203645087,"tx":2132.959051077866}},{"timestamp":1769489336025,"cpu":0,"memory":15,"network":{"rx":2070.4598467177607,"tx":2110.0466511162945}},{"timestamp":1769489396056,"cpu":0,"memory":15,"network":{"rx":2073.4287284902803,"tx":2113.0082790558213}},{"timestamp":1769489456082,"cpu":0,"memory":15,"network":{"rx":2081.364741945157,"tx":2119.7814280478456}},{"timestamp":1769489516108,"cpu":0,"memory":15,"network":{"rx":2073.5348015859795,"tx":2113.117649018758}},{"timestamp":1769489576133,"cpu":0,"memory":15,"network":{"rx":2079.000416493128,"tx":2118.5839233652646}},{"timestamp":1769489636153,"cpu":0,"memory":15,"network":{"rx":2071.976007997334,"tx":2111.562812395868}},{"timestamp":1769489696180,"cpu":0,"memory":15,"network":{"rx":2076.7974945025653,"tx":2116.379023122543}},{"timestamp":1769489756207,"cpu":0,"memory":15,"network":{"rx":2087.1122513577448,"tx":2126.695098790524}},{"timestamp":1769489816231,"cpu":0,"memory":15,"network":{"rx":4850.593096094895,"tx":4969.345595095296}},{"timestamp":1769489876251,"cpu":0,"memory":15,"network":{"rx":2917.627457514162,"tx":2977.007664111962}},{"timestamp":1769489936275,"cpu":0,"memory":15,"network":{"rx":2077.219112355058,"tx":2116.8032786885246}},{"timestamp":1769489996309,"cpu":1,"memory":15,"network":{"rx":153417.36349402007,"tx":6730.319485624813}},{"timestamp":1769490056342,"cpu":0,"memory":15,"network":{"rx":2085.619575899922,"tx":2125.197807872337}},{"timestamp":1769490116357,"cpu":0,"memory":15,"network":{"rx":2074.8966942148763,"tx":2114.486137030125}},{"timestamp":1769490176351,"cpu":0,"memory":15,"network":{"rx":2076.1422165919357,"tx":2115.7468371309988}},{"timestamp":1769490236384,"cpu":0,"memory":15,"network":{"rx":2072.52677693935,"tx":2112.1050089117653}},{"timestamp":1769490296401,"cpu":0,"memory":15,"network":{"rx":2082.9251224632612,"tx":2122.513246026192}},{"timestamp":1769490356424,"cpu":0,"memory":15,"network":{"rx":2073.938323642604,"tx":2113.5231494593736}},{"timestamp":1769490416447,"cpu":0,"memory":15,"network":{"rx":2094.4986838159343,"tx":2134.084169137983}},{"timestamp":1769490476472,"cpu":0,"memory":15,"network":{"rx":2071.670137442732,"tx":2111.253644314869}},{"timestamp":1769490536496,"cpu":0,"memory":15,"network":{"rx":2069.121199500208,"tx":2108.704706372345}},{"timestamp":1769490596524,"cpu":0,"memory":15,"network":{"rx":2078.248121678578,"tx":2117.8303096939712}},{"timestamp":1769490656547,"cpu":0,"memory":15,"network":{"rx":2071.789147493461,"tx":2111.373973310231}},{"timestamp":1769490716571,"cpu":0,"memory":15,"network":{"rx":4823.14035818409,"tx":4945.189504373177}},{"timestamp":1769490776600,"cpu":0,"memory":15,"network":{"rx":2073.8155527420536,"tx":2113.3970813620313}},{"timestamp":1769490836621,"cpu":0,"memory":15,"network":{"rx":2087.184698943721,"tx":2126.7701842657693}},{"timestamp":1769490896650,"cpu":0,"memory":15,"network":{"rx":2078.145562977894,"tx":2117.7264322244246}},{"timestamp":1769490956672,"cpu":0,"memory":15,"network":{"rx":2093.7989403885244,"tx":2133.384425710573}},{"timestamp":1769491016701,"cpu":0,"memory":15,"network":{"rx":2071.565410051808,"tx":2111.146279298339}},{"timestamp":1769491076694,"cpu":0,"memory":15,"network":{"rx":529.8804780876494,"tx":529.8804780876494}},{"timestamp":1769491136718,"cpu":0,"memory":15,"network":{"rx":2077.554819458512,"tx":2115.4972761367676}},{"timestamp":1769491196722,"cpu":3,"memory":18,"network":{"rx":476237.8977844297,"tx":15721.299408690533}},{"timestamp":1769491256744,"cpu":8,"memory":30,"network":{"rx":619107.8629517348,"tx":24597.300418651652}},{"timestamp":1769491316778,"cpu":0,"memory":16,"network":{"rx":2084.5436183514475,"tx":2122.6315283254785}}] \ No newline at end of file diff --git a/backend/data/icon.svg b/backend/data/icon.svg new file mode 100644 index 0000000..98247d0 --- /dev/null +++ b/backend/data/icon.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/data/pwa-192x192.png b/backend/data/pwa-192x192.png new file mode 100644 index 0000000..e358ab0 Binary files /dev/null and b/backend/data/pwa-192x192.png differ diff --git a/backend/data/pwa-512x512.png b/backend/data/pwa-512x512.png new file mode 100644 index 0000000..6abe905 Binary files /dev/null and b/backend/data/pwa-512x512.png differ diff --git a/backend/data/subscriptions.json b/backend/data/subscriptions.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/backend/data/subscriptions.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/backend/data/vapid.json b/backend/data/vapid.json new file mode 100644 index 0000000..c0136b5 --- /dev/null +++ b/backend/data/vapid.json @@ -0,0 +1 @@ +{"publicKey":"BFOD5bzNh9Dch_Rjt8cpWc2IfSktAoEUx0virpARRvPfpmsuvQka3ppCa0WajNFaIJS7Z62VNUAeo_6yMBvLVZA","privateKey":"ymBn65iLw_M19FJPdzJ7FqA7jgHVBCfSV6vSdM_8GIw"} \ No newline at end of file diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..252dcf5 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,21 @@ +{ + "name": "snstatus-backend", + "version": "1.0.0", + "description": "System monitoring backend for Synology NAS", + "main": "server.js", + "type": "module", + "scripts": { + "start": "node server.js", + "dev": "node --watch server.js" + }, + "dependencies": { + "cors": "^2.8.5", + "dockerode": "^4.0.0", + "express": "^4.18.2", + "socket.io": "^4.7.2", + "systeminformation": "^5.21.22", + "web-push": "^3.6.0" + }, + "author": "", + "license": "ISC" +} \ No newline at end of file diff --git a/backend/server.js b/backend/server.js new file mode 100644 index 0000000..e746670 --- /dev/null +++ b/backend/server.js @@ -0,0 +1,785 @@ +import express from 'express'; +import cors from 'cors'; +import si from 'systeminformation'; +import Docker from 'dockerode'; +import { createServer } from 'http'; +import { Server } from 'socket.io'; +import fs from 'fs'; +import path from 'path'; +import webpush from 'web-push'; +import crypto from 'crypto'; + +const app = express(); +const httpServer = createServer(app); +const io = new Server(httpServer, { + cors: { + origin: "*", + methods: ["GET", "POST"] + } +}); +const PORT = 8001; + +// Docker Connection +const docker = new Docker({ socketPath: '/var/run/docker.sock' }); + +app.use(cors()); +app.use(express.json()); + +// Helper: Read Synology Info from Host +const getSynologyInfo = () => { + try { + const versionPath = '/host/etc/VERSION'; + const synoInfoPath = '/host/etc/synoinfo.conf'; + + let dsmVersion = 'Unknown'; + let model = 'Synology NAS'; + + if (fs.existsSync(versionPath)) { + const versionContent = fs.readFileSync(versionPath, 'utf8'); + const productVersion = versionContent.match(/productversion="([^"]+)"/); + const buildNumber = versionContent.match(/buildnumber="([^"]+)"/); + if (productVersion && buildNumber) { + dsmVersion = `DSM ${productVersion[1]}-${buildNumber[1]}`; + } + } + + if (fs.existsSync(synoInfoPath)) { + // Try to read model from synoinfo (unique="synology_geminilake_920+") or similar + // Or usually /proc/sys/kernel/syno_hw_version if available + // Simpler fallback: check hostname or use generic if deep parsing fails. + // Actually synoinfo.conf has 'upnpmodelname="DS920+"' usually. + const infoContent = fs.readFileSync(synoInfoPath, 'utf8'); + const modelMatch = infoContent.match(/upnpmodelname="([^"]+)"/); + if (modelMatch) { + model = modelMatch[1]; + } + } + + return { model, dsmVersion }; + } catch (e) { + console.error('Failed to read host files:', e); + return { model: 'Docker Container', dsmVersion: 'Unknown' }; + } +}; + +// --- Docker Config & Security --- +const DATA_DIR = path.join(process.cwd(), 'data'); +const CONFIG_FILE = path.join(DATA_DIR, 'config.json'); + +// Ensure data directory exists +if (!fs.existsSync(DATA_DIR)) { + try { fs.mkdirSync(DATA_DIR, { recursive: true }); } + catch (e) { console.error('Failed to create data directory:', e); } +} + +let dockerConfig = { + enabled: false, + passwordHash: null, + salt: null, + retentionHours: 24, + alertThresholds: { cpu: 80, memory: 80, disk: 90 }, + containerAlertEnabled: false, + alertCooldownSeconds: 300 // 5 minutes default +}; + +const saveDockerConfig = () => { + try { fs.writeFileSync(CONFIG_FILE, JSON.stringify(dockerConfig, null, 2)); } + catch (e) { console.error('Failed to save docker config:', e); } +}; + +const loadDockerConfig = () => { + try { + if (fs.existsSync(CONFIG_FILE)) { + const data = fs.readFileSync(CONFIG_FILE, 'utf8'); + dockerConfig = JSON.parse(data); + // Merge defaults + if (!dockerConfig.alertThresholds) dockerConfig.alertThresholds = { cpu: 90, mem: 90, disk: 90 }; + // Restore history limit from saved retention hours + // Note: historyRetentionHours and HISTORY_LIMIT are defined later in the file + // This function is called AFTER those variables are initialized (line 438) + if (dockerConfig.retentionHours) { + historyRetentionHours = dockerConfig.retentionHours; + HISTORY_LIMIT = historyRetentionHours * 60; + } + } else { + saveDockerConfig(); + } + } catch (e) { console.error('Failed to load docker config:', e); } +}; +// I will Stub loadDockerConfig here and call the real logic later? +// No, I should move History Vars up too. + +// --- Helper: Secure Hash --- +const hashPassword = (password, salt = null) => { + if (!password) return null; + try { + if (!salt) salt = crypto.randomBytes(16).toString('hex'); + const hash = crypto.scryptSync(password, salt, 64).toString('hex'); + return { hash, salt }; + } catch (e) { console.error("Hashing error:", e); return null; } +}; + +// --- Push Notification System Variables --- +const SUBSCRIPTIONS_FILE = path.join(process.cwd(), 'data', 'subscriptions.json'); +let subscriptions = []; +let vapidKeys = { publicKey: '', privateKey: '' }; +let alertState = { cpu: 0, memory: 0, disk: 0 }; + +const initPushNotifications = () => { + // Ensure data directory exists + if (!fs.existsSync(process.cwd() + '/data')) { + fs.mkdirSync(process.cwd() + '/data', { recursive: true }); + } + + // Initialize VAPID keys + if (!fs.existsSync(process.cwd() + '/data/vapid.json')) { + vapidKeys = webpush.generateVAPIDKeys(); + fs.writeFileSync(process.cwd() + '/data/vapid.json', JSON.stringify(vapidKeys)); + } else { + vapidKeys = JSON.parse(fs.readFileSync(process.cwd() + '/data/vapid.json', 'utf8')); + } + webpush.setVapidDetails('mailto:admin@example.com', vapidKeys.publicKey, vapidKeys.privateKey); + + // Initialize subscriptions - CREATE FILE IF MISSING + if (fs.existsSync(SUBSCRIPTIONS_FILE)) { + try { + subscriptions = JSON.parse(fs.readFileSync(SUBSCRIPTIONS_FILE, 'utf8')); + console.log('[PUSH] Loaded', subscriptions.length, 'existing subscriptions'); + } catch (e) { + console.error('[PUSH] Error loading subscriptions:', e); + subscriptions = []; + } + } else { + console.log('[PUSH] Creating new subscriptions file'); + subscriptions = []; + fs.writeFileSync(SUBSCRIPTIONS_FILE, JSON.stringify(subscriptions)); + } +}; + +const saveSubscriptions = () => { + try { fs.writeFileSync(SUBSCRIPTIONS_FILE, JSON.stringify(subscriptions)); } + catch (e) { console.error('Failed to save subscriptions:', e); } +}; + +const sendNotification = (payload) => { + const notificationPayload = JSON.stringify(payload); + console.log('[PUSH] Sending notification to', subscriptions.length, 'subscribers'); + console.log('[PUSH] Payload:', payload); + + if (subscriptions.length === 0) { + console.warn('[PUSH] No subscriptions available!'); + return; + } + + subscriptions.forEach((subscription, index) => { + console.log(`[PUSH] Sending to subscription ${index + 1}/${subscriptions.length}`); + webpush.sendNotification(subscription, notificationPayload) + .then(() => { + console.log(`[PUSH] Successfully sent to subscription ${index + 1}`); + }) + .catch(error => { + console.error(`[PUSH] Error sending to subscription ${index + 1}:`, error); + if (error.statusCode === 410 || error.statusCode === 404) { + console.log('[PUSH] Removing expired subscription'); + subscriptions.splice(index, 1); + saveSubscriptions(); + } + }); + }); +}; + +const checkAlerts = (stats) => { + const now = Date.now(); + const thresholds = dockerConfig.alertThresholds || { cpu: 90, memory: 90, disk: 90 }; + const COOLDOWN = (dockerConfig.alertCooldownSeconds || 300) * 1000; // Convert seconds to ms + + if (stats.cpu > thresholds.cpu) { + if (now - alertState.cpu > COOLDOWN) { + console.log('[ALERT] CPU alert sent:', stats.cpu + '%'); + sendNotification({ title: 'snStatus 알림', body: `CPU 사용률 높음: ${stats.cpu}%` }); + alertState.cpu = now; + } + } else { alertState.cpu = 0; } + + if (stats.memory > thresholds.memory) { + if (now - alertState.memory > COOLDOWN) { + console.log('[ALERT] Memory alert sent:', stats.memory + '%'); + sendNotification({ title: 'snStatus 알림', body: `메모리 사용률 높음: ${stats.memory}%` }); + alertState.memory = now; + } + } else { alertState.memory = 0; } +}; + +// VAPID Public Key Endpoint +app.get('/api/notifications/vapidPublicKey', (req, res) => { + try { + if (!vapidKeys || !vapidKeys.publicKey) { + return res.status(500).json({ error: 'VAPID keys not initialized' }); + } + res.json({ publicKey: vapidKeys.publicKey }); + } catch (error) { + console.error('Error getting VAPID key:', error); + res.status(500).json({ error: 'Failed to get VAPID key' }); + } +}); + +// Subscribe to push notifications +app.post('/api/notifications/subscribe', async (req, res) => { + const subscription = req.body; + if (!subscription || !subscription.endpoint) { + return res.status(400).json({ error: 'Invalid subscription' }); + } + // Remove existing subscription with same endpoint + subscriptions = subscriptions.filter(sub => sub.endpoint !== subscription.endpoint); + subscriptions.push(subscription); + saveSubscriptions(); + console.log('Push subscription added:', subscription.endpoint); + res.status(201).json({ message: 'Subscription added' }); +}); + +// Unsubscribe from push notifications +app.post('/api/notifications/unsubscribe', async (req, res) => { + const { endpoint } = req.body; + subscriptions = subscriptions.filter(sub => sub.endpoint !== endpoint); + saveSubscriptions(); + console.log('Push subscription removed:', endpoint); + res.json({ message: 'Unsubscribed' }); +}); + +// --- System Info APIs --- +// Global state for network speed calculation +let previousNetStats = {}; +let previousTime = Date.now(); + +// Function to read and parse /host/proc/net/dev +const getHostNetworkStats = () => { + try { + // Use /sys/class/net mounted from host to bypass container network namespace isolation. + // This is the correct way to see host interfaces (eth0, eth1, etc.) from a bridge container. + const netDir = '/host/sys/class/net'; + + let interfaces = []; + try { + if (fs.existsSync(netDir)) { + interfaces = fs.readdirSync(netDir); + } else { + console.warn("Host sysfs not found at /host/sys/class/net"); + return []; + } + } catch (e) { return []; } + + const timestamp = Date.now(); + const currentStats = {}; + const timeDiff = (timestamp - previousTime) / 1000; + let result = []; + + for (const iface of interfaces) { + // Filter unwanted interfaces + if (iface === 'lo' || + iface.startsWith('veth') || + iface.startsWith('docker') || + iface.startsWith('br-') || + iface.startsWith('cali') || + iface.startsWith('flannel') || + iface.startsWith('cni') || + iface.startsWith('sit') || + iface.startsWith('tun') || + iface.startsWith('tap')) continue; + + try { + const rxPath = path.join(netDir, iface, 'statistics', 'rx_bytes'); + const txPath = path.join(netDir, iface, 'statistics', 'tx_bytes'); + + if (!fs.existsSync(rxPath) || !fs.existsSync(txPath)) continue; + + const rxBytes = parseInt(fs.readFileSync(rxPath, 'utf8').trim()); + const txBytes = parseInt(fs.readFileSync(txPath, 'utf8').trim()); + + currentStats[iface] = { rx: rxBytes, tx: txBytes }; + + let rx_sec = 0; + let tx_sec = 0; + + if (previousNetStats[iface] && timeDiff > 0) { + const prevRx = previousNetStats[iface].rx; + const prevTx = previousNetStats[iface].tx; + + // Simple difference (handle counter wrap if needed, but unlikely for 64bit counters in JS for short periods) + if (rxBytes >= prevRx) rx_sec = (rxBytes - prevRx) / timeDiff; + if (txBytes >= prevTx) tx_sec = (txBytes - prevTx) / timeDiff; + } + + result.push({ + iface: iface, + rx_sec: rx_sec, + tx_sec: tx_sec + }); + + } catch (err) { + continue; + } + } + + previousNetStats = currentStats; + previousTime = timestamp; + + return result; + + } catch (error) { + console.error('Failed to read host network stats from /sys:', error.message); + return []; + } +}; + +app.get('/api/stats', async (req, res) => { + try { + const [cpu, mem, fsData, osInfo, system] = await Promise.all([ + si.currentLoad(), + si.mem(), + si.fsSize(), + si.osInfo(), + si.system() + ]); + + // Get Host Network Stats + const network = getHostNetworkStats(); + + // Get Host Info + const hostInfo = getSynologyInfo(); + + // Disk Filtering Logic + const filteredStorage = fsData.filter(disk => { + let mountPoint = disk.mount; + if (mountPoint.startsWith('/host')) mountPoint = mountPoint.substring(5); + if (mountPoint.includes('@') || mountPoint.includes('docker') || mountPoint.includes('container') || + mountPoint.includes('appdata') || mountPoint.includes('tmp') || + disk.fs.startsWith('overlay') || disk.fs.startsWith('tmpfs') || disk.fs.startsWith('shm')) { + return false; + } + return /^\/volume\d+$/.test(mountPoint); + }); + + const stats = { + cpu: { + load: Math.round(cpu.currentLoad), + cores: cpu.cpus.map(c => Math.round(c.load)), + }, + memory: { + total: mem.total, + used: mem.used, + percentage: Math.round((mem.active / mem.total) * 100), + }, + storage: filteredStorage.map(disk => ({ + fs: disk.fs, + type: disk.type, + size: disk.size, + used: disk.used, + use: Math.round(disk.use), + mount: disk.mount, + })), + network: network, // Use custom stats + system: { + model: hostInfo.model !== 'Docker Container' ? hostInfo.model : system.model, // Prefer host info + manufacturer: system.manufacturer, + os: 'DSM', // Hardcode or derive + release: hostInfo.dsmVersion, + kernel: osInfo.kernel, + hostname: osInfo.hostname, + uptime: osInfo.uptime + } + }; + + res.json(stats); + + } catch (error) { + console.error('Error fetching system stats:', error); + res.status(500).json({ error: 'Failed to retrieve system statistics' }); + } +}); + +// --- History Data --- +const HISTORY_FILE = path.join(process.cwd(), 'data', 'history.json'); +let historyRetentionHours = 24; // Default 24 hours +let HISTORY_LIMIT = historyRetentionHours * 60; // points (1 min interval) +let historyData = []; + +const saveHistory = () => { + try { + fs.writeFileSync(HISTORY_FILE, JSON.stringify(historyData)); + } catch (e) { console.error('Failed to save history:', e); } +}; + +const loadHistory = () => { + try { + if (fs.existsSync(HISTORY_FILE)) { + const data = fs.readFileSync(HISTORY_FILE, 'utf8'); + historyData = JSON.parse(data); + // Ensure data respects current limit (if limit changed while offline? Unlikely but good practice) + if (historyData.length > HISTORY_LIMIT) { + historyData = historyData.slice(historyData.length - HISTORY_LIMIT); + } + } + } catch (e) { console.error('Failed to load history:', e); } +}; +loadHistory(); + +// Retention Settings API +app.get('/api/settings/retention', (req, res) => { + res.json({ hours: historyRetentionHours }); +}); + +app.post('/api/settings/retention', (req, res) => { + const { hours } = req.body; + if (!hours || hours < 1) { + return res.status(400).json({ error: 'Invalid retention period' }); + } + historyRetentionHours = parseInt(hours); + HISTORY_LIMIT = historyRetentionHours * 60; + + // Trim existing data if needed + if (historyData.length > HISTORY_LIMIT) { + historyData = historyData.slice(historyData.length - HISTORY_LIMIT); + } + saveHistory(); // Save truncated data + + // Save preference to config + dockerConfig.retentionHours = historyRetentionHours; + saveDockerConfig(); + + console.log(`History retention updated to ${historyRetentionHours} hours (${HISTORY_LIMIT} points)`); + res.json({ message: 'Retention updated', hours: historyRetentionHours }); +}); + +const updateHistory = async () => { + try { + const [cpu, mem] = await Promise.all([ + si.currentLoad(), + si.mem() + ]); + + // Capture Network Stats for History + // Note: This relies on the shared getHostNetworkStats state. + // It captures the "current rate" since the last poll (whether by UI or this loop). + const netStats = getHostNetworkStats(); + let totalRx = 0; + let totalTx = 0; + netStats.forEach(net => { + // Include all interfaces or filter? Let's include all physical-ish ones found by our filter + totalRx += net.rx_sec; + totalTx += net.tx_sec; + }); + + const point = { + timestamp: Date.now(), + cpu: Math.round(cpu.currentLoad), + memory: Math.round((mem.active / mem.total) * 100), + network: { + rx: totalRx, + tx: totalTx + } + }; + + historyData.push(point); + if (historyData.length > HISTORY_LIMIT) { + historyData.shift(); // Remove oldest + } + + saveHistory(); + checkAlerts(point); + } catch (e) { + console.error("Failed to update history:", e); + } +}; + +// Update history every 1 minute +setInterval(updateHistory, 60 * 1000); +// Initial run +updateHistory(); + +app.get('/api/history', (req, res) => { + res.json(historyData); +}); + + +// Config saved via helper +// NOTE: loadDockerConfig normally called here. +// But check scope: HISTORY_LIMIT is defined above at 220. +// Our new loadDockerConfig at top CANNOT see HISTORY_LIMIT. +// So we must fix loadDockerConfig logic. +// I'll handle that separately. For now, deleting this block. + +// Helper: Secure Hash (Scrypt + Salt) + + +loadDockerConfig(); // Initial Load + +// Docker Config API +app.get('/api/docker/config', (req, res) => { + res.json({ + enabled: dockerConfig.enabled, + alertThresholds: dockerConfig.alertThresholds || { cpu: 90, memory: 90, disk: 90 }, + containerAlertEnabled: dockerConfig.containerAlertEnabled !== undefined ? dockerConfig.containerAlertEnabled : true, + alertCooldownSeconds: dockerConfig.alertCooldownSeconds || 300 + }); +}); + +app.post('/api/docker/config', (req, res) => { + const { enabled, password } = req.body; + const isEnabling = !!enabled; + + // Case 1: Disabling (Require Verification) + if (!isEnabling && dockerConfig.enabled) { + if (!password) return res.status(400).json({ error: 'Password is required to disable Docker features.' }); + + if (dockerConfig.passwordHash) { + let match = false; + + if (!dockerConfig.salt) { + // Legacy SHA256 Check + const legacyHash = crypto.createHash('sha256').update(password).digest('hex'); + match = (legacyHash === dockerConfig.passwordHash); + } else { + // Modern Scrypt Check + try { + const { hash } = hashPassword(password, dockerConfig.salt); + match = crypto.timingSafeEqual( + Buffer.from(hash, 'hex'), + Buffer.from(dockerConfig.passwordHash, 'hex') + ); + } catch (e) { + return res.status(500).json({ error: 'Verification error.' }); + } + } + + if (!match) return res.status(401).json({ error: '패스워드가 올바르지 않습니다.' }); + } + } + + // Case 2: Enabling (Require Password if no existing hash) + if (isEnabling && !password && !dockerConfig.passwordHash) { + return res.status(400).json({ error: 'Password is required to enable Docker features.' }); + } + + dockerConfig.enabled = isEnabling; + + // If Enabling and password provided, update/set password + if (isEnabling && password) { + const result = hashPassword(password); + if (result) { + dockerConfig.passwordHash = result.hash; + dockerConfig.salt = result.salt; + } + } + + saveDockerConfig(); + console.log(`Docker feature ${dockerConfig.enabled ? 'Enabled' : 'Disabled'}`); + res.json({ message: 'Configuration saved', enabled: dockerConfig.enabled }); +}); + +// Verify Password API +app.post('/api/docker/verify', (req, res) => { + const { password } = req.body; + if (!password) return res.status(400).json({ success: false }); + + // Validate if config exists + if (!dockerConfig.passwordHash || !dockerConfig.salt) { + return res.status(500).json({ success: false, error: 'Security configuration not found. Please reset in Settings.' }); + } + + const { hash } = hashPassword(password, dockerConfig.salt); + + // Constant-time comparison to prevent timing attacks + try { + const match = crypto.timingSafeEqual( + Buffer.from(hash, 'hex'), + Buffer.from(dockerConfig.passwordHash, 'hex') + ); + + if (match) { + res.json({ success: true }); + } else { + res.status(401).json({ success: false, error: '패스워드가 올바르지 않습니다.' }); + } + } catch (e) { + res.status(401).json({ success: false, error: 'Verification failed' }); + } +}); + +// --- Docker APIs --- +// Helper: Calculate CPU Percent from Docker Stats +const calculateCPUPercent = (stats) => { + if (!stats || !stats.cpu_stats || !stats.precpu_stats) return 0; + + const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage; + const systemCpuDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage; + const numberCpus = stats.cpu_stats.online_cpus || (stats.cpu_stats.cpu_usage.percpu_usage ? stats.cpu_stats.cpu_usage.percpu_usage.length : 1); + + if (systemCpuDelta > 0 && cpuDelta > 0) { + return (cpuDelta / systemCpuDelta) * numberCpus * 100.0; + } + return 0; +}; + +// --- Docker APIs --- +app.get('/api/docker/containers', async (req, res) => { + // Check if enabled + if (!dockerConfig.enabled) { + return res.status(403).json({ error: 'Docker features are disabled.' }); + } + + try { + const containers = await docker.listContainers({ all: true }); + + // Fetch stats for running containers + const containersWithStats = await Promise.all(containers.map(async (containerInfo) => { + if (containerInfo.State !== 'running') { + return { ...containerInfo, stats: null }; + } + + try { + const container = docker.getContainer(containerInfo.Id); + const stats = await container.stats({ stream: false }); + + const cpuPercent = calculateCPUPercent(stats); + const memoryUsage = stats.memory_stats.usage || 0; + const memoryLimit = stats.memory_stats.limit || 0; + const memoryPercent = memoryLimit > 0 ? (memoryUsage / memoryLimit) * 100 : 0; + + return { + ...containerInfo, + stats: { + cpu: cpuPercent, + memory: memoryUsage, + memoryLimit: memoryLimit, + memoryPercent: memoryPercent + } + }; + } catch (e) { + console.error(`Failed to fetch stats for ${containerInfo.Names[0]}:`, e.message); + return { ...containerInfo, stats: null }; + } + })); + + res.json(containersWithStats); + } catch (error) { + console.error('Error fetching containers:', error); + res.status(500).json({ error: 'Failed to fetch containers. Is Docker Sock mounted?' }); + } +}); + +// Container Control APIs +app.post('/api/docker/containers/:id/:action', async (req, res) => { + const { id, action } = req.params; + try { + const container = docker.getContainer(id); + let data; + switch (action) { + case 'start': data = await container.start(); break; + case 'stop': data = await container.stop(); break; + case 'restart': data = await container.restart(); break; + default: return res.status(400).json({ error: 'Invalid action' }); + } + res.json({ message: `Container ${action} successful`, data }); + } catch (error) { + console.error(`Error performing ${action} on container ${id}:`, error); + res.status(500).json({ error: `Failed to ${action} container: ${error.message}` }); + } +}); + +// Save Alert Thresholds +app.post('/api/settings/thresholds', (req, res) => { + const { thresholds, containerAlertEnabled, alertCooldownSeconds } = req.body; + if (thresholds) { + dockerConfig.alertThresholds = thresholds; + if (containerAlertEnabled !== undefined) { + dockerConfig.containerAlertEnabled = containerAlertEnabled; + } + if (alertCooldownSeconds !== undefined) { + dockerConfig.alertCooldownSeconds = Math.max(10, Math.min(3600, alertCooldownSeconds)); + } + saveDockerConfig(); + res.json({ success: true }); + } else { + res.status(400).json({ error: 'Invalid thresholds' }); + } +}); + +// Container Logs API +app.get('/api/docker/containers/:id/logs', async (req, res) => { + const { id } = req.params; + try { + const container = docker.getContainer(id); + const logs = await container.logs({ stdout: true, stderr: true, tail: 100, timestamps: true }); + res.send(logs.toString('utf8')); + } catch (error) { + console.error('Error fetching logs:', error); + res.status(500).json({ error: 'Failed to fetch logs' }); + } +}); + + +const monitorDockerEvents = () => { + docker.getEvents((err, stream) => { + if (err) return console.error('Error getting docker events:', err); + + stream.on('data', chunk => { + if (!dockerConfig.enabled) return; // Ignore if disabled + if (!dockerConfig.containerAlertEnabled) return; // Ignore if container alerts disabled + + try { + const event = JSON.parse(chunk.toString()); + // Filter for container stop/die + if (event.Type === 'container' && (event.Action === 'die' || event.Action === 'stop')) { + // Check if it's "unexpected"? + // Usually we alert on any stop if the monitoring is strictly keeping uptime. + // But if user stops it manually via UI, we might get an event too. + // However, UI stop calls API -> API stops container. + // It is hard to distinguish "crash" from "manual stop" without state tracking. + // For now, user asked "컨테이너가 까지게 되면", effectively "stops". + // We will alert. + const containerName = event.Actor?.Attributes?.name || event.id.substring(0, 12); + sendNotification({ + title: 'snStatus 알림', + body: `컨테이너 종료: ${containerName}` + }); + } + } catch (e) { + console.error('Error parsing docker event:', e); + } + }); + }); +}; + +initPushNotifications(); +if (dockerConfig.enabled) monitorDockerEvents(); // Start if enabled, or always start? +// Better always start listening, but filter inside. +// Actually monitorDockerEvents is a stream. If I call it multiple times it duplicates. +// Just call it once. +monitorDockerEvents(); + + +// --- WebSocket for Terminal & Logs --- +io.on('connection', (socket) => { + let stream = null; + socket.on('attach-terminal', async (containerId) => { + try { + const container = docker.getContainer(containerId); + const exec = await container.exec({ + AttachStdin: true, AttachStdout: true, AttachStderr: true, Tty: true, Cmd: ['/bin/sh'] + }); + stream = await exec.start({ hijack: true, stdin: true }); + stream.on('data', (chunk) => socket.emit('terminal-output', chunk.toString('utf8'))); + socket.on('terminal-input', (data) => { if (stream) stream.write(data); }); + socket.on('resize-terminal', ({ cols, rows }) => { if (exec) exec.resize({ w: cols, h: rows }).catch(() => { }); }); + } catch (err) { + console.error('Error attaching to container:', err); + socket.emit('terminal-output', '\r\nError: Failed to attach to container. ' + err.message + '\r\n'); + } + }); + + socket.on('disconnect', () => { if (stream) stream.end(); }); +}); + +httpServer.listen(PORT, '0.0.0.0', () => { + console.log(`Server running on port ${PORT}`); +}); diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..c33e3fd --- /dev/null +++ b/build.sh @@ -0,0 +1,2 @@ +docker build --no-cache -t sa8001/snstatus-frontend:$1 ./frontend +docker build --no-cache -t sa8001/snstatus-backend:$1 ./backend diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml new file mode 100644 index 0000000..e61d5c2 --- /dev/null +++ b/docker-compose-dev.yml @@ -0,0 +1,25 @@ +services: + backend: + build: ./backend + container_name: snstatus-backend + restart: always + environment: + - NODE_ENV=production + privileged: true + volumes: + - /:/host:ro + - /proc:/host/proc:ro + - /sys:/host/sys:ro + - /var/run/docker.sock:/var/run/docker.sock + - ./backend/data:/app/data + ports: + - "8001:8001" + + frontend: + build: ./frontend + container_name: snstatus-frontend + restart: always + ports: + - "3005:80" + depends_on: + - backend diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..1ed3542 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,25 @@ +services: + backend: + image: sa8001/snstatus-backend:v0.1.5 + container_name: snstatus-backend + restart: always + environment: + - NODE_ENV=production + privileged: true + volumes: + - /:/host:ro + - /proc:/host/proc:ro + - /sys:/host/sys:ro + - /var/run/docker.sock:/var/run/docker.sock + - ./data:/app/data + ports: + - "8001:8001" + + frontend: + image: sa8001/snstatus-frontend:v0.1.5 + container_name: snstatus-frontend + restart: always + ports: + - "3005:80" + depends_on: + - backend diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..3c69781 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,15 @@ +# Build Stage +FROM node:18-alpine as build +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY . . +RUN npm run build + +# Production Stage +FROM nginx:alpine +COPY --from=build /app/dist /usr/share/nginx/html +# Copy custom nginx config if needed (optional but good for SPA) +COPY nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/backend/Dockerfile b/frontend/backend/Dockerfile new file mode 100644 index 0000000..4935625 --- /dev/null +++ b/frontend/backend/Dockerfile @@ -0,0 +1,12 @@ +FROM node:18-alpine + +WORKDIR /app + +COPY package*.json ./ +RUN npm install + +COPY . . + +EXPOSE 8001 + +CMD ["node", "server.js"] diff --git a/frontend/backend/convert_icons.js b/frontend/backend/convert_icons.js new file mode 100644 index 0000000..81c4981 --- /dev/null +++ b/frontend/backend/convert_icons.js @@ -0,0 +1,30 @@ +const sharp = require('sharp'); +const fs = require('fs'); +const path = require('path'); + +// /app/data 디렉토리 사용 +const DATA_DIR = path.join(__dirname, 'data'); +const svgPath = path.join(DATA_DIR, 'icon.svg'); + +// icon.svg가 존재하는지 확인 +if (!fs.existsSync(svgPath)) { + console.error(`File not found: ${svgPath}`); + process.exit(1); +} + +const svgBuffer = fs.readFileSync(svgPath); + +async function convert() { + try { + console.log('Converting icons from:', svgPath); + await sharp(svgBuffer).resize(64, 64).png().toFile(path.join(DATA_DIR, 'favicon.png')); + await sharp(svgBuffer).resize(192, 192).png().toFile(path.join(DATA_DIR, 'pwa-192x192.png')); + await sharp(svgBuffer).resize(512, 512).png().toFile(path.join(DATA_DIR, 'pwa-512x512.png')); + await sharp(svgBuffer).resize(180, 180).png().toFile(path.join(DATA_DIR, 'apple-touch-icon.png')); + console.log('Conversion complete. Files saved to:', DATA_DIR); + } catch (err) { + console.error('Error converting icons:', err); + process.exit(1); + } +} +convert(); diff --git a/frontend/backend/data/apple-touch-icon.png b/frontend/backend/data/apple-touch-icon.png new file mode 100644 index 0000000..5b34bf8 Binary files /dev/null and b/frontend/backend/data/apple-touch-icon.png differ diff --git a/frontend/backend/data/config.json b/frontend/backend/data/config.json new file mode 100644 index 0000000..51839bb --- /dev/null +++ b/frontend/backend/data/config.json @@ -0,0 +1,13 @@ +{ + "enabled": true, + "passwordHash": "d7fe5080df575511eb7b3c5e50a99c0402952461f5e202e8c90461c1e52d208a9c14329317d5b39ec85de62caca369114295d559466e044d9f8b3a5e94b5ebd3", + "salt": "ced784250b4ed141397cc2115e864dac", + "retentionHours": 24, + "alertThresholds": { + "cpu": 80, + "memory": 80, + "disk": 90 + }, + "containerAlertEnabled": false, + "alertCooldownSeconds": 300 +} \ No newline at end of file diff --git a/frontend/backend/data/convert_icons_exec.cjs b/frontend/backend/data/convert_icons_exec.cjs new file mode 100644 index 0000000..af19a6d --- /dev/null +++ b/frontend/backend/data/convert_icons_exec.cjs @@ -0,0 +1,30 @@ +const sharp = require('sharp'); +const fs = require('fs'); +const path = require('path'); + +// 이미 /app/data 내에서 실행되므로 현재 디렉토리가 data 폴더임 +const DATA_DIR = __dirname; +const svgPath = path.join(DATA_DIR, 'icon.svg'); + +// icon.svg가 존재하는지 확인 +if (!fs.existsSync(svgPath)) { + console.error(`File not found: ${svgPath}`); + process.exit(1); +} + +const svgBuffer = fs.readFileSync(svgPath); + +async function convert() { + try { + console.log('Converting icons from:', svgPath); + await sharp(svgBuffer).resize(64, 64).png().toFile(path.join(DATA_DIR, 'favicon.png')); + await sharp(svgBuffer).resize(192, 192).png().toFile(path.join(DATA_DIR, 'pwa-192x192.png')); + await sharp(svgBuffer).resize(512, 512).png().toFile(path.join(DATA_DIR, 'pwa-512x512.png')); + await sharp(svgBuffer).resize(180, 180).png().toFile(path.join(DATA_DIR, 'apple-touch-icon.png')); + console.log('Conversion complete. Files saved to:', DATA_DIR); + } catch (err) { + console.error('Error converting icons:', err); + process.exit(1); + } +} +convert(); diff --git a/frontend/backend/data/favicon.png b/frontend/backend/data/favicon.png new file mode 100644 index 0000000..9a8c30a Binary files /dev/null and b/frontend/backend/data/favicon.png differ diff --git a/frontend/backend/data/history.json b/frontend/backend/data/history.json new file mode 100644 index 0000000..1bfaba7 --- /dev/null +++ b/frontend/backend/data/history.json @@ -0,0 +1 @@ +[{"timestamp":1769232137824,"cpu":0,"memory":13,"network":{"rx":0,"tx":0}},{"timestamp":1769232197803,"cpu":1,"memory":13,"network":{"rx":17526.239067055394,"tx":17622.448979591834}},{"timestamp":1769232257797,"cpu":2,"memory":13,"network":{"rx":16948.979591836734,"tx":17045.18950437318}},{"timestamp":1769274730671,"cpu":0,"memory":13,"network":{"rx":7.1127441453073414,"tx":7.266378455621464}},{"timestamp":1769274790689,"cpu":0,"memory":13,"network":{"rx":887.4174614820249,"tx":897.1019809244314}},{"timestamp":1769274850712,"cpu":0,"memory":13,"network":{"rx":882.2515584891823,"tx":891.932526585992}},{"timestamp":1769274910711,"cpu":0,"memory":13,"network":{"rx":887.0269873863303,"tx":896.7072455265474}},{"timestamp":1769274970730,"cpu":0,"memory":13,"network":{"rx":891.1155889357025,"tx":900.7876900531232}},{"timestamp":1769275030753,"cpu":0,"memory":13,"network":{"rx":886.8969057046488,"tx":896.575744244024}},{"timestamp":1769275090769,"cpu":0,"memory":13,"network":{"rx":897.4800380924474,"tx":907.1496593656142}},{"timestamp":1769275150791,"cpu":0,"memory":13,"network":{"rx":907.6574104562252,"tx":917.316064830059}},{"timestamp":1769275210808,"cpu":0,"memory":13,"network":{"rx":914.7006108489704,"tx":924.3571454698416}},{"timestamp":1769275270826,"cpu":0,"memory":13,"network":{"rx":902.1349711193975,"tx":911.7862104262631}},{"timestamp":1769275330843,"cpu":0,"memory":13,"network":{"rx":903.721830600095,"tx":913.3642572774754}},{"timestamp":1769275390864,"cpu":0,"memory":13,"network":{"rx":905.5371026024748,"tx":915.1731941453445}},{"timestamp":1769275450878,"cpu":5,"memory":16,"network":{"rx":1230115.1207764724,"tx":36951.98131795957}},{"timestamp":1769275492446,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769275493594,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769275494850,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769275496235,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769275498028,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769275500604,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769275504749,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769275512194,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769275526088,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769275552727,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769275604947,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769275666057,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769275726999,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769275787999,"cpu":0,"memory":13,"network":{"rx":0,"tx":0}},{"timestamp":1769275848920,"cpu":0,"memory":13,"network":{"rx":0,"tx":0}},{"timestamp":1769275909920,"cpu":0,"memory":13,"network":{"rx":0,"tx":0}},{"timestamp":1769275970956,"cpu":0,"memory":13,"network":{"rx":0,"tx":0}},{"timestamp":1769276031990,"cpu":0,"memory":13,"network":{"rx":0,"tx":0}},{"timestamp":1769276289922,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769276349893,"cpu":1,"memory":14,"network":{"rx":3891.0401048111216,"tx":3943.882378630177}},{"timestamp":1769276409890,"cpu":0,"memory":14,"network":{"rx":2505.7718045951374,"tx":2542.5775150568816}},{"timestamp":1769276469909,"cpu":0,"memory":14,"network":{"rx":2380.862726803179,"tx":2420.450190772922}},{"timestamp":1769276529920,"cpu":3,"memory":15,"network":{"rx":10144.390195130893,"tx":10226.15853760144}},{"timestamp":1769276543922,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769276603923,"cpu":0,"memory":14,"network":{"rx":2854.71424523742,"tx":2905.0150835847267}},{"timestamp":1769276663936,"cpu":0,"memory":14,"network":{"rx":2056.4896435653463,"tx":2087.2839979337123}},{"timestamp":1769276723928,"cpu":1,"memory":14,"network":{"rx":7216.576914071646,"tx":7293.842191524233}},{"timestamp":1769276783957,"cpu":0,"memory":14,"network":{"rx":2086.2430865596057,"tx":2117.028719930699}},{"timestamp":1769276843980,"cpu":0,"memory":14,"network":{"rx":2065.2583176449025,"tx":2096.046515502391}},{"timestamp":1769276904009,"cpu":0,"memory":14,"network":{"rx":2054.4403538289826,"tx":2085.225474354062}},{"timestamp":1769276964022,"cpu":0,"memory":13,"network":{"rx":2055.6703435864965,"tx":2086.4631585963275}},{"timestamp":1769277024051,"cpu":0,"memory":13,"network":{"rx":2041.3147416995685,"tx":2072.1008879337633}},{"timestamp":1769277060043,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769277119993,"cpu":1,"memory":14,"network":{"rx":8726.259778819369,"tx":8849.743957565344}},{"timestamp":1769277122327,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769277182299,"cpu":0,"memory":14,"network":{"rx":2220.456554001101,"tx":2254.9732370645816}},{"timestamp":1769277220513,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769277280460,"cpu":0,"memory":15,"network":{"rx":4046.659719810119,"tx":4092.50897302304}},{"timestamp":1769277340454,"cpu":0,"memory":15,"network":{"rx":4364.702697825498,"tx":4418.2982946855545}},{"timestamp":1769277400451,"cpu":0,"memory":14,"network":{"rx":2101.771755254429,"tx":2132.573295331433}},{"timestamp":1769277425190,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769277485133,"cpu":0,"memory":14,"network":{"rx":2560.590367685137,"tx":2589.0730191610564}},{"timestamp":1769277545138,"cpu":0,"memory":14,"network":{"rx":2463.8756650798096,"tx":2496.2195463455614}},{"timestamp":1769277605165,"cpu":0,"memory":14,"network":{"rx":3908.841021540307,"tx":3957.219251336898}},{"timestamp":1769277665157,"cpu":1,"memory":14,"network":{"rx":7629.432624113476,"tx":7668.439716312057}},{"timestamp":1769277725168,"cpu":0,"memory":14,"network":{"rx":2130.1562427985436,"tx":2157.533299534498}},{"timestamp":1769277785176,"cpu":0,"memory":14,"network":{"rx":2127.2584808259585,"tx":2154.636799410029}},{"timestamp":1769277845191,"cpu":0,"memory":14,"network":{"rx":2147.393648891552,"tx":2174.7707056275062}},{"timestamp":1769277905203,"cpu":0,"memory":14,"network":{"rx":2129.8802946593,"tx":2157.228360957643}},{"timestamp":1769277965227,"cpu":0,"memory":14,"network":{"rx":2124.327802546307,"tx":2151.6293606655327}},{"timestamp":1769278025242,"cpu":0,"memory":14,"network":{"rx":2156.425351853555,"tx":2183.745745561586}},{"timestamp":1769278085248,"cpu":0,"memory":14,"network":{"rx":2165.730854197349,"tx":2193.068851251841}},{"timestamp":1769278145259,"cpu":0,"memory":14,"network":{"rx":2166.3755057006256,"tx":2193.683339463038}},{"timestamp":1769278205279,"cpu":0,"memory":14,"network":{"rx":2162.3072687224667,"tx":2189.564977973568}},{"timestamp":1769278265287,"cpu":0,"memory":14,"network":{"rx":2890.8129211006863,"tx":2928.2161072980293}},{"timestamp":1769278325311,"cpu":0,"memory":14,"network":{"rx":3802.079168332667,"tx":3879.048380647741}},{"timestamp":1769278385342,"cpu":0,"memory":14,"network":{"rx":2077.6099015508653,"tx":2108.3939964351753}},{"timestamp":1769278437582,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769278497503,"cpu":4,"memory":20,"network":{"rx":15875.467289719625,"tx":16155.80774365821}},{"timestamp":1769278514492,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769278574453,"cpu":0,"memory":15,"network":{"rx":2393.2889711645903,"tx":2427.1109554543787}},{"timestamp":1769278634455,"cpu":0,"memory":15,"network":{"rx":1928.8190393653545,"tx":1957.4180860637978}},{"timestamp":1769278694487,"cpu":0,"memory":14,"network":{"rx":2036.0147257250421,"tx":2066.798820609352}},{"timestamp":1769278754508,"cpu":0,"memory":14,"network":{"rx":2030.272737875077,"tx":2061.061961646757}},{"timestamp":1769278814533,"cpu":0,"memory":14,"network":{"rx":2076.0862321737973,"tx":2101.7093162734905}},{"timestamp":1769278874552,"cpu":0,"memory":14,"network":{"rx":2050.4823311840855,"tx":2086.436413921794}},{"timestamp":1769278934584,"cpu":0,"memory":14,"network":{"rx":2047.4096285190737,"tx":2078.194236215226}},{"timestamp":1769278994606,"cpu":0,"memory":14,"network":{"rx":2055.4944688791147,"tx":2086.2821538051444}},{"timestamp":1769279054638,"cpu":0,"memory":14,"network":{"rx":2050.5730277185503,"tx":2081.3566098081023}},{"timestamp":1769279114651,"cpu":0,"memory":14,"network":{"rx":2071.4011964074452,"tx":2102.1945245196875}},{"timestamp":1769279174673,"cpu":0,"memory":14,"network":{"rx":2042.3511379160975,"tx":2073.1398487221354}},{"timestamp":1769279234696,"cpu":0,"memory":14,"network":{"rx":3868.4337670559617,"tx":3946.5038401945917}},{"timestamp":1769279294703,"cpu":3,"memory":14,"network":{"rx":14035.179815351798,"tx":14226.943972269439}},{"timestamp":1769279300955,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769279360949,"cpu":0,"memory":15,"network":{"rx":1980.6483873656139,"tx":2005.883823651971}},{"timestamp":1769279420956,"cpu":0,"memory":15,"network":{"rx":2056.844315568443,"tx":2087.6412358764123}},{"timestamp":1769304067845,"cpu":0,"memory":15,"network":{"rx":15.139030325490573,"tx":15.465724700590002}},{"timestamp":1769304127875,"cpu":0,"memory":14,"network":{"rx":2060.254210464942,"tx":2085.8751603391693}},{"timestamp":1769304187906,"cpu":0,"memory":14,"network":{"rx":2057.670203728074,"tx":2093.6182972131064}},{"timestamp":1769304249603,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769304309586,"cpu":0,"memory":15,"network":{"rx":1963.104983244694,"tx":1991.0304929894467}},{"timestamp":1769304369613,"cpu":0,"memory":15,"network":{"rx":2055.1251936628514,"tx":2088.4935112532694}},{"timestamp":1769304429628,"cpu":0,"memory":14,"network":{"rx":2054.8344635686553,"tx":2085.625739373844}},{"timestamp":1769304487720,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769304525387,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769304562281,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769304622241,"cpu":0,"memory":15,"network":{"rx":2246.201571073567,"tx":2280.024683533748}},{"timestamp":1769304682258,"cpu":0,"memory":15,"network":{"rx":2522.243327001899,"tx":2564.030790762771}},{"timestamp":1769304696312,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769304733402,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769304793353,"cpu":0,"memory":15,"network":{"rx":2312.850414891231,"tx":2345.7426926814683}},{"timestamp":1769304853381,"cpu":0,"memory":15,"network":{"rx":2037.9489571533286,"tx":2069.8340774305325}},{"timestamp":1769304885038,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769304944975,"cpu":2,"memory":15,"network":{"rx":31627.53036437247,"tx":31761.133603238864}},{"timestamp":1769339780907,"cpu":4,"memory":15,"network":{"rx":7.990280764204416,"tx":8.194001493002265}},{"timestamp":1769339840925,"cpu":0,"memory":15,"network":{"rx":2779.704029841619,"tx":2820.063596893536}},{"timestamp":1769339900949,"cpu":0,"memory":15,"network":{"rx":2384.305674061434,"tx":2419.501919795222}},{"timestamp":1769339960981,"cpu":0,"memory":15,"network":{"rx":2385.367803837953,"tx":2420.549040511727}},{"timestamp":1769340021001,"cpu":0,"memory":15,"network":{"rx":2382.1421912042833,"tx":2417.3037479022937}},{"timestamp":1769340080999,"cpu":1,"memory":14,"network":{"rx":3005.1579626047715,"tx":3047.7111540941332}},{"timestamp":1769340140994,"cpu":1,"memory":15,"network":{"rx":5995.863336908227,"tx":6076.758081814003}},{"timestamp":1769340201020,"cpu":0,"memory":15,"network":{"rx":2374.1847898421483,"tx":2409.3220113397397}},{"timestamp":1769340261049,"cpu":0,"memory":14,"network":{"rx":2370.435383919785,"tx":2405.54270060374}},{"timestamp":1769340321076,"cpu":0,"memory":14,"network":{"rx":2378.200186095972,"tx":2413.2925694536752}},{"timestamp":1769340381096,"cpu":0,"memory":14,"network":{"rx":2376.844736352275,"tx":2411.9445847847473}},{"timestamp":1769340419449,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769340479426,"cpu":0,"memory":15,"network":{"rx":3213.3116476917303,"tx":3254.59777305142}},{"timestamp":1769340539443,"cpu":0,"memory":15,"network":{"rx":2427.586458485335,"tx":2461.2943236538745}},{"timestamp":1769340599445,"cpu":0,"memory":15,"network":{"rx":2962.559715587157,"tx":2999.2223086323743}},{"timestamp":1769340659454,"cpu":0,"memory":15,"network":{"rx":2151.054562541794,"tx":2182.0679185266845}},{"timestamp":1769340684696,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769340732016,"cpu":0,"memory":16,"network":{"rx":0,"tx":0}},{"timestamp":1769340791978,"cpu":0,"memory":16,"network":{"rx":6402.012248468942,"tx":6474.19072615923}},{"timestamp":1769340852004,"cpu":0,"memory":15,"network":{"rx":2322.1877941637904,"tx":2355.840131785378}},{"timestamp":1769340912000,"cpu":0,"memory":16,"network":{"rx":8487.974398719936,"tx":8559.077953897695}},{"timestamp":1769340931295,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769340991276,"cpu":0,"memory":15,"network":{"rx":1936.9466997882662,"tx":1967.4563611810404}},{"timestamp":1769341052398,"cpu":0,"memory":15,"network":{"rx":2040.149209777167,"tx":2072.5434377147344}},{"timestamp":1769341112428,"cpu":0,"memory":15,"network":{"rx":2048.2933248929685,"tx":2079.078445418048}},{"timestamp":1769341173488,"cpu":0,"memory":15,"network":{"rx":2074.6654874793235,"tx":2099.7559737303263}},{"timestamp":1769343788695,"cpu":0,"memory":15,"network":{"rx":93.54705247154337,"tx":95.68531451418013}},{"timestamp":1769343848723,"cpu":0,"memory":15,"network":{"rx":2038.3654294662492,"tx":2069.1510628373426}},{"timestamp":1769343908752,"cpu":0,"memory":15,"network":{"rx":2044.4285262123306,"tx":2075.2136467374103}},{"timestamp":1769343968776,"cpu":0,"memory":15,"network":{"rx":2027.9055044648808,"tx":2058.6931893909104}},{"timestamp":1769344028802,"cpu":0,"memory":15,"network":{"rx":2043.2645853463498,"tx":2074.0512444607334}},{"timestamp":1769344088828,"cpu":0,"memory":15,"network":{"rx":2042.6974528129008,"tx":2073.4835990470956}},{"timestamp":1769344148854,"cpu":0,"memory":15,"network":{"rx":2039.966680549771,"tx":2070.7538525614327}},{"timestamp":1769344208862,"cpu":1,"memory":15,"network":{"rx":12919.177443007598,"tx":13107.63564858019}},{"timestamp":1769344218270,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769344278213,"cpu":3,"memory":16,"network":{"rx":15367.766044408856,"tx":15546.769430959412}},{"timestamp":1769344290057,"cpu":0,"memory":16,"network":{"rx":0,"tx":0}},{"timestamp":1769344350054,"cpu":0,"memory":16,"network":{"rx":465.16550551685054,"tx":476.36587886262873}},{"timestamp":1769344410073,"cpu":0,"memory":16,"network":{"rx":465.13604025391965,"tx":469.5346473616688}},{"timestamp":1769344470108,"cpu":0,"memory":14,"network":{"rx":466.352400306493,"tx":470.7499083852484}},{"timestamp":1769344530092,"cpu":0,"memory":14,"network":{"rx":1511.0692722196975,"tx":1528.208790495358}},{"timestamp":1769344590116,"cpu":0,"memory":14,"network":{"rx":529.0457256461233,"tx":534.2942345924454}},{"timestamp":1769344650143,"cpu":0,"memory":14,"network":{"rx":996.0850298204111,"tx":1000.4831239796088}},{"timestamp":1769344680889,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769344740867,"cpu":0,"memory":15,"network":{"rx":475.44974074259324,"tx":480.21807632671437}},{"timestamp":1769344800894,"cpu":0,"memory":15,"network":{"rx":473.47027171106333,"tx":477.8682926016626}},{"timestamp":1769344860892,"cpu":1,"memory":15,"network":{"rx":8793.404461687682,"tx":8889.427740058196}},{"timestamp":1769344920933,"cpu":0,"memory":14,"network":{"rx":490.5561951359908,"tx":495.3050798676068}},{"timestamp":1769344946925,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769344993180,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769345053167,"cpu":0,"memory":15,"network":{"rx":458.364591147787,"tx":465.76644161040264}},{"timestamp":1769345113186,"cpu":0,"memory":15,"network":{"rx":469.92669110296566,"tx":474.32522492502494}},{"timestamp":1769345173213,"cpu":0,"memory":15,"network":{"rx":467.1064687557266,"tx":471.50448964632585}},{"timestamp":1769345233233,"cpu":0,"memory":14,"network":{"rx":475.2582472509164,"tx":479.6567810729756}},{"timestamp":1769345293272,"cpu":0,"memory":14,"network":{"rx":474.70810639750823,"tx":479.1052482553007}},{"timestamp":1769345353290,"cpu":0,"memory":14,"network":{"rx":467.184297782295,"tx":471.5830514687505}},{"timestamp":1769345413312,"cpu":0,"memory":14,"network":{"rx":471.5359112340269,"tx":475.93422521366807}},{"timestamp":1769345473337,"cpu":0,"memory":14,"network":{"rx":468.22157434402334,"tx":472.6197417742607}},{"timestamp":1769345533362,"cpu":0,"memory":14,"network":{"rx":624.6230911389943,"tx":631.0427001264468}},{"timestamp":1769345593386,"cpu":0,"memory":14,"network":{"rx":2582.9834732773556,"tx":2646.757963481274}},{"timestamp":1769345653411,"cpu":0,"memory":14,"network":{"rx":469.7542690545606,"tx":474.15243648479805}},{"timestamp":1769345713435,"cpu":0,"memory":14,"network":{"rx":469.76209516193524,"tx":474.16033586565374}},{"timestamp":1769345773463,"cpu":0,"memory":14,"network":{"rx":475.92790031318714,"tx":480.3258479376291}},{"timestamp":1769345833488,"cpu":0,"memory":14,"network":{"rx":466.45564348188253,"tx":470.85381091211997}},{"timestamp":1769345893517,"cpu":0,"memory":14,"network":{"rx":469.3153423288355,"tx":473.71314342828583}},{"timestamp":1769345953547,"cpu":0,"memory":14,"network":{"rx":472.9292996601586,"tx":477.3272472846005}},{"timestamp":1769346013565,"cpu":0,"memory":14,"network":{"rx":470.10113464069707,"tx":474.49974174844635}},{"timestamp":1769346073590,"cpu":0,"memory":14,"network":{"rx":468.2548937942524,"tx":472.6530612244898}},{"timestamp":1769346133617,"cpu":0,"memory":14,"network":{"rx":468.2392923184567,"tx":472.63731320905595}},{"timestamp":1769346193643,"cpu":0,"memory":14,"network":{"rx":469.74644320794323,"tx":474.14453736714086}},{"timestamp":1769346253671,"cpu":0,"memory":14,"network":{"rx":466.7477426448539,"tx":471.14583680405156}},{"timestamp":1769346313673,"cpu":3,"memory":15,"network":{"rx":30155.572961802543,"tx":30314.345710285983}},{"timestamp":1769346317787,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769346377782,"cpu":0,"memory":15,"network":{"rx":481.931526485982,"tx":485.59855985598557}},{"timestamp":1769346437791,"cpu":0,"memory":15,"network":{"rx":465.61349130963686,"tx":470.01283140862205}},{"timestamp":1769346497806,"cpu":0,"memory":15,"network":{"rx":971.3904857119054,"tx":975.7893859868366}},{"timestamp":1769346557816,"cpu":0,"memory":14,"network":{"rx":474.9454248529387,"tx":479.34476495192393}},{"timestamp":1769346617805,"cpu":0,"memory":14,"network":{"rx":472.18749479088535,"tx":476.5881548899001}},{"timestamp":1769346677808,"cpu":3,"memory":15,"network":{"rx":25012.54937253137,"tx":25168.341582920853}},{"timestamp":1769346680555,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769346740543,"cpu":0,"memory":15,"network":{"rx":471.5443088617723,"tx":476.745349069814}},{"timestamp":1769346800558,"cpu":0,"memory":15,"network":{"rx":455.6694159793385,"tx":460.0683162542698}},{"timestamp":1769346860583,"cpu":0,"memory":15,"network":{"rx":465.71485689534535,"tx":470.11295105454303}},{"timestamp":1769346920609,"cpu":0,"memory":14,"network":{"rx":471.22817539650805,"tx":475.6264161002266}},{"timestamp":1769346980633,"cpu":0,"memory":14,"network":{"rx":475.61849229487717,"tx":478.9171178675552}},{"timestamp":1769347040660,"cpu":0,"memory":14,"network":{"rx":464.6075932497043,"tx":469.0056141403035}},{"timestamp":1769347100683,"cpu":0,"memory":14,"network":{"rx":473.7350682238475,"tx":478.1333822034886}},{"timestamp":1769347160709,"cpu":0,"memory":14,"network":{"rx":468.22937491669995,"tx":472.6276156204185}},{"timestamp":1769347220735,"cpu":0,"memory":14,"network":{"rx":467.8139472895079,"tx":472.2120414487056}},{"timestamp":1769347280754,"cpu":0,"memory":14,"network":{"rx":476.0167274787158,"tx":479.2489295413272}},{"timestamp":1769347340788,"cpu":0,"memory":14,"network":{"rx":473.9814105340307,"tx":478.37891861278615}},{"timestamp":1769347400810,"cpu":0,"memory":14,"network":{"rx":2622.132253711201,"tx":2690.308392062778}},{"timestamp":1769347460828,"cpu":0,"memory":14,"network":{"rx":473.8078576427072,"tx":478.2065380385884}},{"timestamp":1769347520849,"cpu":0,"memory":14,"network":{"rx":472.2847003548758,"tx":476.68316089368716}},{"timestamp":1769347580879,"cpu":0,"memory":14,"network":{"rx":464.9175412293853,"tx":469.3153423288356}},{"timestamp":1769347640905,"cpu":0,"memory":14,"network":{"rx":472.64518708559626,"tx":477.0432812447939}},{"timestamp":1769347700906,"cpu":2,"memory":20,"network":{"rx":22155.544815172827,"tx":22203.226559114693}},{"timestamp":1769347720239,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769347780234,"cpu":0,"memory":15,"network":{"rx":478.2057906755788,"tx":484.5065257613388}},{"timestamp":1769347840243,"cpu":0,"memory":15,"network":{"rx":456.8148111116666,"tx":461.2141512106517}},{"timestamp":1769347900268,"cpu":0,"memory":15,"network":{"rx":472.60399486897563,"tx":477.0020157595749}},{"timestamp":1769347960295,"cpu":0,"memory":14,"network":{"rx":464.91520341185486,"tx":469.3132975710525}},{"timestamp":1769348020319,"cpu":0,"memory":14,"network":{"rx":465.7148568953453,"tx":470.11295105454303}},{"timestamp":1769348080343,"cpu":0,"memory":14,"network":{"rx":471.9278955084633,"tx":476.3261362121818}},{"timestamp":1769348140368,"cpu":0,"memory":14,"network":{"rx":466.8299346927895,"tx":471.22817539650805}},{"timestamp":1769348200368,"cpu":0,"memory":14,"network":{"rx":470.232341078036,"tx":474.6324877495917}},{"timestamp":1769348260376,"cpu":0,"memory":14,"network":{"rx":1004.4825862356274,"tx":1008.881853024496}},{"timestamp":1769348320374,"cpu":0,"memory":14,"network":{"rx":471.73239107970267,"tx":476.1325377512584}},{"timestamp":1769348380369,"cpu":0,"memory":14,"network":{"rx":466.6966696669667,"tx":471.09710971097115}},{"timestamp":1769348440361,"cpu":0,"memory":14,"network":{"rx":470.84611281504203,"tx":475.24669955994136}},{"timestamp":1769348500364,"cpu":0,"memory":14,"network":{"rx":706.2215629739505,"tx":713.921434642756}},{"timestamp":1769348560352,"cpu":2,"memory":20,"network":{"rx":9038.88851846913,"tx":9114.081877583678}},{"timestamp":1769348580259,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769348631963,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769348691959,"cpu":0,"memory":15,"network":{"rx":482.67297851416,"tx":488.5736669278082}},{"timestamp":1769348751926,"cpu":3,"memory":15,"network":{"rx":8213.730656350053,"tx":8268.59324973319}},{"timestamp":1769348765691,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769348825659,"cpu":0,"memory":15,"network":{"rx":477.0457084729935,"tx":482.24857004685913}},{"timestamp":1769348885653,"cpu":1,"memory":15,"network":{"rx":15642.611683848798,"tx":15812.714776632303}},{"timestamp":1769348945643,"cpu":1,"memory":15,"network":{"rx":10519.658119658121,"tx":10632.478632478635}},{"timestamp":1769349005668,"cpu":0,"memory":14,"network":{"rx":654.1758073567676,"tx":660.9101576450181}},{"timestamp":1769349065694,"cpu":0,"memory":14,"network":{"rx":466.8066036950039,"tx":471.20462458560314}},{"timestamp":1769349093262,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769349153258,"cpu":0,"memory":15,"network":{"rx":2744.207754108744,"tx":2816.5149848318165}},{"timestamp":1769349213276,"cpu":0,"memory":15,"network":{"rx":469.23474233159504,"tx":473.6333494393442}},{"timestamp":1769349273308,"cpu":0,"memory":15,"network":{"rx":470.3893118326142,"tx":474.78718619333983}},{"timestamp":1769349333299,"cpu":2,"memory":14,"network":{"rx":15914.565826330534,"tx":16007.002801120449}},{"timestamp":1769349393338,"cpu":0,"memory":14,"network":{"rx":525.8427412154521,"tx":531.0807325251483}},{"timestamp":1769349453321,"cpu":1,"memory":14,"network":{"rx":9842.076798269334,"tx":9949.161709031909}},{"timestamp":1769349513316,"cpu":0,"memory":14,"network":{"rx":4741.532047941636,"tx":4793.121417404898}},{"timestamp":1769349573325,"cpu":0,"memory":14,"network":{"rx":1255.578523347644,"tx":1269.5712089892404}},{"timestamp":1769349633347,"cpu":0,"memory":14,"network":{"rx":538.2811560508278,"tx":543.5723734316751}},{"timestamp":1769349693342,"cpu":0,"memory":14,"network":{"rx":534.8235765838011,"tx":540.1162790697674}},{"timestamp":1769349753351,"cpu":3,"memory":15,"network":{"rx":32161.117121448893,"tx":32347.07697239251}},{"timestamp":1769349760861,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769349820864,"cpu":0,"memory":15,"network":{"rx":491.43704220439486,"tx":498.0292989187303}},{"timestamp":1769349880872,"cpu":0,"memory":15,"network":{"rx":480.1792064709574,"tx":484.78139599748977}},{"timestamp":1769349940891,"cpu":0,"memory":15,"network":{"rx":477.0193044811485,"tx":481.61892814830304}},{"timestamp":1769350000893,"cpu":1,"memory":14,"network":{"rx":11119.801980198019,"tx":11185.148514851484}},{"timestamp":1769350060884,"cpu":1,"memory":14,"network":{"rx":11431.166347992352,"tx":11494.263862332697}},{"timestamp":1769350120888,"cpu":0,"memory":14,"network":{"rx":1995.3606073386757,"tx":2016.2378743146353}},{"timestamp":1769350180915,"cpu":0,"memory":14,"network":{"rx":470.41282111085195,"tx":474.8109152700496}},{"timestamp":1769350240941,"cpu":0,"memory":14,"network":{"rx":587.052043542582,"tx":592.8812735984455}},{"timestamp":1769350271656,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769350331628,"cpu":0,"memory":15,"network":{"rx":467.10131394650836,"tx":472.30374174614815}},{"timestamp":1769350391621,"cpu":1,"memory":15,"network":{"rx":16216.740088105726,"tx":16391.189427312776}},{"timestamp":1769350451634,"cpu":0,"memory":15,"network":{"rx":860.9226835245232,"tx":870.2986823880385}},{"timestamp":1769350511657,"cpu":0,"memory":14,"network":{"rx":1364.9406918104978,"tx":1383.6920235812204}},{"timestamp":1769350558474,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769350618439,"cpu":1,"memory":15,"network":{"rx":6164.08876933423,"tx":6230.665770006724}},{"timestamp":1769350678431,"cpu":1,"memory":15,"network":{"rx":6154.362416107382,"tx":6220.805369127516}},{"timestamp":1769350738427,"cpu":0,"memory":15,"network":{"rx":2275.316852804618,"tx":2300.163132137031}},{"timestamp":1769350798433,"cpu":0,"memory":14,"network":{"rx":1576.782023071281,"tx":1594.3855437754219}},{"timestamp":1769350858437,"cpu":1,"memory":14,"network":{"rx":6121.992544900034,"tx":6189.088444595052}},{"timestamp":1769350911236,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769350971204,"cpu":0,"memory":15,"network":{"rx":2404.619776797301,"tx":2430.3140410070073}},{"timestamp":1769351031208,"cpu":0,"memory":15,"network":{"rx":6317.373461012312,"tx":6385.088919288646}},{"timestamp":1769351091228,"cpu":0,"memory":15,"network":{"rx":580.332894583382,"tx":586.9308052450058}},{"timestamp":1769351151252,"cpu":0,"memory":14,"network":{"rx":466.43009462881514,"tx":470.8283353325337}},{"timestamp":1769351211289,"cpu":0,"memory":14,"network":{"rx":464.89664706764165,"tx":468.12798774089316}},{"timestamp":1769351271280,"cpu":3,"memory":14,"network":{"rx":11804.87398319776,"tx":11969.49593279104}},{"timestamp":1769351273778,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769351333734,"cpu":0,"memory":15,"network":{"rx":592.0372285418821,"tx":600.5437502084932}},{"timestamp":1769351393733,"cpu":0,"memory":15,"network":{"rx":5673.218673218674,"tx":5734.029484029485}},{"timestamp":1769351453728,"cpu":0,"memory":15,"network":{"rx":1801.2895662368112,"tx":1820.63305978898}},{"timestamp":1769351513730,"cpu":0,"memory":14,"network":{"rx":1806.0239789453162,"tx":1825.3241056633199}},{"timestamp":1769351573732,"cpu":0,"memory":14,"network":{"rx":1830.6774855802132,"tx":1850.034216443445}},{"timestamp":1769351633730,"cpu":0,"memory":14,"network":{"rx":1861.3010353584687,"tx":1880.6407501465133}},{"timestamp":1769351693730,"cpu":0,"memory":14,"network":{"rx":1872.032822115854,"tx":1891.374426101397}},{"timestamp":1769351753731,"cpu":0,"memory":14,"network":{"rx":1848.046875,"tx":1867.3828125}},{"timestamp":1769351813728,"cpu":0,"memory":14,"network":{"rx":1821.9097832454597,"tx":1841.24194493263}},{"timestamp":1769351873732,"cpu":0,"memory":14,"network":{"rx":1875.146771037182,"tx":1894.5205479452054}},{"timestamp":1769351933735,"cpu":0,"memory":14,"network":{"rx":1839.6632732967894,"tx":1859.0446358653094}},{"timestamp":1769351993729,"cpu":0,"memory":14,"network":{"rx":1869.403714565005,"tx":1888.758553274682}},{"timestamp":1769352053731,"cpu":0,"memory":14,"network":{"rx":1845.9430361162767,"tx":1865.3225017128316}},{"timestamp":1769352113734,"cpu":0,"memory":14,"network":{"rx":1917.2682926829268,"tx":1930.1463414634147}},{"timestamp":1769352173737,"cpu":0,"memory":14,"network":{"rx":1933.002239750706,"tx":1952.2835719154737}},{"timestamp":1769352233737,"cpu":0,"memory":14,"network":{"rx":1903.0905722920932,"tx":1922.3944623184168}},{"timestamp":1769352293731,"cpu":0,"memory":14,"network":{"rx":1919.328059380799,"tx":1938.665885340365}},{"timestamp":1769352353737,"cpu":0,"memory":14,"network":{"rx":1919.0672153635119,"tx":1938.467568097198}},{"timestamp":1769352413740,"cpu":0,"memory":14,"network":{"rx":1937.4695626765365,"tx":1956.7546508230255}},{"timestamp":1769352473738,"cpu":0,"memory":14,"network":{"rx":1934.320795166634,"tx":1953.6152796725783}},{"timestamp":1769352533738,"cpu":0,"memory":14,"network":{"rx":1963.8318670576734,"tx":1983.186705767351}},{"timestamp":1769352576211,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769352618522,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769352678509,"cpu":0,"memory":15,"network":{"rx":759.611816754674,"tx":767.1471385757101}},{"timestamp":1769352738526,"cpu":0,"memory":15,"network":{"rx":739.1515359141258,"tx":746.6883635948384}},{"timestamp":1769352798551,"cpu":0,"memory":15,"network":{"rx":721.6659535016403,"tx":729.1969761802881}},{"timestamp":1769352858555,"cpu":1,"memory":14,"network":{"rx":5865.446371226719,"tx":5929.03018625562}},{"timestamp":1769352918551,"cpu":0,"memory":14,"network":{"rx":1754.8179871520342,"tx":1775.0076475986539}},{"timestamp":1769352978556,"cpu":0,"memory":14,"network":{"rx":3466.5432274604764,"tx":3513.5450790485693}},{"timestamp":1769353006454,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769353066411,"cpu":1,"memory":15,"network":{"rx":6359.189378057302,"tx":6428.371767994409}},{"timestamp":1769353126409,"cpu":1,"memory":15,"network":{"rx":6247.793618465716,"tx":6315.00339443313}},{"timestamp":1769353186406,"cpu":1,"memory":15,"network":{"rx":6312.776831345826,"tx":6380.238500851789}},{"timestamp":1769353246392,"cpu":1,"memory":14,"network":{"rx":6271.878242822552,"tx":6340.3666551366305}},{"timestamp":1769353306389,"cpu":1,"memory":14,"network":{"rx":6462.173314993122,"tx":6530.261348005502}},{"timestamp":1769353366381,"cpu":1,"memory":14,"network":{"rx":6416.149068322981,"tx":6484.472049689441}},{"timestamp":1769353426380,"cpu":1,"memory":14,"network":{"rx":6437.804030576789,"tx":6506.601806810285}},{"timestamp":1769383077987,"cpu":1,"memory":14,"network":{"rx":11.457266792927156,"tx":11.778134139499038}},{"timestamp":1769474461904,"cpu":6,"memory":11,"network":{"rx":0,"tx":0}},{"timestamp":1769474486342,"cpu":6,"memory":13,"network":{"rx":0,"tx":0}},{"timestamp":1769474546315,"cpu":0,"memory":13,"network":{"rx":6010.301109350237,"tx":6114.896988906497}},{"timestamp":1769474606334,"cpu":0,"memory":13,"network":{"rx":4474.491836054648,"tx":4534.905031656114}},{"timestamp":1769474666359,"cpu":0,"memory":13,"network":{"rx":2048.6630570595585,"tx":2080.5497709287797}},{"timestamp":1769474726388,"cpu":0,"memory":13,"network":{"rx":2057.888687134551,"tx":2089.7732762498126}},{"timestamp":1769474786400,"cpu":0,"memory":12,"network":{"rx":2049.9900019996003,"tx":2080.7171898953543}},{"timestamp":1769474846427,"cpu":0,"memory":12,"network":{"rx":2052.0099288653437,"tx":2083.8955803221884}},{"timestamp":1769474906454,"cpu":0,"memory":12,"network":{"rx":2053.82577839972,"tx":2085.7114298565643}},{"timestamp":1769474966478,"cpu":0,"memory":12,"network":{"rx":2060.8923097427696,"tx":2092.7795548447284}},{"timestamp":1769475026484,"cpu":0,"memory":12,"network":{"rx":2046.1953804619538,"tx":2076.9256407692565}},{"timestamp":1769475086507,"cpu":0,"memory":12,"network":{"rx":2049.664295353448,"tx":2081.552071705846}},{"timestamp":1769475146539,"cpu":0,"memory":12,"network":{"rx":2034.4816098081023,"tx":2066.3646055437102}},{"timestamp":1769475206570,"cpu":0,"memory":12,"network":{"rx":2048.3750062467725,"tx":2080.258533091236}},{"timestamp":1769475266568,"cpu":0,"memory":12,"network":{"rx":2062.50208340278,"tx":2094.403146771559}},{"timestamp":1769475326598,"cpu":0,"memory":12,"network":{"rx":2056.3727531693016,"tx":2088.2573422845626}},{"timestamp":1769475386625,"cpu":0,"memory":12,"network":{"rx":2557.5064969680816,"tx":2589.3916172452855}},{"timestamp":1769475446653,"cpu":0,"memory":12,"network":{"rx":2046.0784967015393,"tx":2077.963616978743}},{"timestamp":1769475506681,"cpu":0,"memory":12,"network":{"rx":2050.9604011528145,"tx":2082.8460526096587}},{"timestamp":1769475566702,"cpu":0,"memory":12,"network":{"rx":2054.38006064443,"tx":2085.1021292192863}},{"timestamp":1769475626707,"cpu":0,"memory":12,"network":{"rx":2052.345637863511,"tx":2084.242979751687}},{"timestamp":1769475686730,"cpu":0,"memory":12,"network":{"rx":4263.3823700914645,"tx":4321.66003032171}},{"timestamp":1769475746758,"cpu":0,"memory":12,"network":{"rx":2060.3718264809754,"tx":2092.2569467581793}},{"timestamp":1769475806787,"cpu":0,"memory":12,"network":{"rx":2065.534991420813,"tx":2097.419580536074}},{"timestamp":1769475866808,"cpu":0,"memory":12,"network":{"rx":2072.258043018277,"tx":2104.1468819246597}},{"timestamp":1769475926834,"cpu":0,"memory":12,"network":{"rx":2057.7759266972093,"tx":2089.662640566431}},{"timestamp":1769475986858,"cpu":0,"memory":12,"network":{"rx":2063.723448563099,"tx":2095.61016243232}},{"timestamp":1769476046885,"cpu":0,"memory":12,"network":{"rx":2069.1533668743546,"tx":2101.0395495285375}},{"timestamp":1769476106910,"cpu":0,"memory":12,"network":{"rx":2064.7885916103023,"tx":2096.674774264485}},{"timestamp":1769476166937,"cpu":0,"memory":12,"network":{"rx":2062.1720225898343,"tx":2094.057674046679}},{"timestamp":1769476226963,"cpu":0,"memory":12,"network":{"rx":2063.7067888379843,"tx":2095.5935027072055}},{"timestamp":1769476286989,"cpu":0,"memory":12,"network":{"rx":4575.05789061589,"tx":4683.90890765822}},{"timestamp":1769476347014,"cpu":0,"memory":12,"network":{"rx":2060.491461890879,"tx":2092.3781757601}},{"timestamp":1769476407041,"cpu":0,"memory":12,"network":{"rx":2054.9586019624503,"tx":2086.8442534192945}},{"timestamp":1769476467071,"cpu":0,"memory":12,"network":{"rx":2058.854886804711,"tx":2090.739475919972}},{"timestamp":1769476527075,"cpu":0,"memory":12,"network":{"rx":2058.9950837430215,"tx":2090.8924256311975}},{"timestamp":1769476587104,"cpu":0,"memory":12,"network":{"rx":2063.3027370104446,"tx":2095.1873261257056}},{"timestamp":1769476647131,"cpu":0,"memory":12,"network":{"rx":2063.8056842035116,"tx":2095.691866857695}},{"timestamp":1769476707159,"cpu":0,"memory":12,"network":{"rx":2069.21769840741,"tx":2101.102818684614}},{"timestamp":1769476767164,"cpu":0,"memory":12,"network":{"rx":4360.603283059745,"tx":4416.631947337722}},{"timestamp":1769476827193,"cpu":0,"memory":12,"network":{"rx":2062.0699995002415,"tx":2093.9545886155024}},{"timestamp":1769476887217,"cpu":0,"memory":12,"network":{"rx":2041.4160766347354,"tx":2073.3027905039567}},{"timestamp":1769476947250,"cpu":0,"memory":12,"network":{"rx":2052.804290973298,"tx":2084.6867556177435}},{"timestamp":1769477007273,"cpu":0,"memory":12,"network":{"rx":2052.1308853420414,"tx":2084.0191929625803}},{"timestamp":1769477067298,"cpu":0,"memory":12,"network":{"rx":2062.9893712724484,"tx":2094.8755539266317}},{"timestamp":1769477127328,"cpu":0,"memory":12,"network":{"rx":2080.2112312382346,"tx":2112.0958203534956}},{"timestamp":1769477187352,"cpu":0,"memory":12,"network":{"rx":2568.213244481466,"tx":2600.0999583506873}},{"timestamp":1769477247379,"cpu":0,"memory":12,"network":{"rx":2056.3246539057427,"tx":2088.2103053625865}},{"timestamp":1769477307421,"cpu":0,"memory":12,"network":{"rx":2050.364744678725,"tx":2082.2424302987906}},{"timestamp":1769477367433,"cpu":0,"memory":12,"network":{"rx":2081.25041658335,"tx":2113.144037859095}},{"timestamp":1769477427469,"cpu":0,"memory":12,"network":{"rx":2052.0029982510205,"tx":2083.8844007662196}},{"timestamp":1769477487476,"cpu":0,"memory":12,"network":{"rx":4192.307692307692,"tx":4247.300359952006}},{"timestamp":1769477547500,"cpu":0,"memory":12,"network":{"rx":2051.08041917265,"tx":2082.9681955250485}},{"timestamp":1769477607529,"cpu":0,"memory":12,"network":{"rx":2076.612970397641,"tx":2108.497559512902}},{"timestamp":1769477667552,"cpu":0,"memory":12,"network":{"rx":2055.877648940424,"tx":2087.7648940423833}},{"timestamp":1769477727579,"cpu":0,"memory":12,"network":{"rx":2065.0707181768203,"tx":2096.9563696336645}},{"timestamp":1769477787606,"cpu":0,"memory":12,"network":{"rx":2053.6267617365806,"tx":2085.512944390764}},{"timestamp":1769477847634,"cpu":0,"memory":12,"network":{"rx":2059.055773972146,"tx":2090.94089424935}},{"timestamp":1769477907661,"cpu":0,"memory":12,"network":{"rx":2055.3908176184445,"tx":2087.275937895649}},{"timestamp":1769477967691,"cpu":0,"memory":12,"network":{"rx":2071.182261906745,"tx":2103.066851022006}},{"timestamp":1769478027718,"cpu":0,"memory":12,"network":{"rx":2061.304724461918,"tx":2093.189844739122}},{"timestamp":1769478087748,"cpu":0,"memory":12,"network":{"rx":4571.207249829249,"tx":4678.955171667028}},{"timestamp":1769478147770,"cpu":0,"memory":12,"network":{"rx":2050.530629925195,"tx":2082.4184062775935}},{"timestamp":1769478207802,"cpu":0,"memory":12,"network":{"rx":2061.217350746269,"tx":2093.1003464818764}},{"timestamp":1769478267829,"cpu":0,"memory":12,"network":{"rx":2058.7911904841235,"tx":2090.6773731383064}},{"timestamp":1769478327855,"cpu":0,"memory":12,"network":{"rx":2060.339513885418,"tx":2092.2251653422627}},{"timestamp":1769478387879,"cpu":0,"memory":12,"network":{"rx":2048.330667732907,"tx":2080.217912834866}},{"timestamp":1769478447882,"cpu":0,"memory":12,"network":{"rx":4369.431528423578,"tx":4427.728613569322}},{"timestamp":1769478507909,"cpu":0,"memory":12,"network":{"rx":2064.837489796258,"tx":2096.7231412531028}},{"timestamp":1769478567926,"cpu":0,"memory":12,"network":{"rx":2055.3176599963344,"tx":2087.2086242231367}},{"timestamp":1769478627954,"cpu":0,"memory":12,"network":{"rx":2046.778170187246,"tx":2078.66329046445}},{"timestamp":1769478687960,"cpu":0,"memory":12,"network":{"rx":2054.112157320223,"tx":2086.009499208399}},{"timestamp":1769478747982,"cpu":0,"memory":12,"network":{"rx":2042.533695416757,"tx":2074.421471769155}},{"timestamp":1769478808007,"cpu":0,"memory":12,"network":{"rx":2058.9587671803415,"tx":2090.8454810495627}},{"timestamp":1769478868039,"cpu":0,"memory":12,"network":{"rx":2060.9841417910447,"tx":2091.701092750533}},{"timestamp":1769478928063,"cpu":0,"memory":12,"network":{"rx":2071.1393965646503,"tx":2103.027172917048}},{"timestamp":1769478988090,"cpu":0,"memory":12,"network":{"rx":4551.092823349104,"tx":4658.842540147931}},{"timestamp":1769479048120,"cpu":0,"memory":12,"network":{"rx":2052.173913043478,"tx":2084.0579710144925}},{"timestamp":1769479108145,"cpu":0,"memory":12,"network":{"rx":2057.2761349437733,"tx":2089.1628488129945}},{"timestamp":1769479168174,"cpu":0,"memory":12,"network":{"rx":2046.860684002732,"tx":2078.745273117993}},{"timestamp":1769479228204,"cpu":0,"memory":12,"network":{"rx":2044.6276861569213,"tx":2076.511744127936}},{"timestamp":1769479288231,"cpu":0,"memory":12,"network":{"rx":2044.6798940476785,"tx":2076.565545504523}},{"timestamp":1769479348255,"cpu":0,"memory":12,"network":{"rx":2050.9629481540715,"tx":2082.850193256031}},{"timestamp":1769479408285,"cpu":0,"memory":12,"network":{"rx":2069.5330590214726,"tx":2101.417648136734}},{"timestamp":1769479468313,"cpu":0,"memory":12,"network":{"rx":2058.9381798797244,"tx":2090.822768994986}},{"timestamp":1769479528310,"cpu":1,"memory":19,"network":{"rx":44763.250883392226,"tx":44849.6399759984}},{"timestamp":1769479542955,"cpu":0,"memory":13,"network":{"rx":0,"tx":0}},{"timestamp":1769479602957,"cpu":0,"memory":13,"network":{"rx":2080.9306356454786,"tx":2113.6295456818107}},{"timestamp":1769479662941,"cpu":0,"memory":13,"network":{"rx":3881.749973581317,"tx":3930.5717003064574}},{"timestamp":1769479722944,"cpu":0,"memory":13,"network":{"rx":2437.2875141657223,"tx":2475.7849476701554}},{"timestamp":1769479782967,"cpu":0,"memory":13,"network":{"rx":2144.6802838959047,"tx":2178.767785145447}},{"timestamp":1769479842997,"cpu":0,"memory":13,"network":{"rx":2040.2791890856392,"tx":2072.162715930103}},{"timestamp":1769479903022,"cpu":0,"memory":13,"network":{"rx":4594.08579758434,"tx":4705.139525197834}},{"timestamp":1769479963030,"cpu":0,"memory":13,"network":{"rx":2802.0097320357286,"tx":2848.2035728569526}},{"timestamp":1769480023056,"cpu":0,"memory":13,"network":{"rx":2052.661391087047,"tx":2084.548104956268}},{"timestamp":1769480083057,"cpu":0,"memory":13,"network":{"rx":5416.745570133911,"tx":5503.0434194508325}},{"timestamp":1769480143050,"cpu":0,"memory":13,"network":{"rx":4776.573933625589,"tx":4860.183688096945}},{"timestamp":1769480203076,"cpu":0,"memory":13,"network":{"rx":3509.42107455227,"tx":3568.796334860475}},{"timestamp":1769480225862,"cpu":0,"memory":13,"network":{"rx":0,"tx":0}},{"timestamp":1769480285844,"cpu":0,"memory":13,"network":{"rx":2191.97425894435,"tx":2229.085392284352}},{"timestamp":1769480345866,"cpu":0,"memory":13,"network":{"rx":2308.1536769851054,"tx":2344.44037186365}},{"timestamp":1769480405892,"cpu":0,"memory":13,"network":{"rx":2295.938426681771,"tx":2332.222703495152}},{"timestamp":1769480465918,"cpu":0,"memory":13,"network":{"rx":2309.1160497117917,"tx":2345.400326525172}},{"timestamp":1769480525945,"cpu":0,"memory":13,"network":{"rx":3567.7611741383043,"tx":3626.0349509387443}},{"timestamp":1769480585978,"cpu":0,"memory":13,"network":{"rx":2065.830459913714,"tx":2097.7129245581596}},{"timestamp":1769480646003,"cpu":0,"memory":13,"network":{"rx":2068.9712619741777,"tx":2100.8579758433984}},{"timestamp":1769480706029,"cpu":0,"memory":13,"network":{"rx":2072.668510312198,"tx":2104.554692966381}},{"timestamp":1769480766060,"cpu":0,"memory":13,"network":{"rx":2432.5265279605537,"tx":2462.2111908847096}},{"timestamp":1769480826084,"cpu":0,"memory":13,"network":{"rx":2208.9997334399573,"tx":2243.0860988937757}},{"timestamp":1769480886116,"cpu":0,"memory":13,"network":{"rx":1918.5420865885958,"tx":1948.2267495127517}},{"timestamp":1769480946140,"cpu":0,"memory":13,"network":{"rx":2078.401972544316,"tx":2110.2892176462747}},{"timestamp":1769481006166,"cpu":0,"memory":13,"network":{"rx":2063.3548236626852,"tx":2095.2404751195295}},{"timestamp":1769481066194,"cpu":0,"memory":13,"network":{"rx":2062.220963550343,"tx":2094.106083827547}},{"timestamp":1769481126222,"cpu":0,"memory":13,"network":{"rx":2060.705004331312,"tx":2092.590124608516}},{"timestamp":1769481186229,"cpu":0,"memory":13,"network":{"rx":154340.26597340268,"tx":6771.672832716728}},{"timestamp":1769481246254,"cpu":0,"memory":13,"network":{"rx":6104.506455643482,"tx":6201.266139108705}},{"timestamp":1769481306282,"cpu":0,"memory":13,"network":{"rx":2068.466907661297,"tx":2100.3514967765577}},{"timestamp":1769481366311,"cpu":0,"memory":13,"network":{"rx":2088.823735194656,"tx":2120.708324309917}},{"timestamp":1769481426342,"cpu":0,"memory":13,"network":{"rx":2084.506338391831,"tx":2116.3898652362946}},{"timestamp":1769481486369,"cpu":0,"memory":13,"network":{"rx":2063.3392196714753,"tx":2095.2254023256587}},{"timestamp":1769481546400,"cpu":0,"memory":13,"network":{"rx":2071.2619936034116,"tx":2103.1449893390195}},{"timestamp":1769481606422,"cpu":0,"memory":13,"network":{"rx":2075.6901751053797,"tx":2107.5790140117624}},{"timestamp":1769481666433,"cpu":0,"memory":13,"network":{"rx":4625.758181696994,"tx":4736.8359661401055}},{"timestamp":1769481726461,"cpu":0,"memory":13,"network":{"rx":2082.478176850803,"tx":2114.3632971280067}},{"timestamp":1769481786489,"cpu":0,"memory":13,"network":{"rx":2076.8974478576665,"tx":2108.7825681348704}},{"timestamp":1769481846516,"cpu":0,"memory":13,"network":{"rx":2081.4300231562465,"tx":2113.315674613091}},{"timestamp":1769481906545,"cpu":0,"memory":13,"network":{"rx":2076.9794599277016,"tx":2108.8640490429625}},{"timestamp":1769481966572,"cpu":0,"memory":13,"network":{"rx":2086.279278979109,"tx":2118.165461633292}},{"timestamp":1769482026578,"cpu":0,"memory":13,"network":{"rx":2884.813438432183,"tx":2936.5074074691283}},{"timestamp":1769482070677,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769482130682,"cpu":0,"memory":14,"network":{"rx":1977.3518873427215,"tx":2005.2162319806682}},{"timestamp":1769482190705,"cpu":0,"memory":14,"network":{"rx":2063.8421938257,"tx":2095.7299701780985}},{"timestamp":1769482250705,"cpu":0,"memory":13,"network":{"rx":2067.784463074385,"tx":2098.5183086384773}},{"timestamp":1769482310735,"cpu":0,"memory":14,"network":{"rx":2065.91704147926,"tx":2097.8010994502747}},{"timestamp":1769482370763,"cpu":0,"memory":13,"network":{"rx":2069.2165453364205,"tx":2101.1011344516814}},{"timestamp":1769482430789,"cpu":0,"memory":14,"network":{"rx":2060.5737513744043,"tx":2092.4599340285877}},{"timestamp":1769482490818,"cpu":0,"memory":13,"network":{"rx":2074.3807159872727,"tx":2106.2653051025336}},{"timestamp":1769482550846,"cpu":0,"memory":14,"network":{"rx":2068.134870393816,"tx":2100.0199906710204}},{"timestamp":1769482610844,"cpu":2,"memory":14,"network":{"rx":18048.80162672089,"tx":18344.744824827496}},{"timestamp":1769482619544,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769482679556,"cpu":0,"memory":14,"network":{"rx":1957.9084183163368,"tx":1988.4023195360928}},{"timestamp":1769482739554,"cpu":0,"memory":14,"network":{"rx":2885.912863762125,"tx":2937.614587152905}},{"timestamp":1769482799583,"cpu":0,"memory":14,"network":{"rx":2068.633493811324,"tx":2100.5180829265855}},{"timestamp":1769482859589,"cpu":0,"memory":14,"network":{"rx":154418.5914741859,"tx":6801.203213012032}},{"timestamp":1769482880007,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769482940015,"cpu":0,"memory":14,"network":{"rx":1975.9194760611263,"tx":2007.5157898245204}},{"timestamp":1769483000029,"cpu":0,"memory":14,"network":{"rx":2058.035491127218,"tx":2089.92751812047}},{"timestamp":1769483060060,"cpu":0,"memory":14,"network":{"rx":2062.3677766487317,"tx":2094.2513034931953}},{"timestamp":1769483120085,"cpu":0,"memory":14,"network":{"rx":2063.6578701852595,"tx":2095.5451152872183}},{"timestamp":1769483180115,"cpu":0,"memory":14,"network":{"rx":2055.7878429478105,"tx":2087.671369792274}},{"timestamp":1769483240141,"cpu":0,"memory":14,"network":{"rx":2062.5905872553103,"tx":2094.4773011245316}},{"timestamp":1769483300170,"cpu":0,"memory":14,"network":{"rx":2065.184074629352,"tx":2097.0681326003664}},{"timestamp":1769483360196,"cpu":0,"memory":14,"network":{"rx":2057.0586079365607,"tx":2088.944790590744}},{"timestamp":1769483420224,"cpu":0,"memory":14,"network":{"rx":2065.837039998667,"tx":2097.7226914555117}},{"timestamp":1769483480252,"cpu":0,"memory":14,"network":{"rx":2571.6332378223497,"tx":2603.5183580995536}},{"timestamp":1769483540282,"cpu":0,"memory":14,"network":{"rx":2057.8034682080925,"tx":2089.686995052556}},{"timestamp":1769483600276,"cpu":0,"memory":15,"network":{"rx":10299.0799079908,"tx":10383.821715504884}},{"timestamp":1769483618147,"cpu":0,"memory":14,"network":{"rx":0,"tx":0}},{"timestamp":1769483678121,"cpu":0,"memory":15,"network":{"rx":17098.42995169082,"tx":17297.70531400966}},{"timestamp":1769483738119,"cpu":2,"memory":14,"network":{"rx":28958.163265306124,"tx":29294.897959183672}},{"timestamp":1769483798134,"cpu":0,"memory":14,"network":{"rx":2879.0529660094876,"tx":2922.139530835183}},{"timestamp":1769483858149,"cpu":0,"memory":14,"network":{"rx":2880.1164508560005,"tx":2923.133744677153}},{"timestamp":1769483918165,"cpu":0,"memory":14,"network":{"rx":2903.010033444816,"tx":2946.0105112279025}},{"timestamp":1769483978181,"cpu":0,"memory":14,"network":{"rx":2576.7878077373975,"tx":2614.0419434675005}},{"timestamp":1769484038190,"cpu":0,"memory":14,"network":{"rx":2559.6788194444443,"tx":2596.918402777778}},{"timestamp":1769484098206,"cpu":0,"memory":14,"network":{"rx":2465.8164150616,"tx":2503.0366128752385}},{"timestamp":1769484158221,"cpu":0,"memory":14,"network":{"rx":2444.8634590377114,"tx":2482.0546163849153}},{"timestamp":1769484218236,"cpu":0,"memory":14,"network":{"rx":2479.993071193487,"tx":2517.1487961198686}},{"timestamp":1769484278248,"cpu":0,"memory":14,"network":{"rx":2480.4885443284684,"tx":2517.6490969725846}},{"timestamp":1769484338259,"cpu":0,"memory":14,"network":{"rx":2466.2425344066473,"tx":2503.3757465593353}},{"timestamp":1769484398275,"cpu":0,"memory":14,"network":{"rx":2473.7434034085995,"tx":2510.857340600398}},{"timestamp":1769484458290,"cpu":0,"memory":14,"network":{"rx":2475.066977789301,"tx":2512.1424250280875}},{"timestamp":1769484518306,"cpu":0,"memory":14,"network":{"rx":2483.7414172820313,"tx":2520.7928488146135}},{"timestamp":1769484578323,"cpu":0,"memory":14,"network":{"rx":2453.20792164646,"tx":2490.227380592829}},{"timestamp":1769484638337,"cpu":0,"memory":14,"network":{"rx":2446.1830251303936,"tx":2483.16737790422}},{"timestamp":1769484698354,"cpu":0,"memory":14,"network":{"rx":2470.560344827586,"tx":2507.5431034482763}},{"timestamp":1769484758366,"cpu":0,"memory":14,"network":{"rx":2487.137501615892,"tx":2524.109105011419}},{"timestamp":1769484818381,"cpu":0,"memory":14,"network":{"rx":2482.133631823661,"tx":2519.071809884622}},{"timestamp":1769484878397,"cpu":0,"memory":14,"network":{"rx":2452.769290355898,"tx":2489.69316176787}},{"timestamp":1769484938414,"cpu":0,"memory":14,"network":{"rx":2475.9447955630076,"tx":2512.8337417773764}},{"timestamp":1769484998433,"cpu":0,"memory":14,"network":{"rx":2477.564515436472,"tx":2514.405942719739}},{"timestamp":1769485058446,"cpu":0,"memory":14,"network":{"rx":2465.4557945811325,"tx":2502.2972218643995}},{"timestamp":1769485118460,"cpu":0,"memory":14,"network":{"rx":2452.0559704695684,"tx":2488.8831659369907}},{"timestamp":1769485178474,"cpu":0,"memory":14,"network":{"rx":2443.9584941257185,"tx":2480.7477917845813}},{"timestamp":1769485238489,"cpu":0,"memory":14,"network":{"rx":2435.8710562414267,"tx":2472.6508916323733}},{"timestamp":1769485298503,"cpu":0,"memory":14,"network":{"rx":2450.85021630188,"tx":2487.6001199297552}},{"timestamp":1769485358522,"cpu":0,"memory":14,"network":{"rx":2451.80053032247,"tx":2488.4954238302967}},{"timestamp":1769485418534,"cpu":0,"memory":14,"network":{"rx":2452.53164556962,"tx":2489.2234006158055}},{"timestamp":1769485478551,"cpu":0,"memory":14,"network":{"rx":2427.246698859023,"tx":2463.911798641084}},{"timestamp":1769485538567,"cpu":0,"memory":14,"network":{"rx":2427.0277196429333,"tx":2463.6740272498187}},{"timestamp":1769485598581,"cpu":0,"memory":14,"network":{"rx":2426.8277068840425,"tx":2463.44586231915}},{"timestamp":1769485658597,"cpu":0,"memory":14,"network":{"rx":2471.0849539406345,"tx":2507.6765609007166}},{"timestamp":1769485718612,"cpu":0,"memory":14,"network":{"rx":2467.240717848161,"tx":2503.815166886909}},{"timestamp":1769485778630,"cpu":0,"memory":14,"network":{"rx":2441.108840061318,"tx":2477.6443536024526}},{"timestamp":1769485838641,"cpu":0,"memory":14,"network":{"rx":2452.1842799965934,"tx":2488.716682278804}},{"timestamp":1769485898656,"cpu":0,"memory":14,"network":{"rx":2446.5060856243085,"tx":2480.040854540812}},{"timestamp":1769485958674,"cpu":0,"memory":14,"network":{"rx":2440.556169742325,"tx":2477.0388638489667}},{"timestamp":1769486018691,"cpu":0,"memory":14,"network":{"rx":2430.7934214440525,"tx":2467.255960222685}},{"timestamp":1769486078703,"cpu":0,"memory":14,"network":{"rx":2422.5071104130407,"tx":2458.929405272318}},{"timestamp":1769486138718,"cpu":0,"memory":14,"network":{"rx":2418.6105691746543,"tx":2455.0004241241836}},{"timestamp":1769486198740,"cpu":0,"memory":14,"network":{"rx":2427.808619739797,"tx":2464.169174047548}},{"timestamp":1769486258750,"cpu":0,"memory":14,"network":{"rx":2417.6410777834267,"tx":2453.9908490086427}},{"timestamp":1769486318765,"cpu":0,"memory":14,"network":{"rx":2436.3544003725183,"tx":2472.6749354442704}},{"timestamp":1769486378781,"cpu":0,"memory":14,"network":{"rx":2429.4421400152373,"tx":2465.7580631507662}},{"timestamp":1769486438798,"cpu":0,"memory":14,"network":{"rx":2428.4626770987525,"tx":2464.7494184817087}},{"timestamp":1769486498810,"cpu":0,"memory":14,"network":{"rx":2446.1824638538938,"tx":2482.4553986640735}},{"timestamp":1769486558830,"cpu":0,"memory":14,"network":{"rx":2441.5952008787126,"tx":2477.842085251996}},{"timestamp":1769486618843,"cpu":0,"memory":14,"network":{"rx":2413.5455812185955,"tx":2449.774099565089}},{"timestamp":1769486678857,"cpu":0,"memory":14,"network":{"rx":2441.9104628614787,"tx":2478.0794199477277}},{"timestamp":1769486738874,"cpu":0,"memory":14,"network":{"rx":2429.263565891473,"tx":2465.41118975396}},{"timestamp":1769486798886,"cpu":0,"memory":14,"network":{"rx":2442.8812131423756,"tx":2479.0227464195455}},{"timestamp":1769486858902,"cpu":0,"memory":14,"network":{"rx":2451.822203496946,"tx":2487.971350326522}},{"timestamp":1769486918917,"cpu":0,"memory":14,"network":{"rx":2406.1144565629343,"tx":2442.2453362530005}},{"timestamp":1769486978932,"cpu":0,"memory":14,"network":{"rx":2404.640800369919,"tx":2440.7078902013536}},{"timestamp":1769487038946,"cpu":0,"memory":14,"network":{"rx":2409.090909090909,"tx":2445.1686149188463}},{"timestamp":1769487098963,"cpu":0,"memory":14,"network":{"rx":2418.3506280720917,"tx":2454.396504642272}},{"timestamp":1769487158980,"cpu":0,"memory":14,"network":{"rx":2435.257082896118,"tx":2471.269674711438}},{"timestamp":1769487218981,"cpu":0,"memory":14,"network":{"rx":2383.2536286601226,"tx":2419.2465810890176}},{"timestamp":1769487278998,"cpu":0,"memory":14,"network":{"rx":2413.6889783593356,"tx":2449.6728736789128}},{"timestamp":1769487339013,"cpu":0,"memory":14,"network":{"rx":2392.6071832697708,"tx":2428.5654415154436}},{"timestamp":1769487399013,"cpu":1,"memory":14,"network":{"rx":380788.59369761986,"tx":9325.972175662084}},{"timestamp":1769487459033,"cpu":0,"memory":15,"network":{"rx":2394.60681684951,"tx":2430.5334561594505}},{"timestamp":1769487482870,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769487542896,"cpu":0,"memory":15,"network":{"rx":2128.0724179116987,"tx":2159.880979821051}},{"timestamp":1769487595469,"cpu":0,"memory":15,"network":{"rx":0,"tx":0}},{"timestamp":1769487655462,"cpu":0,"memory":15,"network":{"rx":2248.528644863294,"tx":2276.3912687178727}},{"timestamp":1769487715471,"cpu":0,"memory":15,"network":{"rx":2243.0834947598473,"tx":2277.504594446928}},{"timestamp":1769487775490,"cpu":0,"memory":15,"network":{"rx":2232.5021709465327,"tx":2266.8899640243144}},{"timestamp":1769487835520,"cpu":0,"memory":15,"network":{"rx":2242.2378732268626,"tx":2276.60946334689}},{"timestamp":1769487895545,"cpu":0,"memory":14,"network":{"rx":2420.0545364402574,"tx":2457.6846802181453}},{"timestamp":1769487955570,"cpu":0,"memory":14,"network":{"rx":2482.703117648515,"tx":2520.293192680088}},{"timestamp":1769488015595,"cpu":0,"memory":14,"network":{"rx":2508.6277325279134,"tx":2546.2085014730274}},{"timestamp":1769488075623,"cpu":0,"memory":14,"network":{"rx":2492.4907835811664,"tx":2530.049236707326}},{"timestamp":1769488135648,"cpu":0,"memory":14,"network":{"rx":2487.157301426417,"tx":2524.684185805048}},{"timestamp":1769488195672,"cpu":0,"memory":14,"network":{"rx":2492.7235440911227,"tx":2530.230029896474}},{"timestamp":1769488255697,"cpu":0,"memory":14,"network":{"rx":2470.672495122373,"tx":2508.16230767331}},{"timestamp":1769488315692,"cpu":0,"memory":15,"network":{"rx":6337.059064807218,"tx":6425.041017227235}},{"timestamp":1769488375690,"cpu":0,"memory":15,"network":{"rx":4887.212907096903,"tx":4971.915730524352}},{"timestamp":1769488435694,"cpu":0,"memory":15,"network":{"rx":4368.542097193521,"tx":4440.037330844611}},{"timestamp":1769488495690,"cpu":2,"memory":17,"network":{"rx":488701.43507175357,"tx":17339.200293348}},{"timestamp":1769488555736,"cpu":1,"memory":17,"network":{"rx":98691.13165126154,"tx":6176.017986510117}},{"timestamp":1769488615737,"cpu":4,"memory":15,"network":{"rx":427669.8221696305,"tx":16580.923651272482}},{"timestamp":1769488675758,"cpu":0,"memory":15,"network":{"rx":2088.0510479490854,"tx":2126.5369364566327}},{"timestamp":1769488735775,"cpu":0,"memory":15,"network":{"rx":2084.9592615425627,"tx":2124.5480447206623}},{"timestamp":1769488795802,"cpu":0,"memory":15,"network":{"rx":2098.9071402392296,"tx":2138.4899876720087}},{"timestamp":1769488855824,"cpu":0,"memory":15,"network":{"rx":2070.8728320810355,"tx":2110.457657897806}},{"timestamp":1769488915852,"cpu":0,"memory":15,"network":{"rx":2600.236556273739,"tx":2639.8180848937163}},{"timestamp":1769488975879,"cpu":0,"memory":15,"network":{"rx":2075.8504647985874,"tx":2115.4333122313665}},{"timestamp":1769489035904,"cpu":0,"memory":15,"network":{"rx":2083.9302968713555,"tx":2123.5131443041346}},{"timestamp":1769489095938,"cpu":0,"memory":15,"network":{"rx":2076.3746606033346,"tx":2115.95289257575}},{"timestamp":1769489155954,"cpu":0,"memory":15,"network":{"rx":2092.991868834977,"tx":2132.581311650227}},{"timestamp":1769489215979,"cpu":0,"memory":15,"network":{"rx":2084.9979175343606,"tx":2124.5814244064973}},{"timestamp":1769489276005,"cpu":0,"memory":15,"network":{"rx":2093.376203645087,"tx":2132.959051077866}},{"timestamp":1769489336025,"cpu":0,"memory":15,"network":{"rx":2070.4598467177607,"tx":2110.0466511162945}},{"timestamp":1769489396056,"cpu":0,"memory":15,"network":{"rx":2073.4287284902803,"tx":2113.0082790558213}},{"timestamp":1769489456082,"cpu":0,"memory":15,"network":{"rx":2081.364741945157,"tx":2119.7814280478456}},{"timestamp":1769489516108,"cpu":0,"memory":15,"network":{"rx":2073.5348015859795,"tx":2113.117649018758}},{"timestamp":1769489576133,"cpu":0,"memory":15,"network":{"rx":2079.000416493128,"tx":2118.5839233652646}},{"timestamp":1769489636153,"cpu":0,"memory":15,"network":{"rx":2071.976007997334,"tx":2111.562812395868}},{"timestamp":1769489696180,"cpu":0,"memory":15,"network":{"rx":2076.7974945025653,"tx":2116.379023122543}},{"timestamp":1769489756207,"cpu":0,"memory":15,"network":{"rx":2087.1122513577448,"tx":2126.695098790524}},{"timestamp":1769489816231,"cpu":0,"memory":15,"network":{"rx":4850.593096094895,"tx":4969.345595095296}},{"timestamp":1769489876251,"cpu":0,"memory":15,"network":{"rx":2917.627457514162,"tx":2977.007664111962}},{"timestamp":1769489936275,"cpu":0,"memory":15,"network":{"rx":2077.219112355058,"tx":2116.8032786885246}},{"timestamp":1769489996309,"cpu":1,"memory":15,"network":{"rx":153417.36349402007,"tx":6730.319485624813}},{"timestamp":1769490056342,"cpu":0,"memory":15,"network":{"rx":2085.619575899922,"tx":2125.197807872337}},{"timestamp":1769490116357,"cpu":0,"memory":15,"network":{"rx":2074.8966942148763,"tx":2114.486137030125}},{"timestamp":1769490176351,"cpu":0,"memory":15,"network":{"rx":2076.1422165919357,"tx":2115.7468371309988}},{"timestamp":1769490236384,"cpu":0,"memory":15,"network":{"rx":2072.52677693935,"tx":2112.1050089117653}},{"timestamp":1769490296401,"cpu":0,"memory":15,"network":{"rx":2082.9251224632612,"tx":2122.513246026192}},{"timestamp":1769490356424,"cpu":0,"memory":15,"network":{"rx":2073.938323642604,"tx":2113.5231494593736}},{"timestamp":1769490416447,"cpu":0,"memory":15,"network":{"rx":2094.4986838159343,"tx":2134.084169137983}},{"timestamp":1769490476472,"cpu":0,"memory":15,"network":{"rx":2071.670137442732,"tx":2111.253644314869}},{"timestamp":1769490536496,"cpu":0,"memory":15,"network":{"rx":2069.121199500208,"tx":2108.704706372345}},{"timestamp":1769490596524,"cpu":0,"memory":15,"network":{"rx":2078.248121678578,"tx":2117.8303096939712}},{"timestamp":1769490656547,"cpu":0,"memory":15,"network":{"rx":2071.789147493461,"tx":2111.373973310231}},{"timestamp":1769490716571,"cpu":0,"memory":15,"network":{"rx":4823.14035818409,"tx":4945.189504373177}},{"timestamp":1769490776600,"cpu":0,"memory":15,"network":{"rx":2073.8155527420536,"tx":2113.3970813620313}},{"timestamp":1769490836621,"cpu":0,"memory":15,"network":{"rx":2087.184698943721,"tx":2126.7701842657693}},{"timestamp":1769490896650,"cpu":0,"memory":15,"network":{"rx":2078.145562977894,"tx":2117.7264322244246}},{"timestamp":1769490956672,"cpu":0,"memory":15,"network":{"rx":2093.7989403885244,"tx":2133.384425710573}},{"timestamp":1769491016701,"cpu":0,"memory":15,"network":{"rx":2071.565410051808,"tx":2111.146279298339}},{"timestamp":1769491076694,"cpu":0,"memory":15,"network":{"rx":529.8804780876494,"tx":529.8804780876494}},{"timestamp":1769491136718,"cpu":0,"memory":15,"network":{"rx":2077.554819458512,"tx":2115.4972761367676}},{"timestamp":1769491196722,"cpu":3,"memory":18,"network":{"rx":476237.8977844297,"tx":15721.299408690533}},{"timestamp":1769491256744,"cpu":8,"memory":30,"network":{"rx":619107.8629517348,"tx":24597.300418651652}},{"timestamp":1769491316778,"cpu":0,"memory":16,"network":{"rx":2084.5436183514475,"tx":2122.6315283254785}}] \ No newline at end of file diff --git a/frontend/backend/data/icon.svg b/frontend/backend/data/icon.svg new file mode 100644 index 0000000..98247d0 --- /dev/null +++ b/frontend/backend/data/icon.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/backend/data/pwa-192x192.png b/frontend/backend/data/pwa-192x192.png new file mode 100644 index 0000000..e358ab0 Binary files /dev/null and b/frontend/backend/data/pwa-192x192.png differ diff --git a/frontend/backend/data/pwa-512x512.png b/frontend/backend/data/pwa-512x512.png new file mode 100644 index 0000000..6abe905 Binary files /dev/null and b/frontend/backend/data/pwa-512x512.png differ diff --git a/frontend/backend/data/subscriptions.json b/frontend/backend/data/subscriptions.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/frontend/backend/data/subscriptions.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/frontend/backend/data/vapid.json b/frontend/backend/data/vapid.json new file mode 100644 index 0000000..c0136b5 --- /dev/null +++ b/frontend/backend/data/vapid.json @@ -0,0 +1 @@ +{"publicKey":"BFOD5bzNh9Dch_Rjt8cpWc2IfSktAoEUx0virpARRvPfpmsuvQka3ppCa0WajNFaIJS7Z62VNUAeo_6yMBvLVZA","privateKey":"ymBn65iLw_M19FJPdzJ7FqA7jgHVBCfSV6vSdM_8GIw"} \ No newline at end of file diff --git a/frontend/backend/package.json b/frontend/backend/package.json new file mode 100644 index 0000000..252dcf5 --- /dev/null +++ b/frontend/backend/package.json @@ -0,0 +1,21 @@ +{ + "name": "snstatus-backend", + "version": "1.0.0", + "description": "System monitoring backend for Synology NAS", + "main": "server.js", + "type": "module", + "scripts": { + "start": "node server.js", + "dev": "node --watch server.js" + }, + "dependencies": { + "cors": "^2.8.5", + "dockerode": "^4.0.0", + "express": "^4.18.2", + "socket.io": "^4.7.2", + "systeminformation": "^5.21.22", + "web-push": "^3.6.0" + }, + "author": "", + "license": "ISC" +} \ No newline at end of file diff --git a/frontend/backend/server.js b/frontend/backend/server.js new file mode 100644 index 0000000..e746670 --- /dev/null +++ b/frontend/backend/server.js @@ -0,0 +1,785 @@ +import express from 'express'; +import cors from 'cors'; +import si from 'systeminformation'; +import Docker from 'dockerode'; +import { createServer } from 'http'; +import { Server } from 'socket.io'; +import fs from 'fs'; +import path from 'path'; +import webpush from 'web-push'; +import crypto from 'crypto'; + +const app = express(); +const httpServer = createServer(app); +const io = new Server(httpServer, { + cors: { + origin: "*", + methods: ["GET", "POST"] + } +}); +const PORT = 8001; + +// Docker Connection +const docker = new Docker({ socketPath: '/var/run/docker.sock' }); + +app.use(cors()); +app.use(express.json()); + +// Helper: Read Synology Info from Host +const getSynologyInfo = () => { + try { + const versionPath = '/host/etc/VERSION'; + const synoInfoPath = '/host/etc/synoinfo.conf'; + + let dsmVersion = 'Unknown'; + let model = 'Synology NAS'; + + if (fs.existsSync(versionPath)) { + const versionContent = fs.readFileSync(versionPath, 'utf8'); + const productVersion = versionContent.match(/productversion="([^"]+)"/); + const buildNumber = versionContent.match(/buildnumber="([^"]+)"/); + if (productVersion && buildNumber) { + dsmVersion = `DSM ${productVersion[1]}-${buildNumber[1]}`; + } + } + + if (fs.existsSync(synoInfoPath)) { + // Try to read model from synoinfo (unique="synology_geminilake_920+") or similar + // Or usually /proc/sys/kernel/syno_hw_version if available + // Simpler fallback: check hostname or use generic if deep parsing fails. + // Actually synoinfo.conf has 'upnpmodelname="DS920+"' usually. + const infoContent = fs.readFileSync(synoInfoPath, 'utf8'); + const modelMatch = infoContent.match(/upnpmodelname="([^"]+)"/); + if (modelMatch) { + model = modelMatch[1]; + } + } + + return { model, dsmVersion }; + } catch (e) { + console.error('Failed to read host files:', e); + return { model: 'Docker Container', dsmVersion: 'Unknown' }; + } +}; + +// --- Docker Config & Security --- +const DATA_DIR = path.join(process.cwd(), 'data'); +const CONFIG_FILE = path.join(DATA_DIR, 'config.json'); + +// Ensure data directory exists +if (!fs.existsSync(DATA_DIR)) { + try { fs.mkdirSync(DATA_DIR, { recursive: true }); } + catch (e) { console.error('Failed to create data directory:', e); } +} + +let dockerConfig = { + enabled: false, + passwordHash: null, + salt: null, + retentionHours: 24, + alertThresholds: { cpu: 80, memory: 80, disk: 90 }, + containerAlertEnabled: false, + alertCooldownSeconds: 300 // 5 minutes default +}; + +const saveDockerConfig = () => { + try { fs.writeFileSync(CONFIG_FILE, JSON.stringify(dockerConfig, null, 2)); } + catch (e) { console.error('Failed to save docker config:', e); } +}; + +const loadDockerConfig = () => { + try { + if (fs.existsSync(CONFIG_FILE)) { + const data = fs.readFileSync(CONFIG_FILE, 'utf8'); + dockerConfig = JSON.parse(data); + // Merge defaults + if (!dockerConfig.alertThresholds) dockerConfig.alertThresholds = { cpu: 90, mem: 90, disk: 90 }; + // Restore history limit from saved retention hours + // Note: historyRetentionHours and HISTORY_LIMIT are defined later in the file + // This function is called AFTER those variables are initialized (line 438) + if (dockerConfig.retentionHours) { + historyRetentionHours = dockerConfig.retentionHours; + HISTORY_LIMIT = historyRetentionHours * 60; + } + } else { + saveDockerConfig(); + } + } catch (e) { console.error('Failed to load docker config:', e); } +}; +// I will Stub loadDockerConfig here and call the real logic later? +// No, I should move History Vars up too. + +// --- Helper: Secure Hash --- +const hashPassword = (password, salt = null) => { + if (!password) return null; + try { + if (!salt) salt = crypto.randomBytes(16).toString('hex'); + const hash = crypto.scryptSync(password, salt, 64).toString('hex'); + return { hash, salt }; + } catch (e) { console.error("Hashing error:", e); return null; } +}; + +// --- Push Notification System Variables --- +const SUBSCRIPTIONS_FILE = path.join(process.cwd(), 'data', 'subscriptions.json'); +let subscriptions = []; +let vapidKeys = { publicKey: '', privateKey: '' }; +let alertState = { cpu: 0, memory: 0, disk: 0 }; + +const initPushNotifications = () => { + // Ensure data directory exists + if (!fs.existsSync(process.cwd() + '/data')) { + fs.mkdirSync(process.cwd() + '/data', { recursive: true }); + } + + // Initialize VAPID keys + if (!fs.existsSync(process.cwd() + '/data/vapid.json')) { + vapidKeys = webpush.generateVAPIDKeys(); + fs.writeFileSync(process.cwd() + '/data/vapid.json', JSON.stringify(vapidKeys)); + } else { + vapidKeys = JSON.parse(fs.readFileSync(process.cwd() + '/data/vapid.json', 'utf8')); + } + webpush.setVapidDetails('mailto:admin@example.com', vapidKeys.publicKey, vapidKeys.privateKey); + + // Initialize subscriptions - CREATE FILE IF MISSING + if (fs.existsSync(SUBSCRIPTIONS_FILE)) { + try { + subscriptions = JSON.parse(fs.readFileSync(SUBSCRIPTIONS_FILE, 'utf8')); + console.log('[PUSH] Loaded', subscriptions.length, 'existing subscriptions'); + } catch (e) { + console.error('[PUSH] Error loading subscriptions:', e); + subscriptions = []; + } + } else { + console.log('[PUSH] Creating new subscriptions file'); + subscriptions = []; + fs.writeFileSync(SUBSCRIPTIONS_FILE, JSON.stringify(subscriptions)); + } +}; + +const saveSubscriptions = () => { + try { fs.writeFileSync(SUBSCRIPTIONS_FILE, JSON.stringify(subscriptions)); } + catch (e) { console.error('Failed to save subscriptions:', e); } +}; + +const sendNotification = (payload) => { + const notificationPayload = JSON.stringify(payload); + console.log('[PUSH] Sending notification to', subscriptions.length, 'subscribers'); + console.log('[PUSH] Payload:', payload); + + if (subscriptions.length === 0) { + console.warn('[PUSH] No subscriptions available!'); + return; + } + + subscriptions.forEach((subscription, index) => { + console.log(`[PUSH] Sending to subscription ${index + 1}/${subscriptions.length}`); + webpush.sendNotification(subscription, notificationPayload) + .then(() => { + console.log(`[PUSH] Successfully sent to subscription ${index + 1}`); + }) + .catch(error => { + console.error(`[PUSH] Error sending to subscription ${index + 1}:`, error); + if (error.statusCode === 410 || error.statusCode === 404) { + console.log('[PUSH] Removing expired subscription'); + subscriptions.splice(index, 1); + saveSubscriptions(); + } + }); + }); +}; + +const checkAlerts = (stats) => { + const now = Date.now(); + const thresholds = dockerConfig.alertThresholds || { cpu: 90, memory: 90, disk: 90 }; + const COOLDOWN = (dockerConfig.alertCooldownSeconds || 300) * 1000; // Convert seconds to ms + + if (stats.cpu > thresholds.cpu) { + if (now - alertState.cpu > COOLDOWN) { + console.log('[ALERT] CPU alert sent:', stats.cpu + '%'); + sendNotification({ title: 'snStatus 알림', body: `CPU 사용률 높음: ${stats.cpu}%` }); + alertState.cpu = now; + } + } else { alertState.cpu = 0; } + + if (stats.memory > thresholds.memory) { + if (now - alertState.memory > COOLDOWN) { + console.log('[ALERT] Memory alert sent:', stats.memory + '%'); + sendNotification({ title: 'snStatus 알림', body: `메모리 사용률 높음: ${stats.memory}%` }); + alertState.memory = now; + } + } else { alertState.memory = 0; } +}; + +// VAPID Public Key Endpoint +app.get('/api/notifications/vapidPublicKey', (req, res) => { + try { + if (!vapidKeys || !vapidKeys.publicKey) { + return res.status(500).json({ error: 'VAPID keys not initialized' }); + } + res.json({ publicKey: vapidKeys.publicKey }); + } catch (error) { + console.error('Error getting VAPID key:', error); + res.status(500).json({ error: 'Failed to get VAPID key' }); + } +}); + +// Subscribe to push notifications +app.post('/api/notifications/subscribe', async (req, res) => { + const subscription = req.body; + if (!subscription || !subscription.endpoint) { + return res.status(400).json({ error: 'Invalid subscription' }); + } + // Remove existing subscription with same endpoint + subscriptions = subscriptions.filter(sub => sub.endpoint !== subscription.endpoint); + subscriptions.push(subscription); + saveSubscriptions(); + console.log('Push subscription added:', subscription.endpoint); + res.status(201).json({ message: 'Subscription added' }); +}); + +// Unsubscribe from push notifications +app.post('/api/notifications/unsubscribe', async (req, res) => { + const { endpoint } = req.body; + subscriptions = subscriptions.filter(sub => sub.endpoint !== endpoint); + saveSubscriptions(); + console.log('Push subscription removed:', endpoint); + res.json({ message: 'Unsubscribed' }); +}); + +// --- System Info APIs --- +// Global state for network speed calculation +let previousNetStats = {}; +let previousTime = Date.now(); + +// Function to read and parse /host/proc/net/dev +const getHostNetworkStats = () => { + try { + // Use /sys/class/net mounted from host to bypass container network namespace isolation. + // This is the correct way to see host interfaces (eth0, eth1, etc.) from a bridge container. + const netDir = '/host/sys/class/net'; + + let interfaces = []; + try { + if (fs.existsSync(netDir)) { + interfaces = fs.readdirSync(netDir); + } else { + console.warn("Host sysfs not found at /host/sys/class/net"); + return []; + } + } catch (e) { return []; } + + const timestamp = Date.now(); + const currentStats = {}; + const timeDiff = (timestamp - previousTime) / 1000; + let result = []; + + for (const iface of interfaces) { + // Filter unwanted interfaces + if (iface === 'lo' || + iface.startsWith('veth') || + iface.startsWith('docker') || + iface.startsWith('br-') || + iface.startsWith('cali') || + iface.startsWith('flannel') || + iface.startsWith('cni') || + iface.startsWith('sit') || + iface.startsWith('tun') || + iface.startsWith('tap')) continue; + + try { + const rxPath = path.join(netDir, iface, 'statistics', 'rx_bytes'); + const txPath = path.join(netDir, iface, 'statistics', 'tx_bytes'); + + if (!fs.existsSync(rxPath) || !fs.existsSync(txPath)) continue; + + const rxBytes = parseInt(fs.readFileSync(rxPath, 'utf8').trim()); + const txBytes = parseInt(fs.readFileSync(txPath, 'utf8').trim()); + + currentStats[iface] = { rx: rxBytes, tx: txBytes }; + + let rx_sec = 0; + let tx_sec = 0; + + if (previousNetStats[iface] && timeDiff > 0) { + const prevRx = previousNetStats[iface].rx; + const prevTx = previousNetStats[iface].tx; + + // Simple difference (handle counter wrap if needed, but unlikely for 64bit counters in JS for short periods) + if (rxBytes >= prevRx) rx_sec = (rxBytes - prevRx) / timeDiff; + if (txBytes >= prevTx) tx_sec = (txBytes - prevTx) / timeDiff; + } + + result.push({ + iface: iface, + rx_sec: rx_sec, + tx_sec: tx_sec + }); + + } catch (err) { + continue; + } + } + + previousNetStats = currentStats; + previousTime = timestamp; + + return result; + + } catch (error) { + console.error('Failed to read host network stats from /sys:', error.message); + return []; + } +}; + +app.get('/api/stats', async (req, res) => { + try { + const [cpu, mem, fsData, osInfo, system] = await Promise.all([ + si.currentLoad(), + si.mem(), + si.fsSize(), + si.osInfo(), + si.system() + ]); + + // Get Host Network Stats + const network = getHostNetworkStats(); + + // Get Host Info + const hostInfo = getSynologyInfo(); + + // Disk Filtering Logic + const filteredStorage = fsData.filter(disk => { + let mountPoint = disk.mount; + if (mountPoint.startsWith('/host')) mountPoint = mountPoint.substring(5); + if (mountPoint.includes('@') || mountPoint.includes('docker') || mountPoint.includes('container') || + mountPoint.includes('appdata') || mountPoint.includes('tmp') || + disk.fs.startsWith('overlay') || disk.fs.startsWith('tmpfs') || disk.fs.startsWith('shm')) { + return false; + } + return /^\/volume\d+$/.test(mountPoint); + }); + + const stats = { + cpu: { + load: Math.round(cpu.currentLoad), + cores: cpu.cpus.map(c => Math.round(c.load)), + }, + memory: { + total: mem.total, + used: mem.used, + percentage: Math.round((mem.active / mem.total) * 100), + }, + storage: filteredStorage.map(disk => ({ + fs: disk.fs, + type: disk.type, + size: disk.size, + used: disk.used, + use: Math.round(disk.use), + mount: disk.mount, + })), + network: network, // Use custom stats + system: { + model: hostInfo.model !== 'Docker Container' ? hostInfo.model : system.model, // Prefer host info + manufacturer: system.manufacturer, + os: 'DSM', // Hardcode or derive + release: hostInfo.dsmVersion, + kernel: osInfo.kernel, + hostname: osInfo.hostname, + uptime: osInfo.uptime + } + }; + + res.json(stats); + + } catch (error) { + console.error('Error fetching system stats:', error); + res.status(500).json({ error: 'Failed to retrieve system statistics' }); + } +}); + +// --- History Data --- +const HISTORY_FILE = path.join(process.cwd(), 'data', 'history.json'); +let historyRetentionHours = 24; // Default 24 hours +let HISTORY_LIMIT = historyRetentionHours * 60; // points (1 min interval) +let historyData = []; + +const saveHistory = () => { + try { + fs.writeFileSync(HISTORY_FILE, JSON.stringify(historyData)); + } catch (e) { console.error('Failed to save history:', e); } +}; + +const loadHistory = () => { + try { + if (fs.existsSync(HISTORY_FILE)) { + const data = fs.readFileSync(HISTORY_FILE, 'utf8'); + historyData = JSON.parse(data); + // Ensure data respects current limit (if limit changed while offline? Unlikely but good practice) + if (historyData.length > HISTORY_LIMIT) { + historyData = historyData.slice(historyData.length - HISTORY_LIMIT); + } + } + } catch (e) { console.error('Failed to load history:', e); } +}; +loadHistory(); + +// Retention Settings API +app.get('/api/settings/retention', (req, res) => { + res.json({ hours: historyRetentionHours }); +}); + +app.post('/api/settings/retention', (req, res) => { + const { hours } = req.body; + if (!hours || hours < 1) { + return res.status(400).json({ error: 'Invalid retention period' }); + } + historyRetentionHours = parseInt(hours); + HISTORY_LIMIT = historyRetentionHours * 60; + + // Trim existing data if needed + if (historyData.length > HISTORY_LIMIT) { + historyData = historyData.slice(historyData.length - HISTORY_LIMIT); + } + saveHistory(); // Save truncated data + + // Save preference to config + dockerConfig.retentionHours = historyRetentionHours; + saveDockerConfig(); + + console.log(`History retention updated to ${historyRetentionHours} hours (${HISTORY_LIMIT} points)`); + res.json({ message: 'Retention updated', hours: historyRetentionHours }); +}); + +const updateHistory = async () => { + try { + const [cpu, mem] = await Promise.all([ + si.currentLoad(), + si.mem() + ]); + + // Capture Network Stats for History + // Note: This relies on the shared getHostNetworkStats state. + // It captures the "current rate" since the last poll (whether by UI or this loop). + const netStats = getHostNetworkStats(); + let totalRx = 0; + let totalTx = 0; + netStats.forEach(net => { + // Include all interfaces or filter? Let's include all physical-ish ones found by our filter + totalRx += net.rx_sec; + totalTx += net.tx_sec; + }); + + const point = { + timestamp: Date.now(), + cpu: Math.round(cpu.currentLoad), + memory: Math.round((mem.active / mem.total) * 100), + network: { + rx: totalRx, + tx: totalTx + } + }; + + historyData.push(point); + if (historyData.length > HISTORY_LIMIT) { + historyData.shift(); // Remove oldest + } + + saveHistory(); + checkAlerts(point); + } catch (e) { + console.error("Failed to update history:", e); + } +}; + +// Update history every 1 minute +setInterval(updateHistory, 60 * 1000); +// Initial run +updateHistory(); + +app.get('/api/history', (req, res) => { + res.json(historyData); +}); + + +// Config saved via helper +// NOTE: loadDockerConfig normally called here. +// But check scope: HISTORY_LIMIT is defined above at 220. +// Our new loadDockerConfig at top CANNOT see HISTORY_LIMIT. +// So we must fix loadDockerConfig logic. +// I'll handle that separately. For now, deleting this block. + +// Helper: Secure Hash (Scrypt + Salt) + + +loadDockerConfig(); // Initial Load + +// Docker Config API +app.get('/api/docker/config', (req, res) => { + res.json({ + enabled: dockerConfig.enabled, + alertThresholds: dockerConfig.alertThresholds || { cpu: 90, memory: 90, disk: 90 }, + containerAlertEnabled: dockerConfig.containerAlertEnabled !== undefined ? dockerConfig.containerAlertEnabled : true, + alertCooldownSeconds: dockerConfig.alertCooldownSeconds || 300 + }); +}); + +app.post('/api/docker/config', (req, res) => { + const { enabled, password } = req.body; + const isEnabling = !!enabled; + + // Case 1: Disabling (Require Verification) + if (!isEnabling && dockerConfig.enabled) { + if (!password) return res.status(400).json({ error: 'Password is required to disable Docker features.' }); + + if (dockerConfig.passwordHash) { + let match = false; + + if (!dockerConfig.salt) { + // Legacy SHA256 Check + const legacyHash = crypto.createHash('sha256').update(password).digest('hex'); + match = (legacyHash === dockerConfig.passwordHash); + } else { + // Modern Scrypt Check + try { + const { hash } = hashPassword(password, dockerConfig.salt); + match = crypto.timingSafeEqual( + Buffer.from(hash, 'hex'), + Buffer.from(dockerConfig.passwordHash, 'hex') + ); + } catch (e) { + return res.status(500).json({ error: 'Verification error.' }); + } + } + + if (!match) return res.status(401).json({ error: '패스워드가 올바르지 않습니다.' }); + } + } + + // Case 2: Enabling (Require Password if no existing hash) + if (isEnabling && !password && !dockerConfig.passwordHash) { + return res.status(400).json({ error: 'Password is required to enable Docker features.' }); + } + + dockerConfig.enabled = isEnabling; + + // If Enabling and password provided, update/set password + if (isEnabling && password) { + const result = hashPassword(password); + if (result) { + dockerConfig.passwordHash = result.hash; + dockerConfig.salt = result.salt; + } + } + + saveDockerConfig(); + console.log(`Docker feature ${dockerConfig.enabled ? 'Enabled' : 'Disabled'}`); + res.json({ message: 'Configuration saved', enabled: dockerConfig.enabled }); +}); + +// Verify Password API +app.post('/api/docker/verify', (req, res) => { + const { password } = req.body; + if (!password) return res.status(400).json({ success: false }); + + // Validate if config exists + if (!dockerConfig.passwordHash || !dockerConfig.salt) { + return res.status(500).json({ success: false, error: 'Security configuration not found. Please reset in Settings.' }); + } + + const { hash } = hashPassword(password, dockerConfig.salt); + + // Constant-time comparison to prevent timing attacks + try { + const match = crypto.timingSafeEqual( + Buffer.from(hash, 'hex'), + Buffer.from(dockerConfig.passwordHash, 'hex') + ); + + if (match) { + res.json({ success: true }); + } else { + res.status(401).json({ success: false, error: '패스워드가 올바르지 않습니다.' }); + } + } catch (e) { + res.status(401).json({ success: false, error: 'Verification failed' }); + } +}); + +// --- Docker APIs --- +// Helper: Calculate CPU Percent from Docker Stats +const calculateCPUPercent = (stats) => { + if (!stats || !stats.cpu_stats || !stats.precpu_stats) return 0; + + const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage; + const systemCpuDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage; + const numberCpus = stats.cpu_stats.online_cpus || (stats.cpu_stats.cpu_usage.percpu_usage ? stats.cpu_stats.cpu_usage.percpu_usage.length : 1); + + if (systemCpuDelta > 0 && cpuDelta > 0) { + return (cpuDelta / systemCpuDelta) * numberCpus * 100.0; + } + return 0; +}; + +// --- Docker APIs --- +app.get('/api/docker/containers', async (req, res) => { + // Check if enabled + if (!dockerConfig.enabled) { + return res.status(403).json({ error: 'Docker features are disabled.' }); + } + + try { + const containers = await docker.listContainers({ all: true }); + + // Fetch stats for running containers + const containersWithStats = await Promise.all(containers.map(async (containerInfo) => { + if (containerInfo.State !== 'running') { + return { ...containerInfo, stats: null }; + } + + try { + const container = docker.getContainer(containerInfo.Id); + const stats = await container.stats({ stream: false }); + + const cpuPercent = calculateCPUPercent(stats); + const memoryUsage = stats.memory_stats.usage || 0; + const memoryLimit = stats.memory_stats.limit || 0; + const memoryPercent = memoryLimit > 0 ? (memoryUsage / memoryLimit) * 100 : 0; + + return { + ...containerInfo, + stats: { + cpu: cpuPercent, + memory: memoryUsage, + memoryLimit: memoryLimit, + memoryPercent: memoryPercent + } + }; + } catch (e) { + console.error(`Failed to fetch stats for ${containerInfo.Names[0]}:`, e.message); + return { ...containerInfo, stats: null }; + } + })); + + res.json(containersWithStats); + } catch (error) { + console.error('Error fetching containers:', error); + res.status(500).json({ error: 'Failed to fetch containers. Is Docker Sock mounted?' }); + } +}); + +// Container Control APIs +app.post('/api/docker/containers/:id/:action', async (req, res) => { + const { id, action } = req.params; + try { + const container = docker.getContainer(id); + let data; + switch (action) { + case 'start': data = await container.start(); break; + case 'stop': data = await container.stop(); break; + case 'restart': data = await container.restart(); break; + default: return res.status(400).json({ error: 'Invalid action' }); + } + res.json({ message: `Container ${action} successful`, data }); + } catch (error) { + console.error(`Error performing ${action} on container ${id}:`, error); + res.status(500).json({ error: `Failed to ${action} container: ${error.message}` }); + } +}); + +// Save Alert Thresholds +app.post('/api/settings/thresholds', (req, res) => { + const { thresholds, containerAlertEnabled, alertCooldownSeconds } = req.body; + if (thresholds) { + dockerConfig.alertThresholds = thresholds; + if (containerAlertEnabled !== undefined) { + dockerConfig.containerAlertEnabled = containerAlertEnabled; + } + if (alertCooldownSeconds !== undefined) { + dockerConfig.alertCooldownSeconds = Math.max(10, Math.min(3600, alertCooldownSeconds)); + } + saveDockerConfig(); + res.json({ success: true }); + } else { + res.status(400).json({ error: 'Invalid thresholds' }); + } +}); + +// Container Logs API +app.get('/api/docker/containers/:id/logs', async (req, res) => { + const { id } = req.params; + try { + const container = docker.getContainer(id); + const logs = await container.logs({ stdout: true, stderr: true, tail: 100, timestamps: true }); + res.send(logs.toString('utf8')); + } catch (error) { + console.error('Error fetching logs:', error); + res.status(500).json({ error: 'Failed to fetch logs' }); + } +}); + + +const monitorDockerEvents = () => { + docker.getEvents((err, stream) => { + if (err) return console.error('Error getting docker events:', err); + + stream.on('data', chunk => { + if (!dockerConfig.enabled) return; // Ignore if disabled + if (!dockerConfig.containerAlertEnabled) return; // Ignore if container alerts disabled + + try { + const event = JSON.parse(chunk.toString()); + // Filter for container stop/die + if (event.Type === 'container' && (event.Action === 'die' || event.Action === 'stop')) { + // Check if it's "unexpected"? + // Usually we alert on any stop if the monitoring is strictly keeping uptime. + // But if user stops it manually via UI, we might get an event too. + // However, UI stop calls API -> API stops container. + // It is hard to distinguish "crash" from "manual stop" without state tracking. + // For now, user asked "컨테이너가 까지게 되면", effectively "stops". + // We will alert. + const containerName = event.Actor?.Attributes?.name || event.id.substring(0, 12); + sendNotification({ + title: 'snStatus 알림', + body: `컨테이너 종료: ${containerName}` + }); + } + } catch (e) { + console.error('Error parsing docker event:', e); + } + }); + }); +}; + +initPushNotifications(); +if (dockerConfig.enabled) monitorDockerEvents(); // Start if enabled, or always start? +// Better always start listening, but filter inside. +// Actually monitorDockerEvents is a stream. If I call it multiple times it duplicates. +// Just call it once. +monitorDockerEvents(); + + +// --- WebSocket for Terminal & Logs --- +io.on('connection', (socket) => { + let stream = null; + socket.on('attach-terminal', async (containerId) => { + try { + const container = docker.getContainer(containerId); + const exec = await container.exec({ + AttachStdin: true, AttachStdout: true, AttachStderr: true, Tty: true, Cmd: ['/bin/sh'] + }); + stream = await exec.start({ hijack: true, stdin: true }); + stream.on('data', (chunk) => socket.emit('terminal-output', chunk.toString('utf8'))); + socket.on('terminal-input', (data) => { if (stream) stream.write(data); }); + socket.on('resize-terminal', ({ cols, rows }) => { if (exec) exec.resize({ w: cols, h: rows }).catch(() => { }); }); + } catch (err) { + console.error('Error attaching to container:', err); + socket.emit('terminal-output', '\r\nError: Failed to attach to container. ' + err.message + '\r\n'); + } + }); + + socket.on('disconnect', () => { if (stream) stream.end(); }); +}); + +httpServer.listen(PORT, '0.0.0.0', () => { + console.log(`Server running on port ${PORT}`); +}); diff --git a/frontend/build.sh b/frontend/build.sh new file mode 100644 index 0000000..c33e3fd --- /dev/null +++ b/frontend/build.sh @@ -0,0 +1,2 @@ +docker build --no-cache -t sa8001/snstatus-frontend:$1 ./frontend +docker build --no-cache -t sa8001/snstatus-backend:$1 ./backend diff --git a/frontend/data/config.json b/frontend/data/config.json new file mode 100644 index 0000000..f8ab95b --- /dev/null +++ b/frontend/data/config.json @@ -0,0 +1,13 @@ +{ + "enabled": true, + "passwordHash": "8930e0e9ba435b538c8789a5eb7b3262ce73b08fad336b9dbc12599599d8885973d640923bc34c789f598124f7ad238de560bc06e88b2ce02ff0819f27a5b590", + "salt": "688750cfc766f9b5672be297ee45bd0e", + "retentionHours": 24, + "alertThresholds": { + "cpu": 80, + "memory": 80, + "disk": 90 + }, + "containerAlertEnabled": true, + "alertCooldownSeconds": 60 +} \ No newline at end of file diff --git a/frontend/data/history.json b/frontend/data/history.json new file mode 100644 index 0000000..81702f4 --- /dev/null +++ b/frontend/data/history.json @@ -0,0 +1 @@ +[{"timestamp":1769480883428,"cpu":8,"memory":28,"network":{"rx":0,"tx":0}},{"timestamp":1769480943381,"cpu":6,"memory":28,"network":{"rx":346.55433547898525,"tx":451.7228322605074}},{"timestamp":1769481003381,"cpu":5,"memory":27,"network":{"rx":253.01003344481603,"tx":454.01337792642136}},{"timestamp":1769481063383,"cpu":5,"memory":27,"network":{"rx":666.1944601846604,"tx":1530.9823005899802}},{"timestamp":1769481123385,"cpu":5,"memory":27,"network":{"rx":543.8076032066133,"tx":1063.648939184347}},{"timestamp":1769481183387,"cpu":5,"memory":27,"network":{"rx":494.64193456993814,"tx":979.8843391163775}},{"timestamp":1769481243390,"cpu":5,"memory":27,"network":{"rx":619.8626712442918,"tx":796.3067897736742}},{"timestamp":1769481303390,"cpu":5,"memory":27,"network":{"rx":398.04336594390094,"tx":563.0406159897335}},{"timestamp":1769481766830,"cpu":8,"memory":28,"network":{"rx":0,"tx":0}},{"timestamp":1769481826797,"cpu":14,"memory":28,"network":{"rx":1834.710743801653,"tx":27726.58402203857}},{"timestamp":1769481886800,"cpu":5,"memory":28,"network":{"rx":637.1941272430669,"tx":579.4453507340946}},{"timestamp":1769481946804,"cpu":6,"memory":27,"network":{"rx":281.61211915663336,"tx":1837.7706178282745}},{"timestamp":1769482006807,"cpu":5,"memory":27,"network":{"rx":435.1562997389699,"tx":365.69682307251543}},{"timestamp":1769482447287,"cpu":8,"memory":28,"network":{"rx":0,"tx":0}},{"timestamp":1769482507227,"cpu":6,"memory":28,"network":{"rx":189.54623779437105,"tx":794.0838598506605}},{"timestamp":1769482567227,"cpu":9,"memory":28,"network":{"rx":263.1879098944967,"tx":606.7864271457086}},{"timestamp":1769482627228,"cpu":8,"memory":28,"network":{"rx":730.7938540332906,"tx":9175.73623559539}},{"timestamp":1769482687228,"cpu":5,"memory":27,"network":{"rx":410.95,"tx":323}},{"timestamp":1769482747231,"cpu":5,"memory":27,"network":{"rx":546.0651311622946,"tx":1327.2390920302655}},{"timestamp":1769482807230,"cpu":5,"memory":27,"network":{"rx":466.4,"tx":701.95}},{"timestamp":1769482867232,"cpu":5,"memory":27,"network":{"rx":571.1309623012565,"tx":634.5788473717543}},{"timestamp":1769482927231,"cpu":5,"memory":27,"network":{"rx":545.1257520958683,"tx":757.9959665994432}},{"timestamp":1769483371187,"cpu":8,"memory":28,"network":{"rx":0,"tx":0}},{"timestamp":1769483431124,"cpu":7,"memory":28,"network":{"rx":439.56887346502936,"tx":1038.574479444741}},{"timestamp":1769483491126,"cpu":5,"memory":28,"network":{"rx":581.9250551065393,"tx":2686.872397746755}},{"timestamp":1769483551123,"cpu":5,"memory":27,"network":{"rx":1419.6043135490108,"tx":34304.34855076087}},{"timestamp":1769483611124,"cpu":6,"memory":27,"network":{"rx":458.65902234962755,"tx":528.1411976467059}},{"timestamp":1769483671124,"cpu":5,"memory":27,"network":{"rx":471.1,"tx":445.05}},{"timestamp":1769484070162,"cpu":8,"memory":28,"network":{"rx":0,"tx":0}},{"timestamp":1769484130130,"cpu":4,"memory":28,"network":{"rx":647.422680412371,"tx":1310.6529209621992}},{"timestamp":1769484190131,"cpu":5,"memory":27,"network":{"rx":1236.2433435533324,"tx":497.01468452477}},{"timestamp":1769484250132,"cpu":6,"memory":27,"network":{"rx":1242.2585845857348,"tx":5556.2515056612865}},{"timestamp":1769484310134,"cpu":6,"memory":27,"network":{"rx":252.57769387162358,"tx":859.6427534127215}},{"timestamp":1769484370135,"cpu":5,"memory":27,"network":{"rx":470.51666666666665,"tx":1004.1666666666666}},{"timestamp":1769484430135,"cpu":5,"memory":27,"network":{"rx":474.18333333333334,"tx":691.2}},{"timestamp":1769484490135,"cpu":5,"memory":27,"network":{"rx":458.79235346077564,"tx":788.503524941251}},{"timestamp":1769484550136,"cpu":5,"memory":27,"network":{"rx":526.024566257229,"tx":530.2578290361828}},{"timestamp":1769484610137,"cpu":5,"memory":27,"network":{"rx":424.96666666666664,"tx":555.2166666666667}},{"timestamp":1769484670137,"cpu":8,"memory":27,"network":{"rx":752.7227183620126,"tx":4583.805271182749}},{"timestamp":1769484730139,"cpu":5,"memory":27,"network":{"rx":629.512349588347,"tx":1906.55311489617}},{"timestamp":1769484790141,"cpu":5,"memory":27,"network":{"rx":428.6857104763174,"tx":407.0364321189293}},{"timestamp":1769484850143,"cpu":5,"memory":27,"network":{"rx":446.4184527182427,"tx":486.4671177627412}},{"timestamp":1769484910145,"cpu":6,"memory":27,"network":{"rx":512.6914551424143,"tx":714.5047582540291}},{"timestamp":1769484970146,"cpu":5,"memory":27,"network":{"rx":467.74220429659505,"tx":301.411643139281}},{"timestamp":1769485030146,"cpu":5,"memory":27,"network":{"rx":402.74328761187314,"tx":439.94266762220633}},{"timestamp":1769485090147,"cpu":5,"memory":27,"network":{"rx":495.56666666666666,"tx":416.0833333333333}},{"timestamp":1769485150147,"cpu":5,"memory":27,"network":{"rx":474.2420959650673,"tx":480.1253312444793}},{"timestamp":1769485210148,"cpu":5,"memory":27,"network":{"rx":389.37684371927133,"tx":309.9781670305495}},{"timestamp":1769485270150,"cpu":8,"memory":27,"network":{"rx":519.2413459775671,"tx":512.1247979200347}},{"timestamp":1769485330160,"cpu":5,"memory":27,"network":{"rx":499.6167305449092,"tx":585.3357773704383}},{"timestamp":1769485390164,"cpu":5,"memory":27,"network":{"rx":416.7055529631358,"tx":289.06406239584027}},{"timestamp":1769485450166,"cpu":5,"memory":27,"network":{"rx":663.533489992167,"tx":561.2386047364298}},{"timestamp":1769485510166,"cpu":5,"memory":27,"network":{"rx":506.8,"tx":460.6333333333333}},{"timestamp":1769485570167,"cpu":5,"memory":27,"network":{"rx":446.3592273462109,"tx":454.79242012633125}},{"timestamp":1769485630168,"cpu":5,"memory":27,"network":{"rx":447.73333333333335,"tx":293.6166666666667}},{"timestamp":1769485690168,"cpu":5,"memory":27,"network":{"rx":575.0904151597474,"tx":449.5925067915535}},{"timestamp":1769485750170,"cpu":5,"memory":27,"network":{"rx":518.599380020666,"tx":349.70500983300553}},{"timestamp":1769485810171,"cpu":5,"memory":27,"network":{"rx":443.8592690121831,"tx":262.74562090631827}},{"timestamp":1769485870175,"cpu":5,"memory":27,"network":{"rx":497.85844041131276,"tx":529.5235238238088}},{"timestamp":1769485930174,"cpu":5,"memory":27,"network":{"rx":606.5267754462574,"tx":669.2444874081234}},{"timestamp":1769485990175,"cpu":5,"memory":27,"network":{"rx":472.1254645755904,"tx":329.32784453592444}},{"timestamp":1769486050175,"cpu":5,"memory":27,"network":{"rx":455.50907484875256,"tx":492.525124581257}},{"timestamp":1769486110179,"cpu":5,"memory":27,"network":{"rx":534.681021265249,"tx":602.0265315645623}},{"timestamp":1769486170180,"cpu":6,"memory":27,"network":{"rx":388.2768620522991,"tx":287.1118814686422}},{"timestamp":1769486230180,"cpu":5,"memory":27,"network":{"rx":368.3666666666667,"tx":426.23333333333335}},{"timestamp":1769486290180,"cpu":5,"memory":27,"network":{"rx":414.46666666666664,"tx":381.1}},{"timestamp":1769486350182,"cpu":5,"memory":27,"network":{"rx":563.9645345155161,"tx":620.4126529115696}},{"timestamp":1769486410182,"cpu":5,"memory":27,"network":{"rx":467.06666666666666,"tx":347.3}},{"timestamp":1769486470183,"cpu":6,"memory":27,"network":{"rx":411.4,"tx":445.15}},{"timestamp":1769486530184,"cpu":5,"memory":27,"network":{"rx":574.9904168263862,"tx":707.9382010299829}},{"timestamp":1769486590185,"cpu":5,"memory":27,"network":{"rx":437.9020699310023,"tx":393.9702009933002}},{"timestamp":1769486650185,"cpu":5,"memory":27,"network":{"rx":421.9166666666667,"tx":466.15}},{"timestamp":1769486710186,"cpu":5,"memory":27,"network":{"rx":466.45889235179413,"tx":320.74465425576244}},{"timestamp":1769486770188,"cpu":5,"memory":27,"network":{"rx":465.458909018183,"tx":611.9231346144231}},{"timestamp":1769486830190,"cpu":5,"memory":27,"network":{"rx":418.1693943535215,"tx":356.0714642845238}},{"timestamp":1769486890192,"cpu":5,"memory":27,"network":{"rx":512.507707947936,"tx":278.46940986284017}},{"timestamp":1769486950194,"cpu":5,"memory":27,"network":{"rx":462.667911069631,"tx":384.9371687610413}},{"timestamp":1769487010195,"cpu":5,"memory":27,"network":{"rx":492.975117081382,"tx":348.9441842635956}},{"timestamp":1769487070196,"cpu":5,"memory":27,"network":{"rx":531.1411476475392,"tx":552.3907934867752}},{"timestamp":1769487130197,"cpu":5,"memory":27,"network":{"rx":567.8405359910669,"tx":570.2238296028399}},{"timestamp":1769487190201,"cpu":5,"memory":27,"network":{"rx":403.2064529031398,"tx":369.3587094193721}},{"timestamp":1769487250202,"cpu":5,"memory":27,"network":{"rx":466.99221679638674,"tx":422.5762903951601}},{"timestamp":1769487310203,"cpu":5,"memory":27,"network":{"rx":322.5279578673689,"tx":381.86030232829455}},{"timestamp":1769487370205,"cpu":5,"memory":27,"network":{"rx":504.38318722709244,"tx":372.05426485783806}},{"timestamp":1769487430205,"cpu":5,"memory":27,"network":{"rx":360.28333333333336,"tx":410.75}},{"timestamp":1769487490206,"cpu":5,"memory":27,"network":{"rx":446.4425592906785,"tx":436.44272595456744}},{"timestamp":1769487550207,"cpu":5,"memory":27,"network":{"rx":541.357644039266,"tx":2076.5153914101434}},{"timestamp":1769487610207,"cpu":5,"memory":27,"network":{"rx":457.3333333333333,"tx":385.46666666666664}},{"timestamp":1769488176509,"cpu":8,"memory":28,"network":{"rx":0,"tx":0}},{"timestamp":1769488236440,"cpu":6,"memory":28,"network":{"rx":504.7053327104051,"tx":727.2575585663752}},{"timestamp":1769488296440,"cpu":6,"memory":28,"network":{"rx":1201.3287459959663,"tx":11369.91339423419}},{"timestamp":1769488356445,"cpu":5,"memory":28,"network":{"rx":515.2489834011066,"tx":1755.7662822478503}},{"timestamp":1769488416446,"cpu":5,"memory":27,"network":{"rx":817.1727609079697,"tx":1265.2578247391752}},{"timestamp":1769488476450,"cpu":5,"memory":27,"network":{"rx":502.04986334244387,"tx":574.9450036664223}},{"timestamp":1769488536453,"cpu":6,"memory":27,"network":{"rx":1138.1001254418977,"tx":3830.8358991903297}},{"timestamp":1769489113900,"cpu":8,"memory":28,"network":{"rx":0,"tx":0}},{"timestamp":1769489173842,"cpu":6,"memory":28,"network":{"rx":659.1705315137966,"tx":2329.5352173767974}},{"timestamp":1769489233841,"cpu":5,"memory":28,"network":{"rx":484.9,"tx":1081.0166666666667}},{"timestamp":1769489293843,"cpu":6,"memory":27,"network":{"rx":852.2024632922785,"tx":2111.6148064198933}},{"timestamp":1769489353847,"cpu":5,"memory":27,"network":{"rx":516.8402633113907,"tx":1000.3166402799766}},{"timestamp":1769489413850,"cpu":5,"memory":27,"network":{"rx":518.4407446294352,"tx":1076.7128310251155}},{"timestamp":1769489473852,"cpu":5,"memory":27,"network":{"rx":616.5127829072364,"tx":896.5701143295223}},{"timestamp":1769489533853,"cpu":5,"memory":27,"network":{"rx":474.4420926317895,"tx":861.1356477392044}},{"timestamp":1769489593855,"cpu":5,"memory":27,"network":{"rx":383.9705343155228,"tx":549.098363387887}},{"timestamp":1769489653857,"cpu":5,"memory":27,"network":{"rx":578.4903584940251,"tx":869.7521707971534}},{"timestamp":1769489713857,"cpu":6,"memory":27,"network":{"rx":525.1304237180303,"tx":5382.364168469271}},{"timestamp":1769489773858,"cpu":6,"memory":27,"network":{"rx":537.7022960203716,"tx":1464.59161984132}},{"timestamp":1769489833861,"cpu":5,"memory":27,"network":{"rx":464.6601003283169,"tx":485.1090778794394}},{"timestamp":1769489893863,"cpu":5,"memory":27,"network":{"rx":378.8436859385677,"tx":346.3775603739938}},{"timestamp":1769489953864,"cpu":5,"memory":27,"network":{"rx":717.7927402419919,"tx":701.9599346688443}},{"timestamp":1769490013864,"cpu":5,"memory":27,"network":{"rx":407,"tx":362.78333333333336}},{"timestamp":1769490073866,"cpu":5,"memory":27,"network":{"rx":555.931468951035,"tx":625.6624779174027}},{"timestamp":1769490133868,"cpu":5,"memory":27,"network":{"rx":428.46905103163226,"tx":463.96786773774204}},{"timestamp":1769490193868,"cpu":5,"memory":27,"network":{"rx":267,"tx":316.78333333333336}},{"timestamp":1769490253868,"cpu":7,"memory":27,"network":{"rx":2248.616666666667,"tx":2128.4666666666667}},{"timestamp":1769490313868,"cpu":5,"memory":27,"network":{"rx":424.73333333333335,"tx":500.45}},{"timestamp":1769490373871,"cpu":5,"memory":27,"network":{"rx":441.29460193656985,"tx":587.3372998016766}},{"timestamp":1769490433873,"cpu":5,"memory":27,"network":{"rx":459.4680177327422,"tx":430.7356421452618}},{"timestamp":1769490493873,"cpu":5,"memory":27,"network":{"rx":428.85,"tx":461.6166666666667}},{"timestamp":1769490553874,"cpu":5,"memory":27,"network":{"rx":618.9666666666667,"tx":317.35}},{"timestamp":1769490613874,"cpu":5,"memory":27,"network":{"rx":431.25947900868323,"tx":335.97773370443826}},{"timestamp":1769491007026,"cpu":8,"memory":28,"network":{"rx":0,"tx":0}},{"timestamp":1769491066970,"cpu":5,"memory":28,"network":{"rx":568.330075110608,"tx":595.7403025002573}},{"timestamp":1769491126971,"cpu":5,"memory":28,"network":{"rx":580.0569990500159,"tx":1066.3155614073098}},{"timestamp":1769491186973,"cpu":5,"memory":27,"network":{"rx":463.86014032631704,"tx":821.0922787193973}},{"timestamp":1769491246974,"cpu":5,"memory":27,"network":{"rx":580.5,"tx":790.0166666666667}},{"timestamp":1769491872250,"cpu":8,"memory":28,"network":{"rx":0,"tx":0}},{"timestamp":1769491932218,"cpu":9,"memory":28,"network":{"rx":298.0245761393685,"tx":492.14496811323687}},{"timestamp":1769491992219,"cpu":7,"memory":28,"network":{"rx":840.2076318742985,"tx":615.6004489337822}},{"timestamp":1769492052219,"cpu":5,"memory":28,"network":{"rx":745.1496085294058,"tx":1652.6735317323837}},{"timestamp":1769492112222,"cpu":6,"memory":27,"network":{"rx":920.5340938553279,"tx":2211.822660098522}},{"timestamp":1769492172222,"cpu":5,"memory":27,"network":{"rx":508.6915218079699,"tx":811.4031432809453}},{"timestamp":1769492232223,"cpu":5,"memory":27,"network":{"rx":592.373460442326,"tx":1548.424192930118}},{"timestamp":1769492292227,"cpu":5,"memory":27,"network":{"rx":477.9848010132658,"tx":859.3593760415972}},{"timestamp":1769492352230,"cpu":5,"memory":27,"network":{"rx":676.2941235292157,"tx":889.2370254324856}},{"timestamp":1769492412231,"cpu":5,"memory":27,"network":{"rx":482.48391720275987,"tx":736.3587880403986}},{"timestamp":1769492472233,"cpu":8,"memory":27,"network":{"rx":556.8314389520349,"tx":942.1185960467984}},{"timestamp":1769492532234,"cpu":6,"memory":27,"network":{"rx":1163.3507853403141,"tx":531.4136125654451}},{"timestamp":1769492592236,"cpu":4,"memory":27,"network":{"rx":467.5089179548157,"tx":1179.1319857312724}},{"timestamp":1769492652237,"cpu":5,"memory":27,"network":{"rx":843.8359360677323,"tx":431.2428126197897}},{"timestamp":1769492712238,"cpu":5,"memory":27,"network":{"rx":430.9,"tx":422.93333333333334}},{"timestamp":1769492772239,"cpu":5,"memory":27,"network":{"rx":476.60077997400083,"tx":450.75164161194624}},{"timestamp":1769492832240,"cpu":5,"memory":27,"network":{"rx":480.90865152247466,"tx":588.4901918301362}},{"timestamp":1769492892240,"cpu":6,"memory":27,"network":{"rx":513.2333333333333,"tx":487.65}},{"timestamp":1769492952242,"cpu":5,"memory":27,"network":{"rx":560.6906551557474,"tx":367.09388176863723}},{"timestamp":1769493012244,"cpu":5,"memory":27,"network":{"rx":440.92795360231986,"tx":400.4966418345749}},{"timestamp":1769493072246,"cpu":6,"memory":27,"network":{"rx":565.7644745175161,"tx":496.5001166627779}},{"timestamp":1769493132249,"cpu":5,"memory":27,"network":{"rx":472.25092496916767,"tx":559.9313356221459}},{"timestamp":1769493192249,"cpu":5,"memory":27,"network":{"rx":485.45857569040516,"tx":325.57790703488274}},{"timestamp":1769493252250,"cpu":5,"memory":27,"network":{"rx":628.5228579523675,"tx":452.39246012566457}},{"timestamp":1769493312251,"cpu":4,"memory":27,"network":{"rx":661.4897466827504,"tx":875.1507840772015}},{"timestamp":1769493372253,"cpu":5,"memory":27,"network":{"rx":551.3532704034751,"tx":440.08437056219196}},{"timestamp":1769493432255,"cpu":5,"memory":27,"network":{"rx":579.7640078664044,"tx":452.70157661411287}},{"timestamp":1769493492256,"cpu":5,"memory":27,"network":{"rx":454.3757604039933,"tx":402.9766170563824}},{"timestamp":1769493552257,"cpu":5,"memory":27,"network":{"rx":514.85,"tx":399.5833333333333}},{"timestamp":1769493612258,"cpu":5,"memory":27,"network":{"rx":414.3028565714476,"tx":415.452818239392}},{"timestamp":1769493672259,"cpu":6,"memory":27,"network":{"rx":560.4166666666666,"tx":414.65}},{"timestamp":1769493732261,"cpu":5,"memory":27,"network":{"rx":465.5511482950568,"tx":446.1851271624279}},{"timestamp":1769493792258,"cpu":5,"memory":27,"network":{"rx":659.0389016018307,"tx":298.5342321726761}},{"timestamp":1769493852258,"cpu":5,"memory":27,"network":{"rx":522.7333333333333,"tx":486.3666666666667}},{"timestamp":1769493912261,"cpu":5,"memory":27,"network":{"rx":439.2447044314451,"tx":381.9309034548273}},{"timestamp":1769493972262,"cpu":6,"memory":27,"network":{"rx":527.65,"tx":422.81666666666666}},{"timestamp":1769494032262,"cpu":6,"memory":27,"network":{"rx":508.3581940300995,"tx":537.274378760354}},{"timestamp":1769494092264,"cpu":5,"memory":27,"network":{"rx":458.3680543981867,"tx":471.71760941301955}},{"timestamp":1769494152265,"cpu":5,"memory":27,"network":{"rx":597.2567123881269,"tx":412.59312344794256}},{"timestamp":1769494212266,"cpu":5,"memory":27,"network":{"rx":475.9166666666667,"tx":401.3}},{"timestamp":1769494272265,"cpu":5,"memory":27,"network":{"rx":659.35,"tx":428.56666666666666}},{"timestamp":1769494332266,"cpu":5,"memory":27,"network":{"rx":524.75,"tx":604.1666666666666}},{"timestamp":1769494392268,"cpu":5,"memory":27,"network":{"rx":485.4590603803143,"tx":355.41556255520555}},{"timestamp":1769494452269,"cpu":5,"memory":27,"network":{"rx":573.7166666666667,"tx":386.35}},{"timestamp":1769494512269,"cpu":5,"memory":27,"network":{"rx":276.562057299045,"tx":249.41250979150348}},{"timestamp":1769494572269,"cpu":5,"memory":27,"network":{"rx":437.46666666666664,"tx":317.6666666666667}},{"timestamp":1769494632270,"cpu":5,"memory":27,"network":{"rx":414.2930951150814,"tx":353.3774437092715}},{"timestamp":1769494692271,"cpu":5,"memory":27,"network":{"rx":504.74158764020603,"tx":586.5068915518075}},{"timestamp":1769494752274,"cpu":5,"memory":27,"network":{"rx":537.5897871773078,"tx":357.96543506158025}},{"timestamp":1769494812275,"cpu":5,"memory":27,"network":{"rx":497.7583706938218,"tx":384.4935917734705}},{"timestamp":1769494872276,"cpu":6,"memory":27,"network":{"rx":550.2908284861919,"tx":430.642822619623}},{"timestamp":1769494932277,"cpu":5,"memory":27,"network":{"rx":523.4746087565208,"tx":574.790420159664}},{"timestamp":1769494992281,"cpu":5,"memory":27,"network":{"rx":613.1757882807813,"tx":422.78848076794884}},{"timestamp":1769495052284,"cpu":6,"memory":27,"network":{"rx":432.6950319150709,"tx":352.23238838058097}},{"timestamp":1769495112286,"cpu":5,"memory":27,"network":{"rx":484.03386553781536,"tx":397.753408219726}},{"timestamp":1769495172287,"cpu":5,"memory":27,"network":{"rx":464.59225679572006,"tx":391.0434826086232}},{"timestamp":1769495232292,"cpu":5,"memory":27,"network":{"rx":461.74485459545036,"tx":422.5481209899175}},{"timestamp":1769495292294,"cpu":5,"memory":27,"network":{"rx":487.7004099863338,"tx":535.0154994833505}},{"timestamp":1769495352296,"cpu":5,"memory":27,"network":{"rx":523.0246162563958,"tx":343.6942717621373}},{"timestamp":1769495412296,"cpu":5,"memory":27,"network":{"rx":472.658789020183,"tx":2069.4821752970784}},{"timestamp":1769495472298,"cpu":6,"memory":27,"network":{"rx":875.154161527949,"tx":539.2820239325356}},{"timestamp":1769495532298,"cpu":5,"memory":27,"network":{"rx":493.71666666666664,"tx":569.4166666666666}},{"timestamp":1769495592298,"cpu":5,"memory":27,"network":{"rx":456.65,"tx":381.96666666666664}},{"timestamp":1769495652300,"cpu":5,"memory":27,"network":{"rx":396.70344321855936,"tx":355.1714942835239}},{"timestamp":1769495712301,"cpu":5,"memory":27,"network":{"rx":701.5549740837653,"tx":482.77528707854873}},{"timestamp":1769495772302,"cpu":5,"memory":27,"network":{"rx":521.1746470892152,"tx":236.57939034349428}},{"timestamp":1769495832303,"cpu":5,"memory":27,"network":{"rx":358.1940300994984,"tx":233.12944784253597}},{"timestamp":1769495892305,"cpu":5,"memory":27,"network":{"rx":638.5120495983467,"tx":664.194526849105}},{"timestamp":1769495952306,"cpu":5,"memory":27,"network":{"rx":681.4053099115015,"tx":1236.0293995100083}},{"timestamp":1769496012306,"cpu":5,"memory":27,"network":{"rx":618.1166666666667,"tx":3888.5333333333333}},{"timestamp":1769496072307,"cpu":8,"memory":27,"network":{"rx":1001.65,"tx":20228.683333333334}},{"timestamp":1769496132309,"cpu":5,"memory":27,"network":{"rx":457.70140995300153,"tx":393.170227659078}},{"timestamp":1769496192311,"cpu":5,"memory":27,"network":{"rx":387.81394263620155,"tx":370.7147975934537}},{"timestamp":1769496252314,"cpu":5,"memory":27,"network":{"rx":566.1383597486792,"tx":461.8602403213173}},{"timestamp":1769496312315,"cpu":5,"memory":27,"network":{"rx":475.7254045765904,"tx":365.04391593473446}},{"timestamp":1769496372316,"cpu":6,"memory":27,"network":{"rx":480.8833333333333,"tx":441.4}},{"timestamp":1769496432319,"cpu":5,"memory":27,"network":{"rx":379.19138724085064,"tx":482.6678221451903}},{"timestamp":1769496492322,"cpu":6,"memory":27,"network":{"rx":715.1261624612513,"tx":619.2626912436252}},{"timestamp":1769496552322,"cpu":5,"memory":27,"network":{"rx":651.6058065698905,"tx":316.96138397693375}},{"timestamp":1769496612323,"cpu":5,"memory":27,"network":{"rx":480.27532874452095,"tx":411.7264712254796}},{"timestamp":1769496672326,"cpu":6,"memory":27,"network":{"rx":495.07524623768813,"tx":402.0798960051997}},{"timestamp":1769496732328,"cpu":5,"memory":27,"network":{"rx":437.70937151047485,"tx":469.2421792970117}},{"timestamp":1769496792333,"cpu":5,"memory":27,"network":{"rx":458.5784517956837,"tx":440.89659195067077}},{"timestamp":1769496852336,"cpu":5,"memory":27,"network":{"rx":426.2120227321967,"tx":398.73006349682515}},{"timestamp":1769496912336,"cpu":5,"memory":27,"network":{"rx":483.041949300845,"tx":302.2616289728505}},{"timestamp":1769496972340,"cpu":5,"memory":27,"network":{"rx":393.50709952669825,"tx":312.09586027598164}},{"timestamp":1769497032340,"cpu":5,"memory":27,"network":{"rx":620.25,"tx":567.2833333333333}},{"timestamp":1769497092342,"cpu":5,"memory":27,"network":{"rx":511.8581356977384,"tx":421.5263078948684}},{"timestamp":1769497152343,"cpu":6,"memory":27,"network":{"rx":561.6979434018866,"tx":332.52224925835804}},{"timestamp":1769497212344,"cpu":5,"memory":27,"network":{"rx":457.1257145714238,"tx":349.4275095415077}},{"timestamp":1769497272344,"cpu":5,"memory":27,"network":{"rx":562.25,"tx":528.7833333333333}},{"timestamp":1769497332346,"cpu":5,"memory":27,"network":{"rx":466.08446385120493,"tx":361.771274290857}},{"timestamp":1769497392347,"cpu":5,"memory":27,"network":{"rx":416.1597306711555,"tx":340.9443175947068}},{"timestamp":1769497452349,"cpu":5,"memory":27,"network":{"rx":410.3196560114663,"tx":415.5528149061698}},{"timestamp":1769497512352,"cpu":5,"memory":27,"network":{"rx":410.80297323422553,"tx":255.90813639545348}},{"timestamp":1769497572352,"cpu":6,"memory":27,"network":{"rx":448.0091998466692,"tx":377.8270362160631}},{"timestamp":1769497632357,"cpu":5,"memory":27,"network":{"rx":367.8026831097408,"tx":330.87242729772515}},{"timestamp":1769497692360,"cpu":6,"memory":27,"network":{"rx":684.2991183774145,"tx":2235.454893921971}},{"timestamp":1769497752361,"cpu":5,"memory":27,"network":{"rx":652.0166666666667,"tx":599.1}},{"timestamp":1769497812363,"cpu":5,"memory":27,"network":{"rx":526.1570254820592,"tx":327.41696248520907}},{"timestamp":1769497872364,"cpu":6,"memory":27,"network":{"rx":535.6244062598956,"tx":482.5252912451459}},{"timestamp":1769497932365,"cpu":5,"memory":27,"network":{"rx":396.6333333333333,"tx":367.48333333333335}},{"timestamp":1769497992366,"cpu":5,"memory":27,"network":{"rx":658.7613746208459,"tx":660.5946468451051}},{"timestamp":1769498052366,"cpu":5,"memory":27,"network":{"rx":361.1333333333333,"tx":297.71666666666664}},{"timestamp":1769498112368,"cpu":5,"memory":27,"network":{"rx":401.2433126114565,"tx":388.2435292745121}},{"timestamp":1769498172372,"cpu":6,"memory":27,"network":{"rx":489.1092408965919,"tx":492.3089742521456}},{"timestamp":1769498232374,"cpu":6,"memory":27,"network":{"rx":12923.802539915336,"tx":12186.060464651178}},{"timestamp":1769498292378,"cpu":8,"memory":27,"network":{"rx":8037.364175721619,"tx":220882.69115392308}},{"timestamp":1769498352375,"cpu":5,"memory":27,"network":{"rx":981.0657199526643,"tx":7625.94796406487}},{"timestamp":1769498412376,"cpu":5,"memory":27,"network":{"rx":608.4065265578907,"tx":375.7270712154798}},{"timestamp":1769498472379,"cpu":6,"memory":27,"network":{"rx":564.3551155775544,"tx":439.51135776544504}},{"timestamp":1769498532379,"cpu":5,"memory":27,"network":{"rx":432.21666666666664,"tx":461.93333333333334}},{"timestamp":1769498592383,"cpu":5,"memory":27,"network":{"rx":344.13279336033196,"tx":344.6161025282069}},{"timestamp":1769498652384,"cpu":5,"memory":27,"network":{"rx":371.3542881903936,"tx":372.1375954134862}},{"timestamp":1769498712386,"cpu":5,"memory":27,"network":{"rx":490.7169761007966,"tx":331.7889403686544}},{"timestamp":1769498772389,"cpu":5,"memory":27,"network":{"rx":435.9282035898205,"tx":359.4153625652051}},{"timestamp":1769498832389,"cpu":5,"memory":27,"network":{"rx":338.98333333333335,"tx":259.53333333333336}},{"timestamp":1769498892390,"cpu":5,"memory":27,"network":{"rx":459.24234596090065,"tx":1018.9830169497176}}] \ No newline at end of file diff --git a/frontend/data/subscriptions.json b/frontend/data/subscriptions.json new file mode 100644 index 0000000..d840e60 --- /dev/null +++ b/frontend/data/subscriptions.json @@ -0,0 +1 @@ +[{"endpoint":"https://fcm.googleapis.com/fcm/send/eZsR0N81Ung:APA91bHCYWk4RCoTebqCmyx-ll84oAF7LVDlF3ok1WDmLTVZP7vkD6B3hHmqvsb0zEVAFQ7XxQjFTjaUEPJ3rFX2p8vOGOYrTo-BYp4Q0hRDb76PQsixyoZmpGGhWXGTsTkMuJcKm9zS","expirationTime":null,"keys":{"p256dh":"BJMqcs9HG0iPmG1Y32gIefoQZFFRizyikKnSllNkuXa7QdKG7_XGlt8-klGVg5hTNOy1KQznuNZszLKrMIpVvyE","auth":"_yxFem3vuCmVuC4W0jA3ng"}}] \ No newline at end of file diff --git a/frontend/data/vapid.json b/frontend/data/vapid.json new file mode 100644 index 0000000..650c61b --- /dev/null +++ b/frontend/data/vapid.json @@ -0,0 +1 @@ +{"publicKey":"BNIK7MBUGgvdkwfxDckbbCEbFXtk6DWJg8bQ12SaDIiqWHpGJxx5hzjCEnqvAzNv7maZ_Mw2fTV8bJL22vRAtAU","privateKey":"xqscc6KwgAo2GCa5X4AJIhGiyZsr5lfC2BRSVCLzWrM"} \ No newline at end of file diff --git a/frontend/docker-compose-dev.yml b/frontend/docker-compose-dev.yml new file mode 100644 index 0000000..e61d5c2 --- /dev/null +++ b/frontend/docker-compose-dev.yml @@ -0,0 +1,25 @@ +services: + backend: + build: ./backend + container_name: snstatus-backend + restart: always + environment: + - NODE_ENV=production + privileged: true + volumes: + - /:/host:ro + - /proc:/host/proc:ro + - /sys:/host/sys:ro + - /var/run/docker.sock:/var/run/docker.sock + - ./backend/data:/app/data + ports: + - "8001:8001" + + frontend: + build: ./frontend + container_name: snstatus-frontend + restart: always + ports: + - "3005:80" + depends_on: + - backend diff --git a/frontend/docker-compose.yml b/frontend/docker-compose.yml new file mode 100644 index 0000000..1ed3542 --- /dev/null +++ b/frontend/docker-compose.yml @@ -0,0 +1,25 @@ +services: + backend: + image: sa8001/snstatus-backend:v0.1.5 + container_name: snstatus-backend + restart: always + environment: + - NODE_ENV=production + privileged: true + volumes: + - /:/host:ro + - /proc:/host/proc:ro + - /sys:/host/sys:ro + - /var/run/docker.sock:/var/run/docker.sock + - ./data:/app/data + ports: + - "8001:8001" + + frontend: + image: sa8001/snstatus-frontend:v0.1.5 + container_name: snstatus-frontend + restart: always + ports: + - "3005:80" + depends_on: + - backend diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..09dfa05 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + snStatus - Nas Monitor + + + + + + +
+ + + + \ No newline at end of file diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..afa694e --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,28 @@ +server { + listen 80; + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri $uri/ /index.html; + } + + # Proxy API requests to backend + location /api/ { + proxy_pass http://backend:8001; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + } + + # Proxy WebSocket requests + location /socket.io/ { + proxy_pass http://backend:8001; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..8fe43a9 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,28 @@ +{ + "name": "snstatus-frontend", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "lucide-react": "^0.309.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.21.1", + "recharts": "^2.10.3", + "socket.io-client": "^4.7.2", + "xterm": "^5.3.0", + "xterm-addon-fit": "^0.8.0" + }, + "devDependencies": { + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@vitejs/plugin-react": "^4.2.1", + "vite": "^5.0.8", + "vite-plugin-pwa": "^0.19.2" + } +} \ No newline at end of file diff --git a/frontend/public/apple-touch-icon.png b/frontend/public/apple-touch-icon.png new file mode 100644 index 0000000..5b34bf8 Binary files /dev/null and b/frontend/public/apple-touch-icon.png differ diff --git a/frontend/public/favicon.png b/frontend/public/favicon.png new file mode 100644 index 0000000..9a8c30a Binary files /dev/null and b/frontend/public/favicon.png differ diff --git a/frontend/public/icon.svg b/frontend/public/icon.svg new file mode 100644 index 0000000..4783de6 --- /dev/null +++ b/frontend/public/icon.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/public/push-handler.js b/frontend/public/push-handler.js new file mode 100644 index 0000000..bca946a --- /dev/null +++ b/frontend/public/push-handler.js @@ -0,0 +1,66 @@ +// Push notification event handler +// This will be injected into the Service Worker + +self.addEventListener('push', function (event) { + console.log('[SW Push] Push event received'); + + let notificationData = { + title: 'snStatus Alert', + body: 'System alert', + icon: '/pwa-192x192.png', + badge: '/pwa-192x192.png', + vibrate: [200, 100, 200], + requireInteraction: true, + tag: 'snstatus-alert', + data: { + url: '/' + } + }; + + if (event.data) { + try { + const data = event.data.json(); + console.log('[SW Push] Data:', data); + if (data.title) notificationData.title = data.title; + if (data.body) notificationData.body = data.body; + if (data.icon) notificationData.icon = data.icon; + } catch (e) { + console.error('[SW Push] Parse error:', e); + notificationData.body = event.data.text(); + } + } + + event.waitUntil( + self.registration.showNotification(notificationData.title, { + body: notificationData.body, + icon: notificationData.icon, + badge: notificationData.badge, + vibrate: notificationData.vibrate, + requireInteraction: notificationData.requireInteraction, + tag: notificationData.tag, + data: notificationData.data + }) + ); +}); + +self.addEventListener('notificationclick', function (event) { + console.log('[SW] Notification clicked'); + event.notification.close(); + + event.waitUntil( + clients.matchAll({ type: 'window', includeUncontrolled: true }) + .then(function (clientList) { + // Focus existing window if available + for (let i = 0; i < clientList.length; i++) { + const client = clientList[i]; + if (client.url === '/' && 'focus' in client) { + return client.focus(); + } + } + // Open new window + if (clients.openWindow) { + return clients.openWindow('/'); + } + }) + ); +}); diff --git a/frontend/public/pwa-192x192.png b/frontend/public/pwa-192x192.png new file mode 100644 index 0000000..e358ab0 Binary files /dev/null and b/frontend/public/pwa-192x192.png differ diff --git a/frontend/public/pwa-512x512.png b/frontend/public/pwa-512x512.png new file mode 100644 index 0000000..6abe905 Binary files /dev/null and b/frontend/public/pwa-512x512.png differ diff --git a/frontend/public/sw-custom.js b/frontend/public/sw-custom.js new file mode 100644 index 0000000..84eee8f --- /dev/null +++ b/frontend/public/sw-custom.js @@ -0,0 +1,50 @@ +// Custom Service Worker for Push Notifications +self.addEventListener('push', function (event) { + console.log('[SW] Push event received:', event); + + let notificationData = { + title: 'snStatus Alert', + body: 'System alert triggered', + icon: '/pwa-192x192.png', + badge: '/pwa-192x192.png', + vibrate: [200, 100, 200], + requireInteraction: true, + tag: 'snstatus-alert' + }; + + if (event.data) { + try { + const data = event.data.json(); + console.log('[SW] Push data:', data); + notificationData.title = data.title || notificationData.title; + notificationData.body = data.body || notificationData.body; + notificationData.icon = data.icon || notificationData.icon; + } catch (e) { + console.error('[SW] Error parsing push data:', e); + notificationData.body = event.data.text(); + } + } + + const promiseChain = self.registration.showNotification( + notificationData.title, + { + body: notificationData.body, + icon: notificationData.icon, + badge: notificationData.badge, + vibrate: notificationData.vibrate, + requireInteraction: notificationData.requireInteraction, + tag: notificationData.tag + } + ); + + event.waitUntil(promiseChain); +}); + +self.addEventListener('notificationclick', function (event) { + console.log('[SW] Notification clicked'); + event.notification.close(); + + event.waitUntil( + clients.openWindow('/') + ); +}); diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..6dc8911 --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { BrowserRouter, Routes, Route } from 'react-router-dom'; +import { SettingsProvider } from './context/SettingsContext'; +import Layout from './components/Layout'; +import Dashboard from './pages/Dashboard'; +import DockerManager from './pages/DockerManager'; +import Settings from './pages/Settings'; + +import History from './pages/History'; + +import DockerAuth from './components/DockerAuth'; + +function App() { + return ( + + + + }> + } /> + } /> + + + + } /> + } /> + + + + + ); +} + +export default App; diff --git a/frontend/src/components/AlertSettingsModal.jsx b/frontend/src/components/AlertSettingsModal.jsx new file mode 100644 index 0000000..bbc07a4 --- /dev/null +++ b/frontend/src/components/AlertSettingsModal.jsx @@ -0,0 +1,663 @@ +import React, { useState, useEffect } from 'react'; +import { X, Bell, AlertTriangle } from 'lucide-react'; + +const AlertSettingsModal = ({ isOpen, onClose, onSave }) => { + const [thresholds, setThresholds] = useState({ cpu: 90, memory: 90, disk: 90 }); + const [containerAlertEnabled, setContainerAlertEnabled] = useState(true); + const [alertCooldownSeconds, setAlertCooldownSeconds] = useState(300); + const [loading, setLoading] = useState(false); + const [notifPermission, setNotifPermission] = useState(Notification.permission); + const [hasActiveSubscription, setHasActiveSubscription] = useState(false); + + useEffect(() => { + if (isOpen) { + // Update permission state + setNotifPermission(Notification.permission); + + // Check for active subscription + const checkSubscription = async () => { + try { + if ('serviceWorker' in navigator) { + const registration = await navigator.serviceWorker.ready; + const subscription = await registration.pushManager.getSubscription(); + setHasActiveSubscription(!!subscription); + } + } catch (e) { + console.error('Failed to check subscription:', e); + setHasActiveSubscription(false); + } + }; + checkSubscription(); + + // Fetch current settings from backend + const fetchSettings = async () => { + try { + const res = await fetch('/api/docker/config'); + if (res.ok) { + const data = await res.json(); + if (data.alertThresholds) { + setThresholds(prev => ({ ...prev, ...data.alertThresholds })); + } + if (data.containerAlertEnabled !== undefined) { + setContainerAlertEnabled(data.containerAlertEnabled); + } + if (data.alertCooldownSeconds !== undefined) { + setAlertCooldownSeconds(data.alertCooldownSeconds); + } + } + } catch (e) { + console.error('Failed to fetch alert settings:', e); + } + }; + fetchSettings(); + } + }, [isOpen]); + + const enableNotifications = async () => { + console.log('=== 알림 권한 요청 시작 ==='); + console.log('Notification support:', 'Notification' in window); + console.log('ServiceWorker support:', 'serviceWorker' in navigator); + console.log('Secure context:', window.isSecureContext); + console.log('Current URL:', window.location.href); + console.log('User Agent:', navigator.userAgent); + + // Check if browser supports notifications + if (!('Notification' in window)) { + alert('이 브라우저는 알림을 지원하지 않습니다.'); + return; + } + + // Check if Service Worker is supported + if (!('serviceWorker' in navigator)) { + console.error('Service Worker not supported'); + alert('이 브라우저는 Service Worker를 지원하지 않습니다.\n\n브라우저 정보:\n- User Agent: ' + navigator.userAgent.substring(0, 100) + '...'); + return; + } + + // Check if we're on a secure context (HTTPS or localhost) + if (!window.isSecureContext) { + alert('푸시 알림은 HTTPS 또는 localhost 환경에서만 사용할 수 있습니다.\n현재 연결이 안전하지 않습니다.'); + return; + } + + try { + // Request notification permission + const permission = await Notification.requestPermission(); + console.log('Notification permission:', permission); + setNotifPermission(permission); + + if (permission !== 'granted') { + alert('알림 권한이 거부되었습니다.\n브라우저 설정에서 알림 권한을 허용해주세요.'); + return; + } + + // Wait for service worker to be ready + console.log('Waiting for service worker...'); + const registration = await navigator.serviceWorker.ready; + console.log('Service Worker ready:', registration); + + // Get VAPID public key + console.log('Fetching VAPID key...'); + const keyRes = await fetch('/api/notifications/vapidPublicKey'); + if (!keyRes.ok) { + throw new Error(`VAPID 키 가져오기 실패: ${keyRes.status}`); + } + const { publicKey } = await keyRes.json(); + console.log('VAPID key received'); + + // Subscribe to push notifications + console.log('Subscribing to push notifications...'); + const subscription = await registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: publicKey + }); + console.log('Push subscription created:', subscription); + + // Send subscription to backend + console.log('Sending subscription to backend...'); + const response = await fetch('/api/notifications/subscribe', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(subscription) + }); + + if (!response.ok) { + throw new Error(`구독 등록 실패: ${response.status}`); + } + + console.log('Subscription saved successfully'); + alert('푸시 알림이 설정되었습니다!'); + } catch (e) { + console.error('알림 설정 오류:', e); + + // Provide detailed error messages + let errorMsg = '알림 설정에 실패했습니다.\n\n'; + + if (e.name === 'NotAllowedError') { + errorMsg += '알림 권한이 거부되었습니다. 브라우저 설정에서 알림을 허용해주세요.'; + } else if (e.message.includes('VAPID')) { + errorMsg += 'VAPID 키를 가져오는데 실패했습니다. 서버 연결을 확인해주세요.'; + } else if (e.message.includes('구독')) { + errorMsg += '푸시 알림 구독에 실패했습니다. 서버 연결을 확인해주세요.'; + } else { + errorMsg += `오류: ${e.message}`; + } + + errorMsg += '\n\n콘솔(F12)에서 자세한 오류를 확인할 수 있습니다.'; + alert(errorMsg); + } + }; + + const disableNotifications = async () => { + try { + const registration = await navigator.serviceWorker.ready; + const subscription = await registration.pushManager.getSubscription(); + + if (subscription) { + // Unsubscribe from push notifications + await subscription.unsubscribe(); + + // Notify backend + await fetch('/api/notifications/unsubscribe', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ endpoint: subscription.endpoint }) + }); + + console.log('[Notifications] Unsubscribed from push notifications'); + } + + // CRITICAL: Unregister Service Worker completely + // This is essential for mobile devices where dev tools aren't available + await registration.unregister(); + console.log('[Service Worker] Unregistered successfully'); + + setNotifPermission('default'); + alert('푸시 알림이 해제되었습니다.\n\n페이지를 새로고침하여 Service Worker를 업데이트합니다.'); + + // Reload page to download and register new Service Worker + window.location.reload(); + } catch (e) { + console.error('[Notifications] 알림 해제 오류:', e); + alert('알림 해제 중 오류가 발생했습니다.\n\n페이지를 새로고침 후 다시 시도해주세요.'); + } + }; + + const handleSave = async () => { + // Validate inputs + if (!thresholds.cpu || !thresholds.memory || !thresholds.disk || !alertCooldownSeconds) { + alert('모든 값을 입력해주세요.\n\nCPU, 메모리, 디스크 임계값과 알림 재전송 대기 시간을 입력해야 합니다.'); + return; + } + + setLoading(true); + try { + // Save thresholds + const thresholdRes = await fetch('/api/settings/thresholds', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + thresholds, + containerAlertEnabled, + alertCooldownSeconds + }) + }); + + if (thresholdRes.ok) { + alert('알림 설정이 저장되었습니다.'); + if (onSave) onSave(); + onClose(); + } else { + alert('설정 저장 실패'); + } + } catch (e) { + console.error('Failed to save alert settings:', e); + alert('설정 저장 중 오류 발생'); + } finally { + setLoading(false); + } + }; + + if (!isOpen) return null; + + return ( +
+
e.stopPropagation()}> +
+
+ +

알림 설정

+
+ +
+ +
+
+
+ +

푸시 알림 권한

+
+

+ {notifPermission === 'granted' + ? '알림이 활성화되었습니다. 해제하려면 아래 버튼을 클릭하세요.' + : '리소스 임계값 초과 및 컨테이너 종료 시 푸시 알림을 받으려면 권한을 허용해야 합니다.'} +

+
+ {!hasActiveSubscription ? ( + + ) : ( + <> + + + + )} +
+
+ +
+
+ +

리소스 사용률 임계값

+
+

설정된 사용률을 초과하면 푸시 알림을 받습니다.

+ +
+
+ + setThresholds({ ...thresholds, cpu: parseInt(e.target.value) || '' })} + className="threshold-input" + /> +
+ +
+ + setThresholds({ ...thresholds, memory: parseInt(e.target.value) || '' })} + className="threshold-input" + /> +
+ +
+ + setThresholds({ ...thresholds, disk: parseInt(e.target.value) || '' })} + className="threshold-input" + /> +
+ +
+ + setAlertCooldownSeconds(parseInt(e.target.value) || '')} + className="threshold-input" + /> + + 동일한 알림이 다시 전송되기까지의 대기 시간 (최소 10초, 기본 300초) + +
+
+
+ +
+
+ +

컨테이너 알림

+
+

Docker 컨테이너가 중지되거나 종료될 때 알림을 받습니다.

+ +
+ +
+
+
+ +
+ + +
+
+ + +
+ ); +}; + +export default AlertSettingsModal; diff --git a/frontend/src/components/DockerAuth.jsx b/frontend/src/components/DockerAuth.jsx new file mode 100644 index 0000000..84b8dfd --- /dev/null +++ b/frontend/src/components/DockerAuth.jsx @@ -0,0 +1,141 @@ +import React, { useState } from 'react'; +import { useSettings } from '../context/SettingsContext'; +import { Lock } from 'lucide-react'; + +const DockerAuth = ({ children }) => { + const { dockerEnabled } = useSettings(); + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const [checking, setChecking] = useState(false); + + // If disabled globablly (from backend), show message + if (!dockerEnabled) { + return ( +
+ +

기능 비활성화

+

Docker 관리 기능이 현재 비활성화되어 있습니다.

+

설정 페이지에서 기능을 활성화해주세요.

+
+ ); + } + + const handleLogin = async (e) => { + e.preventDefault(); + setChecking(true); + setError(''); + + try { + const res = await fetch('/api/docker/verify', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ password }) + }); + const data = await res.json(); + + if (res.ok && data.success) { + setIsAuthenticated(true); + } else { + setError('비밀번호가 올바르지 않습니다.'); + } + } catch (err) { + setError('서버 연결 실패'); + } finally { + setChecking(false); + } + }; + + if (isAuthenticated) { + return <>{children}; + } + + return ( +
+
+
+ +

Docker 보안 접근

+
+

이 기능에 접근하려면 비밀번호를 입력하세요.

+ +
+ setPassword(e.target.value)} + className="auth-input" + autoFocus + /> + {error &&

{error}

} + +
+
+ + +
+ ); +}; + +export default DockerAuth; diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx new file mode 100644 index 0000000..974ab31 --- /dev/null +++ b/frontend/src/components/Layout.jsx @@ -0,0 +1,367 @@ +import React from 'react'; +import { NavLink, Outlet } from 'react-router-dom'; +import { useSettings } from '../context/SettingsContext'; +import { + Activity, + LayoutGrid, + Container, + Settings, + Moon, + Sun, + ChevronLeft, + ChevronRight +} from 'lucide-react'; + +const Layout = () => { + const { theme, toggleTheme, dockerEnabled, isSidebarCollapsed, setIsSidebarCollapsed } = useSettings(); + + return ( +
+ {/* Desktop Sidebar */} + + + {/* Mobile Bottom Navigation */} + + + {/* Main Content Area */} +
+ {/* Mobile Header */} +
+ +

snStatus

+
+ +
+ + +
+ ); +}; + +export default Layout; diff --git a/frontend/src/components/TerminalComponent.jsx b/frontend/src/components/TerminalComponent.jsx new file mode 100644 index 0000000..cc06fb5 --- /dev/null +++ b/frontend/src/components/TerminalComponent.jsx @@ -0,0 +1,91 @@ +import React, { useEffect, useRef } from 'react'; +import { Terminal } from 'xterm'; +import { FitAddon } from 'xterm-addon-fit'; +import { io } from 'socket.io-client'; +import 'xterm/css/xterm.css'; + +const TerminalComponent = ({ containerId }) => { + const terminalRef = useRef(null); + const socketRef = useRef(null); + const termRef = useRef(null); + + useEffect(() => { + // Initialize Socket + // Use relative path if proxied, or absolute for dev + // Since we are proxying /api, we might need to be careful with socket.io path + // Default socket.io path is /socket.io + // If nginx proxies /socket.io -> backend:8001/socket.io, it should work. + socketRef.current = io({ + path: '/socket.io', + }); + + // Initialize xterm + const term = new Terminal({ + cursorBlink: true, + theme: { + background: '#1e1e1e', + foreground: '#f0f0f0', + }, + fontFamily: 'Menlo, Monaco, "Courier New", monospace', + fontSize: 14, + }); + + const fitAddon = new FitAddon(); + term.loadAddon(fitAddon); + + if (terminalRef.current) { + term.open(terminalRef.current); + fitAddon.fit(); + termRef.current = term; + } + + // Attach to container + socketRef.current.emit('attach-terminal', containerId); + + // Handle Socket Events + socketRef.current.on('terminal-output', (data) => { + term.write(data); + }); + + // Handle Terminal Input + term.onData((data) => { + socketRef.current.emit('terminal-input', data); + }); + + // Handle Resize + const handleResize = () => { + fitAddon.fit(); + if (terminalRef.current) { + const { cols, rows } = term; + socketRef.current.emit('resize-terminal', { cols, rows }); + } + }; + + window.addEventListener('resize', handleResize); + + // Initial resize + setTimeout(() => handleResize(), 100); + + return () => { + window.removeEventListener('resize', handleResize); + if (socketRef.current) socketRef.current.disconnect(); + if (termRef.current) termRef.current.dispose(); + }; + }, [containerId]); + + return ( +
+ ); +}; + +export default TerminalComponent; diff --git a/frontend/src/context/SettingsContext.jsx b/frontend/src/context/SettingsContext.jsx new file mode 100644 index 0000000..ccf26aa --- /dev/null +++ b/frontend/src/context/SettingsContext.jsx @@ -0,0 +1,104 @@ +import React, { createContext, useState, useEffect, useContext } from 'react'; + +const SettingsContext = createContext(); + +export const useSettings = () => useContext(SettingsContext); + +export const SettingsProvider = ({ children }) => { + // Theme State + const [theme, setTheme] = useState(() => localStorage.getItem('theme') || 'light'); + + // Dashboard Chart Type State ('bar' or 'line') + const [chartType, setChartType] = useState(() => localStorage.getItem('chartType') || 'bar'); + + // Docker Feature Config (Global Security Setting) + const [dockerEnabled, setDockerEnabled] = useState(false); + + // Fetch Global Docker Config on mount + useEffect(() => { + const fetchConfig = async () => { + try { + const res = await fetch('/api/docker/config'); + if (res.ok) { + const data = await res.json(); + setDockerEnabled(data.enabled); + } + } catch (e) { + console.error('Failed to fetch docker config:', e); + } + }; + fetchConfig(); + }, []); + + // Network Interface Visibility State (Array of hidden interface names) + const [hiddenInterfaces, setHiddenInterfaces] = useState(() => { + const saved = localStorage.getItem('hiddenInterfaces'); + return saved !== null ? JSON.parse(saved) : []; + }); + + // Effects to save to localStorage and apply theme + useEffect(() => { + localStorage.setItem('theme', theme); + document.documentElement.setAttribute('data-theme', theme); + }, [theme]); + + useEffect(() => { + localStorage.setItem('chartType', chartType); + }, [chartType]); + + useEffect(() => { + localStorage.setItem('hiddenInterfaces', JSON.stringify(hiddenInterfaces)); + }, [hiddenInterfaces]); + + // Sidebar Collapsed State (true/false) + const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(() => { + const saved = localStorage.getItem('isSidebarCollapsed'); + return saved !== null ? JSON.parse(saved) : false; + }); + + useEffect(() => { + localStorage.setItem('isSidebarCollapsed', JSON.stringify(isSidebarCollapsed)); + }, [isSidebarCollapsed]); + + const toggleTheme = () => { + setTheme(prev => prev === 'light' ? 'dark' : 'light'); + }; + + const toggleInterfaceVisibility = (ifaceName) => { + setHiddenInterfaces(prev => { + if (prev.includes(ifaceName)) { + return prev.filter(name => name !== ifaceName); // Show (remove from hidden) + } else { + return [...prev, ifaceName]; // Hide (add to hidden) + } + }); + }; + + // Helper to refresh config manually (e.g. after Settings change) + const refreshDockerConfig = async () => { + try { + const res = await fetch('/api/docker/config'); + if (res.ok) { + const data = await res.json(); + setDockerEnabled(data.enabled); + } + } catch (e) { console.error(e); } + }; + + return ( + + {children} + + ); +}; diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..654de39 --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,334 @@ +:root { + /* Light Mode Colors */ + --bg-primary: #e0e5ec; + --bg-gradient: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); + --text-primary: #2d3748; + --text-secondary: #4a5568; + --glass-bg: rgba(255, 255, 255, 0.45); + --glass-border: rgba(255, 255, 255, 0.4); + --glass-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15); + --card-bg: rgba(255, 255, 255, 0.25); + + /* Red Accent */ + --accent-color: #ef4444; + --accent-glow: rgba(239, 68, 68, 0.3); + + /* Volume Orange */ + --volume-color: #f97316; + + /* Text Gradient (Force Orange) */ + --brand-gradient: linear-gradient(135deg, #2d3748 0%, #f97316 100%); + + --success-color: #38a169; + --warning-color: #dd6b20; + --error-color: #e53e3e; + --cpu-color: #3b82f6; + /* Blue for CPU */ + --sidebar-bg: rgba(255, 255, 255, 0.3); + + /* Font */ + --font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; +} + +[data-theme='dark'] { + /* Dark Mode Colors - Neutral/Darker */ + --bg-primary: #0a0a0a; + --bg-gradient: linear-gradient(135deg, #1a1a1a 0%, #000000 100%); + --text-primary: #f7fafc; + --text-secondary: #a0aec0; + --glass-bg: rgba(0, 0, 0, 0.6); + --glass-border: rgba(255, 255, 255, 0.08); + --glass-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.5); + --card-bg: rgba(30, 30, 30, 0.4); + + /* Red Accent in Dark */ + --accent-color: #f87171; + --accent-glow: rgba(248, 113, 113, 0.3); + + --volume-color: #fb923c; + + /* Force Light Mode Gradient in Dark Mode as requested (Orange-ish) */ + --brand-gradient: linear-gradient(135deg, #2d3748 0%, #f97316 100%); + + --sidebar-bg: rgba(10, 10, 10, 0.5); +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: var(--font-family); + background: var(--bg-gradient); + background-attachment: fixed; + color: var(--text-primary); + min-height: 100vh; + transition: background 0.3s ease, color 0.3s ease; + overflow-x: hidden; +} + +#root { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +/* Logo/Brand Styles - STRONG GRADIENT */ +.logo-text { + background: linear-gradient(135deg, #dc2626 0%, #ff8c00 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + font-size: 1.5rem; + font-weight: 700; + letter-spacing: -0.02em; + margin: 0; +} + +/* Model name within system-info-card - STRONG GRADIENT */ +.sys-details h3 { + background: linear-gradient(135deg, #dc2626 0%, #ff8c00 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + font-size: 1.5rem; + font-weight: 700; + margin: 0; + letter-spacing: -0.02em; +} + +.sys-icon svg { + background: linear-gradient(135deg, #dc2626 0%, #ff8c00 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* Light mode icon background */ +.sys-icon { + background: rgba(240, 240, 240, 0.95); + border-radius: 8px; + padding: 0.3rem; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); +} + +/* Dark mode icon background */ +[data-theme='dark'] .sys-icon { + background: rgba(30, 30, 30, 0.95); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); +} + +.sys-subtitle { + color: var(--text-secondary); + font-size: 0.9rem; + margin-top: 0.25rem; +} + +.sys-meta { + display: flex; + gap: 1.5rem; + margin-top: 1rem; +} + +.meta-item { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.meta-item .label { + font-size: 0.75rem; + color: var(--text-secondary); + text-transform: uppercase; +} + +/* Glassmorphism Utilities */ +.glass-panel { + background: var(--glass-bg); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid var(--glass-border); + box-shadow: var(--glass-shadow); + border-radius: 16px; +} + +.glass-card { + background: var(--card-bg); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + border: 1px solid var(--glass-border); + border-radius: 12px; + padding: 1.5rem; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.glass-card:hover { + transform: translateY(-2px); + box-shadow: 0 12px 40px 0 rgba(0, 0, 0, 0.1); +} + +/* Buttons */ +.btn { + padding: 0.5rem 1rem; + border-radius: 8px; + border: none; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + font-family: inherit; +} + +.btn-primary { + background: var(--accent-color); + color: white; +} + +.btn-primary:hover { + filter: brightness(1.1); + transform: translateY(-1px); +} + +.btn-icon { + background: transparent; + border: none; + padding: 0.4rem; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + color: var(--text-primary); + transition: background 0.2s; +} + +.btn-icon:hover { + background: rgba(125, 125, 125, 0.1); +} + +/* Scrollbar Styling */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: rgba(156, 163, 175, 0.5); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: rgba(156, 163, 175, 0.7); +} + +/* Page Containers - Standardized Layout */ +.page-container, +.dashboard-container, +.settings-container, +.history-container { + width: 100%; + max-width: 1280px; + margin: 0 auto; + padding: 1.5rem 1rem; + display: flex; + flex-direction: column; + gap: 1.5rem !important; + /* Force consistent gap */ +} + +@media (max-width: 768px) { + + /* Container 설정 */ + .page-container, + .dashboard-container, + .settings-container, + .history-container { + padding: 0 !important; + gap: 1rem !important; + width: 100% !important; + box-sizing: border-box !important; + } + + /* 모든 카드를 동일하게 - CLASS 통일 */ + .glass-card, + .stat-card, + .settings-card, + .chart-section, + .system-info-card, + .glass-panel, + .full-width, + .resource-card, + .widget-grid>*, + .stats-grid>*, + .settings-grid>* { + width: 100% !important; + max-width: 100% !important; + min-width: 100% !important; + margin: 0 !important; + padding: 1.5rem !important; + box-sizing: border-box !important; + } + + /* Container children - no margins */ + .page-container>*, + .dashboard-container>*, + .settings-container>*, + .history-container>* { + margin: 0 !important; + } + + /* settings-container 위 여백 제거 */ + .settings-container { + padding-top: 0 !important; + } + + /* Charts container */ + .charts-container { + width: 100% !important; + padding: 0 !important; + margin: 0 !important; + } + + /* Widget grids - single column with exact gap */ + .widget-grid, + .stats-grid, + .settings-grid { + grid-template-columns: 1fr !important; + gap: 1rem !important; + } + + /* mobile-header: 높이 40px, 간격 최소화 */ + .mobile-header { + height: 40px !important; + padding: 0.75rem 1.5rem !important; + margin-bottom: 0.5rem !important; + /* 간격 더 줄임 */ + } +} + +/* Loading Spinner Animation */ +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +.spin-animation { + animation: spin 1s linear infinite; +} + +.loading-container-center { + display: flex; + justify-content: center; + align-items: center; + height: 200px; + color: var(--text-secondary); + width: 100%; +} \ No newline at end of file diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx new file mode 100644 index 0000000..16e5bca --- /dev/null +++ b/frontend/src/main.jsx @@ -0,0 +1,13 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.jsx' +import './index.css' + +// vite-plugin-pwa handles Service Worker registration automatically +// Service Worker will be registered at /sw.js by the plugin + +ReactDOM.createRoot(document.getElementById('root')).render( + + + , +) diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx new file mode 100644 index 0000000..687d1b5 --- /dev/null +++ b/frontend/src/pages/Dashboard.jsx @@ -0,0 +1,502 @@ +import React, { useState, useEffect } from 'react'; +import { useOutletContext } from 'react-router-dom'; +import { useSettings } from '../context/SettingsContext'; +import { + HardDrive, + Cpu, + MemoryStick, + Network, + Server, + Activity, + Clock, + Loader2 +} from 'lucide-react'; +import { + BarChart, Bar, LineChart, Line, AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Cell +} from 'recharts'; + +// API URL +const API_URL = '/api/stats'; + +const Dashboard = () => { + const { chartType, hiddenInterfaces } = useSettings(); // Use Global Setting + const [stats, setStats] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [history, setHistory] = useState([]); + + useEffect(() => { + const fetchHistory = async () => { + try { + const response = await fetch('/api/history'); + const data = await response.json(); + + // Get last 60 minutes (60 points) + // Backend sends { timestamp, cpu, memory, network: {rx, tx} } + // Frontend needs { time, cpu, memory, rx, tx } + const formattedHistory = data.slice(-60).map(item => ({ + time: new Date(item.timestamp).toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit' }), + cpu: item.cpu, + memory: item.memory, + rx: item.network ? item.network.rx : 0, + tx: item.network ? item.network.tx : 0 + })); + setHistory(formattedHistory); + } catch (err) { + console.error('Failed to fetch initial history:', err); + } + }; + fetchHistory(); + + const fetchStats = async () => { + try { + const response = await fetch(API_URL); + if (!response.ok) { + throw new Error('Network response was not ok'); + } + const data = await response.json(); + setStats(data); + + // Update history for Line chart + setHistory(prev => { + const now = new Date().toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }); + + // Calculate Total Network Traffic + let totalRx = 0; + let totalTx = 0; + if (data.network) { + data.network.forEach(net => { + if (!hiddenInterfaces?.includes(net.iface)) { + totalRx += (net.rx_sec || 0); + totalTx += (net.tx_sec || 0); + } + }); + } + + const newPoint = { + time: now, + cpu: data.cpu.load, + memory: data.memory.percentage, + rx: totalRx, + tx: totalTx + }; + const newHistory = [...prev, newPoint]; + // Keep last 1 hour (assuming 5s updates + initial 1m data mixed) + // 1 hour = 720 points at 5s interval. + if (newHistory.length > 720) newHistory.shift(); + return newHistory; + }); + + setLoading(false); + } catch (err) { + console.error('Fetch error:', err); + setError('Failed to connect to server. Ensure Backend is running.'); + setLoading(false); + } + }; + + fetchStats(); + const interval = setInterval(fetchStats, 5000); + + return () => clearInterval(interval); + }, []); + + if (loading) return ( +
+ +
+ ); + + return ( +
+ {/* + Row 1: System Info Widget + */} +
+
+ + + + + + + + {/* NAS 외곽 케이스 */} + + {/* 상단 LED 패널 라인 */} + + {/* Drive Bay 1 */} + + {/* Drive Bay 2 */} + + {/* LED 표시등 1 */} + + {/* LED 표시등 2 */} + + {/* HDD 슬롯 라인 Bay 1 */} + + + {/* HDD 슬롯 라인 Bay 2 */} + + + {/* 전원 버튼 */} + + +
+
+

{stats?.system?.model || 'Synology NAS'}

+

시스템 모니터

+
+
+
+ OS + {stats?.system?.os} +
+
+ Version + {stats?.system?.release} +
+
+ Kernel + {stats?.system?.kernel} +
+
+
+ + {error && ( +
+ {error} +
+ )} + + {/* + Row 2: CPU & Memory (Half Width Each) + */} +
+ {/* CPU Widget */} +
+
+
+ +

CPU

+
+ {stats?.cpu?.load}% +
+ + {chartType === 'line' ? ( +
+ + + + + + +
+ ) : ( +
+
80 ? 'var(--error-color)' : 'var(--cpu-color)' + }} + /> +
+ )} +
+ + {/* Memory Widget */} +
+
+
+ +

메모리

+
+ {stats?.memory?.percentage}% +
+ + {chartType === 'line' ? ( +
+ + + + + + +
+ ) : ( +
+
80 ? 'var(--error-color)' : 'var(--success-color)' + }} + /> +
+ )} +
+
+ + {/* + Row 3: Disk Usage + */} +
+
+
+ +

볼륨 사용량

+
+
+
+ {stats?.storage?.length === 0 &&

볼륨 정보를 찾을 수 없습니다.

} + {stats?.storage?.map((disk, idx) => ( +
+
+ {disk.mount} + {(disk.used / 1024 / 1024 / 1024).toFixed(0)} GB / {(disk.size / 1024 / 1024 / 1024).toFixed(0)} GB +
+
+
80 ? 'var(--error-color)' : 'var(--volume-color)' + }} + /> +
+ {disk.use}% +
+ ))} +
+
+ + {/* Network Widget */} +
+
+
+ +

네트워크 (Total)

+
+
+ +
+ + + + + + + + + + + + + + + { + if (value < 1024) return `${value.toFixed(0)} B/s`; + if (value < 1024 * 1024) return `${(value / 1024).toFixed(0)} KB/s`; + return `${(value / 1024 / 1024).toFixed(1)} MB/s`; + }} + style={{ fontSize: '0.75rem', fill: 'var(--text-secondary)' }} + /> + { + let formatted; + if (value < 1024) formatted = `${value.toFixed(0)} B/s`; + else if (value < 1024 * 1024) formatted = `${(value / 1024).toFixed(1)} KB/s`; + else formatted = `${(value / 1024 / 1024).toFixed(2)} MB/s`; + return [formatted, name]; + }} + /> + + + + +
+ + {/* Optional: Simple List below chart for current values per interface */} +
+ {stats?.network?.filter(net => !hiddenInterfaces?.includes(net.iface)).map((net, idx) => { + const formatSpeed = (bytes) => { + if (!bytes) return '0 B/s'; + if (bytes < 1024) return `${bytes.toFixed(0)} B/s`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB/s`; + return `${(bytes / 1024 / 1024).toFixed(1)} MB/s`; + }; + return ( +
+ {net.iface} +
+ ↓ {formatSpeed(net.rx_sec)} + ↑ {formatSpeed(net.tx_sec)} +
+
+ ); + })} +
+
+ + +
+ ); +}; + +export default Dashboard; diff --git a/frontend/src/pages/DockerManager.jsx b/frontend/src/pages/DockerManager.jsx new file mode 100644 index 0000000..e75f65c --- /dev/null +++ b/frontend/src/pages/DockerManager.jsx @@ -0,0 +1,374 @@ +import React, { useState, useEffect } from 'react'; +import { + Box, + Play, + Square, + Terminal as TerminalIcon, + MoreHorizontal, + RefreshCw, + RotateCw, + FileText, + Search, + X, + Loader2 +} from 'lucide-react'; +import TerminalComponent from '../components/TerminalComponent'; + +const API_DOCKER_CONTAINERS = '/api/docker/containers'; + +const formatBytes = (bytes, decimals = 2) => { + if (!+bytes) return '0 Bytes'; + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`; +}; + +const DockerManager = () => { + const [containers, setContainers] = useState([]); + // ... (rest of state items are same) + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [activeTerminal, setActiveTerminal] = useState(null); // containerId + const [searchQuery, setSearchQuery] = useState(''); + const [showLogs, setShowLogs] = useState(null); // containerId + const [logsContent, setLogsContent] = useState(''); + const [actionLoading, setActionLoading] = useState(null); // containerId being acted upon + + const fetchContainers = async () => { + setLoading(true); + try { + const response = await fetch(API_DOCKER_CONTAINERS); + if (!response.ok) throw new Error('Failed to fetch containers'); + const data = await response.json(); + setContainers(data); + setError(null); + } catch (err) { + console.error(err); + setError('Could not load Docker containers.'); + } finally { + setLoading(false); + } + }; + + // ... (rest of useEffect and handlers existing) + + useEffect(() => { + fetchContainers(); + // Poll for updates every 5 seconds to keep stats fresh + const interval = setInterval(fetchContainers, 5000); + return () => clearInterval(interval); + }, []); + + // Docker Actions + const handleContainerAction = async (id, action) => { + setActionLoading(id); + try { + const response = await fetch(`/api/docker/containers/${id}/${action}`, { method: 'POST' }); + if (!response.ok) throw new Error(`Failed to ${action} container`); + await fetchContainers(); // Refresh list + } catch (err) { + alert(err.message); + } finally { + setActionLoading(null); + } + }; + + const handleFetchLogs = async (id) => { + setLogsContent('Loading logs...'); + setShowLogs(id); + try { + const response = await fetch(`/api/docker/containers/${id}/logs`); + if (!response.ok) throw new Error('Failed to fetch logs'); + const data = await response.text(); + setLogsContent(data); + } catch (err) { + setLogsContent(`Error: ${err.message}`); + } + }; + + return ( +
+ {/* ... (header code) */} +
+
+

도커 컨테이너

+
+ {!activeTerminal && ( +
+
+ + setSearchQuery(e.target.value)} + style={{ + padding: '0.5rem 1rem 0.5rem 2.2rem', + borderRadius: '8px', + border: '1px solid rgba(255,255,255,0.1)', + background: 'rgba(255,255,255,0.05)', + color: 'inherit', + outline: 'none', + width: '200px', + fontSize: '0.9rem' + }} + /> +
+ +
+ )} +
+ + {error && ( +
+ {error} +
+ )} + + {/* Terminal View */} + {activeTerminal ? ( +
+
+ 터미널 - {containers.find(c => c.Id === activeTerminal)?.Names[0] || activeTerminal} + +
+
+ +
+
+ ) : ( +
+ {loading && containers.length === 0 ? ( +
+ + +
+ ) : containers + .filter(c => + c.Names[0].toLowerCase().includes(searchQuery.toLowerCase()) || + c.Image.toLowerCase().includes(searchQuery.toLowerCase()) + ) + .map(container => ( +
+
+
+

{container.Names[0].replace(/^\//, '')}

+
+
+
+ 이미지: + {container.Image.length > 20 ? container.Image.substring(0, 20) + '...' : container.Image} +
+
+ 상태: + {container.State} +
+ + {/* Stats Display */} + {container.stats && container.State === 'running' && ( + <> +
+ CPU: + + {container.stats.cpu.toFixed(2)}% + +
+
+ Memory: + + {formatBytes(container.stats.memory)} + +
+ + )} +
+ + {/* Action Buttons */} +
+ {container.State === 'running' ? ( + <> + + + + + ) : ( + + )} + +
+
+ ))} +
+ )} + + {/* Logs Modal */} + {showLogs && ( +
+
+
+

로그

+ +
+
+                            {logsContent}
+                        
+
+
+ )} + + +
+ ); +}; + +export default DockerManager; diff --git a/frontend/src/pages/History.jsx b/frontend/src/pages/History.jsx new file mode 100644 index 0000000..140aaab --- /dev/null +++ b/frontend/src/pages/History.jsx @@ -0,0 +1,259 @@ +import React, { useState, useEffect, useMemo } from 'react'; +import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts'; +import { History as HistoryIcon, Clock, Loader2 } from 'lucide-react'; + +const History = () => { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [timeRange, setTimeRange] = useState('24h'); // '1h', '6h', '12h', '24h' + + useEffect(() => { + const fetchHistory = async () => { + try { + const response = await fetch('/api/history'); + if (!response.ok) throw new Error('Failed to fetch history'); + const result = await response.json(); + setData(result); + setLoading(false); + } catch (err) { + console.error('History fetch error:', err); + setError('Failed to load history data'); + setLoading(false); + } + }; + + fetchHistory(); + const interval = setInterval(fetchHistory, 60000); // 1 min update + return () => clearInterval(interval); + }, []); + + const filteredData = useMemo(() => { + if (!data.length) return []; + const now = Date.now(); + const ranges = { + '1h': 60 * 60 * 1000, + '6h': 6 * 60 * 60 * 1000, + '12h': 12 * 60 * 60 * 1000, + '24h': 24 * 60 * 60 * 1000, + '7d': 7 * 24 * 60 * 60 * 1000, // Add 7d support if retention allows + }; + const rangeMs = ranges[timeRange] || ranges['24h']; + const cutoff = now - rangeMs; + + return data + .filter(item => item.timestamp >= cutoff) + .map(item => ({ + ...item, + rx: item.network ? item.network.rx : 0, + tx: item.network ? item.network.tx : 0, + time: new Date(item.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) + })); + }, [data, timeRange]); + + if (loading) return ( +
+ +
+ ); + + const formatBytes = (bytes) => { + if (bytes === 0) return '0 B/s'; + const k = 1024; + const sizes = ['B/s', 'KB/s', 'MB/s', 'GB/s']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + }; + + const CustomTooltip = ({ active, payload, label, type }) => { + if (active && payload && payload.length) { + return ( +
+

{label}

+ {payload.map((entry, index) => ( +

+ {entry.name}: {type === 'network' ? formatBytes(entry.value) : `${entry.value}%`} +

+ ))} +
+ ); + } + return null; + }; + + return ( +
+
+
+
+ +

시스템 히스토리

+
+
+ {['1h', '6h', '12h', '24h', '7d'].map(range => ( + + ))} +
+
+ + {error ? ( +
{error}
+ ) : ( +
+
+

CPU 사용량

+
+ + + + + + + + + + + + } /> + + + +
+
+ +
+

메모리 사용량

+
+ + + + + + + + + + + + } /> + + + +
+
+ +
+

+ Download + Upload +

+
+ + + + + + + + + + + + + + + { + if (val === 0) return '0 B/s'; + if (val < 1024) return `${val.toFixed(0)} B/s`; + if (val < 1024 * 1024) return `${(val / 1024).toFixed(0)} KB/s`; + return `${(val / 1024 / 1024).toFixed(1)} MB/s`; + }} + /> + } /> + + + + +
+
+
+ )} +
+ + +
+ ); +}; + +export default History; diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx new file mode 100644 index 0000000..938218b --- /dev/null +++ b/frontend/src/pages/Settings.jsx @@ -0,0 +1,576 @@ +import React, { useState, useEffect } from 'react'; +import { useSettings } from '../context/SettingsContext'; +import { + BarChart2, + Activity, + Layout, + Settings as SettingsIcon, + Network, + Clock, + Sun, + Moon, + Bell, + Loader2 +} from 'lucide-react'; +import AlertSettingsModal from '../components/AlertSettingsModal'; + +const Settings = () => { + const { chartType, setChartType, dockerEnabled, refreshDockerConfig, hiddenInterfaces, toggleInterfaceVisibility, theme, toggleTheme } = useSettings(); + const [interfaces, setInterfaces] = useState([]); + const [retentionHours, setRetentionHours] = useState(24); + const [saving, setSaving] = useState(false); + + useEffect(() => { + // Fetch current interfaces + const fetchInterfaces = async () => { + try { + const response = await fetch('/api/stats'); + const data = await response.json(); + if (data.network) { + setInterfaces(data.network.map(n => n.iface)); + } + } catch (error) { + console.error('Failed to fetch stats:', error); + } + }; + fetchInterfaces(); + + // Fetch retention settings + const fetchRetention = async () => { + try { + const response = await fetch('/api/settings/retention'); + if (response.ok) { + const data = await response.json(); + setRetentionHours(data.hours); + } + } catch (error) { + console.error('Failed to fetch retention settings:', error); + } + }; + fetchRetention(); + }, []); + + const saveRetention = async () => { + setSaving(true); + try { + const response = await fetch('/api/settings/retention', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ hours: retentionHours }) + }); + if (response.ok) { + alert('설정이 저장되었습니다.'); + } else { + alert('설정 저장 실패'); + } + } catch (error) { + console.error('Failed to save settings:', error); + alert('설정 저장 중 오류 발생'); + } finally { + setSaving(false); + } + }; + + const [showAlertModal, setShowAlertModal] = useState(false); + const [passwordInput, setPasswordInput] = useState(''); + const [showPasswordInput, setShowPasswordInput] = useState(false); + + const handleDockerToggle = () => { + // Whether enabling or disabling, require password interaction + setShowPasswordInput(true); + }; + + const confirmDockerChange = async () => { + if (!passwordInput) return alert('비밀번호를 입력해주세요.'); + + const targetState = !dockerEnabled; // Toggle intent + + try { + const res = await fetch('/api/docker/config', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ enabled: targetState, password: passwordInput }) + }); + + if (res.ok) { + await refreshDockerConfig(); + setShowPasswordInput(false); + setPasswordInput(''); + alert(`Docker 기능이 ${targetState ? '활성화' : '비활성화'}되었습니다.`); + } else { + const data = await res.json(); + alert(data.error || '설정 실패'); + } + } catch (e) { + console.error(e); + alert('서버 요청 실패'); + } + }; + + return ( + <> + +
+
+

설정

+
+ +
+ {/* Theme Settings */} +
+
+ {theme === 'dark' ? : } +

테마 설정

+
+ +
+
+ 화면 모드 + 라이트 모드와 다크 모드를 전환합니다. +
+
+ +
+
+
+ + {/* Chart Settings */} +
+
+ +

대시보드 설정

+
+ +
+
+ 차트 시각화 + 시스템 리소스 표시 방식을 선택하세요. +
+
+ + +
+
+
+ + {/* Docker Security Settings */} +
+
+ +

Docker 보안 설정

+
+ +
+
+ 도커 관리 기능 + 기능 활성화 시 비밀번호 설정이 필요합니다. +
+
+
+ +
+ + {/* Password Prompt */} + {showPasswordInput && ( +
+ setPasswordInput(e.target.value)} + className="password-input" + autoFocus + /> +
+ + +
+
+ )} +
+
+
+ + {/* Network Interface Settings */} +
+
+ +

네트워크 인터페이스

+
+ +
+
+ 인터페이스 표시 설정 + 대시보드에 표시할 네트워크 인터페이스를 선택하세요. +
+
+ {interfaces.map(iface => ( + + ))} + {interfaces.length === 0 && ( +
+ +
+ )} +
+
+
+ + {/* Notification Settings */} +
+
+ +

알림 설정

+
+ +
+
+ 푸시 알림 및 임계값 설정 + 리소스 사용률 임계값과 컨테이너 종료 알림을 설정합니다. +
+
+ +
+
+
+ + {/* Alert Settings Modal */} + setShowAlertModal(false)} + onSave={() => { + // Optionally refresh settings or show success message + }} + /> + + {/* Data Retention Settings */} +
+
+ +

데이터 보관 설정

+
+ +
+
+ 히스토리 보관 기간 + 시스템 리소스 사용 기록을 보관할 기간을 설정합니다. (시간 단위) +
+
+ setRetentionHours(parseInt(e.target.value) || 0)} + className="number-input" + /> + 시간 + +
+
+
+
+ + +
+ + ); +}; + +export default Settings; diff --git a/frontend/src/sw-custom.js b/frontend/src/sw-custom.js new file mode 100644 index 0000000..4e9f65a --- /dev/null +++ b/frontend/src/sw-custom.js @@ -0,0 +1,81 @@ +import { precacheAndRoute } from 'workbox-precaching'; +import { registerRoute } from 'workbox-routing'; +import { CacheFirst } from 'workbox-strategies'; + +// Precache all assets +precacheAndRoute(self.__WB_MANIFEST); + +// Cache Google Fonts +registerRoute( + /^https:\/\/fonts\.googleapis\.com\/.*/i, + new CacheFirst({ + cacheName: 'google-fonts-cache', + }) +); + +// ===== PUSH NOTIFICATION HANDLER ===== +self.addEventListener('push', function (event) { + console.log('[SW] Push event received'); + + let notificationData = { + title: 'snStatus 알림', + body: '시스템 알림', + vibrate: [200, 100, 200], + requireInteraction: true, + tag: 'snstatus-alert', + data: { + url: '/' + } + }; + + if (event.data) { + try { + const data = event.data.json(); + console.log('[SW] Push data:', data); + if (data.title) notificationData.title = data.title; + if (data.body) notificationData.body = data.body; + } catch (e) { + console.error('[SW] Parse error:', e); + notificationData.body = event.data.text(); + } + } + + // ALWAYS show notification, regardless of page visibility + event.waitUntil( + self.registration.showNotification(notificationData.title, { + body: notificationData.body, + vibrate: notificationData.vibrate, + requireInteraction: notificationData.requireInteraction, + tag: notificationData.tag, + data: notificationData.data, + // Force notification to show even when page is visible + silent: false + }).then(() => { + console.log('[SW] Notification displayed successfully'); + }) + ); +}); + +self.addEventListener('notificationclick', function (event) { + console.log('[SW] Notification clicked'); + event.notification.close(); + + event.waitUntil( + clients.matchAll({ type: 'window', includeUncontrolled: true }) + .then(function (clientList) { + // Focus existing window if available + for (let i = 0; i < clientList.length; i++) { + const client = clientList[i]; + if (client.url.includes('/') && 'focus' in client) { + return client.focus(); + } + } + // Open new window + if (clients.openWindow) { + return clients.openWindow('/'); + } + }) + ); +}); + +console.log('[SW] Service Worker with push notification support loaded'); diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..4cf6f77 --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,70 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import { VitePWA } from 'vite-plugin-pwa' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + react(), + VitePWA({ + strategies: 'injectManifest', + srcDir: 'src', + filename: 'sw-custom.js', + registerType: 'autoUpdate', + injectRegister: 'auto', + devOptions: { + enabled: false + }, + workbox: { + globPatterns: ['**/*.{js,css,html,ico,png,svg,webp,woff,woff2}'], + // Simplified for faster builds + maximumFileSizeToCacheInBytes: 3000000 + }, + includeAssets: ['pwa-192x192.png', 'pwa-512x512.png'], + manifest: { + name: 'snStatus', + short_name: 'snStatus', + theme_color: '#1e3a8a', + background_color: '#1e3a8a', // Deep Navy for splash + display: 'standalone', + start_url: '/', + icons: [ + { + src: 'pwa-192x192.png', + sizes: '192x192', + type: 'image/png' + }, + { + src: 'pwa-512x512.png', + sizes: '512x512', + type: 'image/png', + purpose: 'any maskable' + } + ] + } + }) + ], + build: { + // Optimize for faster builds on NAS + sourcemap: false, // Disable source maps + minify: 'esbuild', // Use faster esbuild minifier + target: 'es2015', // Less transformation needed + cssCodeSplit: false, // Single CSS file + rollupOptions: { + output: { + manualChunks: undefined // Disable code splitting for faster build + } + }, + chunkSizeWarningLimit: 2000 // Suppress warnings + }, + server: { + port: 3000, + host: true, + proxy: { + '/api': { + target: 'http://backend:3001', + changeOrigin: true, + } + } + } +}) diff --git a/img/01.jpg b/img/01.jpg new file mode 100644 index 0000000..3ecefc3 Binary files /dev/null and b/img/01.jpg differ diff --git a/img/02.jpg b/img/02.jpg new file mode 100644 index 0000000..16f2054 Binary files /dev/null and b/img/02.jpg differ diff --git a/img/03.jpg b/img/03.jpg new file mode 100644 index 0000000..8c96367 Binary files /dev/null and b/img/03.jpg differ diff --git a/img/04.jpg b/img/04.jpg new file mode 100644 index 0000000..971e3f3 Binary files /dev/null and b/img/04.jpg differ diff --git a/img/05.jpg b/img/05.jpg new file mode 100644 index 0000000..e58af86 Binary files /dev/null and b/img/05.jpg differ diff --git a/img/06.jpg b/img/06.jpg new file mode 100644 index 0000000..f3ef2da Binary files /dev/null and b/img/06.jpg differ diff --git a/img/07.jpg b/img/07.jpg new file mode 100644 index 0000000..26f7db2 Binary files /dev/null and b/img/07.jpg differ diff --git a/img/08.png b/img/08.png new file mode 100644 index 0000000..6c15c20 Binary files /dev/null and b/img/08.png differ diff --git a/img/09.png b/img/09.png new file mode 100644 index 0000000..5080b4f Binary files /dev/null and b/img/09.png differ diff --git a/img/10.png b/img/10.png new file mode 100644 index 0000000..0720ef7 Binary files /dev/null and b/img/10.png differ diff --git a/img/11.png b/img/11.png new file mode 100644 index 0000000..bb50bb6 Binary files /dev/null and b/img/11.png differ diff --git a/img/12.png b/img/12.png new file mode 100644 index 0000000..d0f1d93 Binary files /dev/null and b/img/12.png differ