feat(nas,macmini): 수집기 코드 추가 및 DB 스키마 단순화\n\n- Mac Mini SSH 기반 원격 수집기 추가 (/api/mac-mini/*)\n- DS1525+ DSM API 기반 수집기 골격 추가 (로그인/코어/디스크 수집)\n- NAS/디스크 간소화 테이블 생성 (nas_data, nas_disk_data)\n- README 업데이트 (SSH 설정, 엔드포인트, 테이블 구성)\n- Docker 통합 후 동작 확인
This commit is contained in:
74
README.md
74
README.md
@@ -77,7 +77,20 @@ docker-compose logs -f
|
||||
docker-compose ps
|
||||
```
|
||||
|
||||
### 4. Tapo 기기 설정
|
||||
### 4. Mac Mini SSH 연결 설정 (집에서)
|
||||
|
||||
```bash
|
||||
# SSH 키 생성 (없는 경우)
|
||||
ssh-keygen -t rsa
|
||||
|
||||
# Mac Mini에 공개키 복사
|
||||
ssh-copy-id hyungiahn@192.168.1.122
|
||||
|
||||
# SSH 연결 테스트
|
||||
ssh hyungiahn@192.168.1.122 "echo 'SSH 연결 성공!'"
|
||||
```
|
||||
|
||||
### 5. Tapo 기기 설정
|
||||
|
||||
```bash
|
||||
# Tapo 기기 설정 파일 편집
|
||||
@@ -147,6 +160,25 @@ PUT /api/devices/:id # 디바이스 업데이트
|
||||
DELETE /api/devices/:id # 디바이스 삭제
|
||||
```
|
||||
|
||||
### 🖥️ Mac Mini 시스템 모니터링
|
||||
|
||||
```bash
|
||||
# 실시간 시스템 데이터 수집 (SSH 원격 실행)
|
||||
GET /api/mac-mini/collect
|
||||
|
||||
# 최근 데이터 조회
|
||||
GET /api/mac-mini/recent?limit=10
|
||||
|
||||
# 특정 기간 데이터 조회
|
||||
GET /api/mac-mini/history?startDate=2025-01-01&endDate=2025-01-02
|
||||
|
||||
# 시스템 상태 요약
|
||||
GET /api/mac-mini/summary
|
||||
|
||||
# SSH 연결 테스트
|
||||
GET /api/mac-mini/test
|
||||
```
|
||||
|
||||
### 💡 기본 사용 예제
|
||||
|
||||
```bash
|
||||
@@ -172,6 +204,12 @@ curl -X POST http://localhost:9306/api/tapo/devices \
|
||||
|
||||
# 전력 소비 데이터 조회
|
||||
curl http://localhost:9306/api/tapo/power
|
||||
|
||||
# Mac Mini 시스템 상태 확인
|
||||
curl http://localhost:9306/api/mac-mini/summary
|
||||
|
||||
# Mac Mini 실시간 데이터 수집
|
||||
curl http://localhost:9306/api/mac-mini/collect
|
||||
```
|
||||
|
||||
## 🗄️ 데이터베이스
|
||||
@@ -191,9 +229,11 @@ curl http://localhost:9306/api/tapo/power
|
||||
```yaml
|
||||
✅ devices # 디바이스 관리 (3개 기본 데이터)
|
||||
✅ users # 사용자 관리 (2개 기본 계정)
|
||||
✅ power_consumption # 전력 소비 데이터
|
||||
✅ network_traffic # 네트워크 트래픽 데이터
|
||||
✅ system_resources # 시스템 리소스 데이터
|
||||
✅ mac_mini_data # Mac Mini 시스템 데이터 (SSH 원격 수집)
|
||||
✅ nas_data # Synology NAS 데이터 (API 연동 예정)
|
||||
✅ power_consumption # 전력 소비 데이터 (기존)
|
||||
✅ network_traffic # 네트워크 트래픽 데이터 (기존)
|
||||
✅ system_resources # 시스템 리소스 데이터 (기존)
|
||||
```
|
||||
|
||||
### 기본 데이터
|
||||
@@ -211,12 +251,38 @@ curl http://localhost:9306/api/tapo/power
|
||||
|
||||
## 🔧 개발 가이드
|
||||
|
||||
### 환경 변수 설정
|
||||
|
||||
`.env` 파일에 다음 설정을 추가하세요:
|
||||
|
||||
```bash
|
||||
# Mac Mini SSH 설정 (집에서 설정)
|
||||
MAC_MINI_HOST=192.168.1.122
|
||||
MAC_MINI_USERNAME=hyungiahn
|
||||
MAC_MINI_SSH_KEY=/Users/hyungiahn/.ssh/id_rsa
|
||||
```
|
||||
|
||||
### 개발 환경별 작업
|
||||
|
||||
**외부에서 (VPN 환경):**
|
||||
- ✅ 데이터베이스 구조 설계
|
||||
- ✅ API 엔드포인트 개발
|
||||
- ✅ SSH 수집기 코드 작성
|
||||
- 🚧 프론트엔드 개발
|
||||
|
||||
**집에서 (로컬 네트워크):**
|
||||
- SSH 키 설정 및 연결 테스트
|
||||
- 실제 데이터 수집 테스트
|
||||
- NAS API 연동
|
||||
- 최종 통합 테스트
|
||||
|
||||
### 폴더 구조
|
||||
|
||||
```
|
||||
src/
|
||||
├── config/ # 설정 파일
|
||||
├── controllers/ # 컨트롤러
|
||||
├── collectors/ # 데이터 수집기 (SSH, API 연동)
|
||||
├── middleware/ # 미들웨어
|
||||
├── models/ # 데이터베이스 모델
|
||||
├── routes/ # 라우터
|
||||
|
||||
164
package-lock.json
generated
164
package-lock.json
generated
@@ -9,6 +9,7 @@
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.11.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
@@ -22,6 +23,7 @@
|
||||
"moment": "^2.29.4",
|
||||
"mysql2": "^3.6.0",
|
||||
"node-cron": "^3.0.2",
|
||||
"node-ssh": "^13.2.1",
|
||||
"redis": "^4.6.7",
|
||||
"sequelize": "^6.32.1",
|
||||
"tp-link-tapo-connect": "^2.0.7",
|
||||
@@ -1728,6 +1730,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asn1": {
|
||||
"version": "0.2.6",
|
||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
|
||||
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": "~2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/assert-plus": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
|
||||
@@ -1747,7 +1758,6 @@
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/aws-ssl-profiles": {
|
||||
@@ -1760,12 +1770,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "0.21.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
|
||||
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
|
||||
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.14.0"
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.4",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-jest": {
|
||||
@@ -1891,6 +1903,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bcrypt-pbkdf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
||||
"integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"tweetnacl": "^0.14.3"
|
||||
}
|
||||
},
|
||||
"node_modules/bcryptjs": {
|
||||
"version": "2.4.3",
|
||||
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
|
||||
@@ -2014,6 +2035,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/buildcheck": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz",
|
||||
"integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
@@ -2297,7 +2327,6 @@
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
@@ -2422,6 +2451,20 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/cpu-features": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz",
|
||||
"integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==",
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"buildcheck": "~0.0.6",
|
||||
"nan": "^2.19.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/create-jest": {
|
||||
"version": "29.7.0",
|
||||
"resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz",
|
||||
@@ -2504,7 +2547,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
@@ -2715,7 +2757,6 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
@@ -3252,7 +3293,6 @@
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
@@ -3542,7 +3582,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
@@ -5229,6 +5268,13 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/nan": {
|
||||
"version": "2.23.0",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz",
|
||||
"integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/natural-compare": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||
@@ -5271,6 +5317,38 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-ssh": {
|
||||
"version": "13.2.1",
|
||||
"resolved": "https://registry.npmjs.org/node-ssh/-/node-ssh-13.2.1.tgz",
|
||||
"integrity": "sha512-rfl4GWMygQfzlExPkQ2LWyya5n2jOBm5vhEnup+4mdw7tQhNpJWbP5ldr09Jfj93k5SfY5lxcn8od5qrQ/6mBg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-stream": "^2.0.0",
|
||||
"make-dir": "^3.1.0",
|
||||
"sb-promise-queue": "^2.1.0",
|
||||
"sb-scandir": "^3.1.0",
|
||||
"shell-escape": "^0.2.0",
|
||||
"ssh2": "^1.14.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/node-ssh/node_modules/make-dir": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"semver": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemon": {
|
||||
"version": "3.1.10",
|
||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",
|
||||
@@ -5791,6 +5869,12 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pstree.remy": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
||||
@@ -6103,11 +6187,31 @@
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sb-promise-queue": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/sb-promise-queue/-/sb-promise-queue-2.1.1.tgz",
|
||||
"integrity": "sha512-qXfdcJQMxMljxmPprn4Q4hl3pJmoljSCzUvvEBa9Kscewnv56n0KqrO6yWSrGLOL9E021wcGdPa39CHGKA6G0w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/sb-scandir": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/sb-scandir/-/sb-scandir-3.1.1.tgz",
|
||||
"integrity": "sha512-Q5xiQMtoragW9z8YsVYTAZcew+cRzdVBefPbb9theaIKw6cBo34WonP9qOCTKgyAmn/Ch5gmtAxT/krUgMILpA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sb-promise-queue": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -6307,6 +6411,12 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shell-escape": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/shell-escape/-/shell-escape-0.2.0.tgz",
|
||||
"integrity": "sha512-uRRBT2MfEOyxuECseCZd28jC1AJ8hmqqneWQ4VWUTgCAFvb3wKU1jLqj6egC4Exrr88ogg3dp+zroH4wJuaXzw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||
@@ -6480,6 +6590,23 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ssh2": {
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz",
|
||||
"integrity": "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"asn1": "^0.2.6",
|
||||
"bcrypt-pbkdf": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.16.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"cpu-features": "~0.0.10",
|
||||
"nan": "^2.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stack-trace": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
|
||||
@@ -6827,6 +6954,15 @@
|
||||
"uuid": "^8.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/tp-link-tapo-connect/node_modules/axios": {
|
||||
"version": "0.21.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
|
||||
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/triple-beam": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz",
|
||||
@@ -6836,6 +6972,12 @@
|
||||
"node": ">= 14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tweetnacl": {
|
||||
"version": "0.14.5",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
||||
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"lint:fix": "eslint src/ --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.11.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
@@ -30,6 +31,7 @@
|
||||
"moment": "^2.29.4",
|
||||
"mysql2": "^3.6.0",
|
||||
"node-cron": "^3.0.2",
|
||||
"node-ssh": "^13.2.1",
|
||||
"redis": "^4.6.7",
|
||||
"sequelize": "^6.32.1",
|
||||
"tp-link-tapo-connect": "^2.0.7",
|
||||
|
||||
@@ -74,6 +74,15 @@ app.listen(PORT, process.env.API_HOST || '0.0.0.0', async () => {
|
||||
logger.error('Failed to connect to database');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Mac Mini 수집기 초기화
|
||||
try {
|
||||
const macMiniCollector = require('./collectors/macMiniCollector');
|
||||
await macMiniCollector.initialize();
|
||||
logger.info('Mac Mini collector initialized successfully');
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize Mac Mini collector:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// 프로세스 종료 핸들링
|
||||
|
||||
364
src/collectors/macMiniCollector.js
Normal file
364
src/collectors/macMiniCollector.js
Normal file
@@ -0,0 +1,364 @@
|
||||
const { NodeSSH } = require('node-ssh');
|
||||
const logger = require('../utils/logger');
|
||||
const { Sequelize, DataTypes } = require('sequelize');
|
||||
const sequelize = require('../config/database');
|
||||
|
||||
class MacMiniCollector {
|
||||
constructor() {
|
||||
this.isInitialized = false;
|
||||
this.MacMiniData = null;
|
||||
this.sshConnection = new NodeSSH();
|
||||
this.macMiniConfig = {
|
||||
host: process.env.MAC_MINI_HOST || '192.168.1.122',
|
||||
username: process.env.MAC_MINI_USERNAME || 'hyungiahn',
|
||||
privateKey: process.env.MAC_MINI_SSH_KEY || '/Users/hyungiahn/.ssh/id_rsa'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 데이터베이스 모델 초기화
|
||||
*/
|
||||
async initialize() {
|
||||
try {
|
||||
// Mac Mini 데이터 모델 정의
|
||||
this.MacMiniData = sequelize.define('mac_mini_data', {
|
||||
id: {
|
||||
type: DataTypes.BIGINT,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
timestamp: {
|
||||
type: DataTypes.DATE(3),
|
||||
defaultValue: Sequelize.NOW
|
||||
},
|
||||
cpu_percent: {
|
||||
type: DataTypes.DECIMAL(5, 2),
|
||||
allowNull: true
|
||||
},
|
||||
memory_percent: {
|
||||
type: DataTypes.DECIMAL(5, 2),
|
||||
allowNull: true
|
||||
},
|
||||
disk_percent: {
|
||||
type: DataTypes.DECIMAL(5, 2),
|
||||
allowNull: true
|
||||
},
|
||||
power_watts: {
|
||||
type: DataTypes.DECIMAL(8, 2),
|
||||
allowNull: true
|
||||
},
|
||||
uptime_seconds: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: true
|
||||
}
|
||||
}, {
|
||||
tableName: 'mac_mini_data',
|
||||
timestamps: false,
|
||||
indexes: [
|
||||
{
|
||||
fields: ['timestamp']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
this.isInitialized = true;
|
||||
logger.info('MacMiniCollector initialized successfully');
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize MacMiniCollector:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SSH 연결 설정
|
||||
*/
|
||||
async connectSSH() {
|
||||
try {
|
||||
await this.sshConnection.connect(this.macMiniConfig);
|
||||
logger.info(`SSH connected to Mac Mini: ${this.macMiniConfig.host}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error('Failed to connect to Mac Mini via SSH:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SSH 연결 해제
|
||||
*/
|
||||
disconnectSSH() {
|
||||
this.sshConnection.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* 원격 명령어 실행
|
||||
*/
|
||||
async executeRemoteCommand(command) {
|
||||
try {
|
||||
const result = await this.sshConnection.execCommand(command);
|
||||
if (result.code === 0) {
|
||||
return result.stdout;
|
||||
} else {
|
||||
logger.error(`Remote command failed: ${command}`, result.stderr);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to execute remote command: ${command}`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CPU 사용률 수집 (원격)
|
||||
*/
|
||||
async getCpuUsage() {
|
||||
try {
|
||||
const output = await this.executeRemoteCommand("top -l 1 | grep 'CPU usage'");
|
||||
if (!output) return null;
|
||||
|
||||
// CPU usage: 8.57% user, 12.20% sys, 79.22% idle
|
||||
const match = output.match(/(\d+\.\d+)% idle/);
|
||||
if (match) {
|
||||
const idlePercent = parseFloat(match[1]);
|
||||
const usagePercent = 100 - idlePercent;
|
||||
return Math.round(usagePercent * 100) / 100; // 소수점 2자리
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
logger.error('Failed to get CPU usage:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 메모리 사용률 수집 (원격)
|
||||
*/
|
||||
async getMemoryUsage() {
|
||||
try {
|
||||
const output = await this.executeRemoteCommand("vm_stat");
|
||||
if (!output) return null;
|
||||
|
||||
const lines = output.split('\n');
|
||||
|
||||
let totalPages = 0;
|
||||
let freePages = 0;
|
||||
let inactivePages = 0;
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.includes('Pages free:')) {
|
||||
freePages = parseInt(line.match(/(\d+)/)[1]);
|
||||
} else if (line.includes('Pages active:')) {
|
||||
totalPages += parseInt(line.match(/(\d+)/)[1]);
|
||||
} else if (line.includes('Pages inactive:')) {
|
||||
inactivePages = parseInt(line.match(/(\d+)/)[1]);
|
||||
totalPages += inactivePages;
|
||||
} else if (line.includes('Pages speculative:')) {
|
||||
totalPages += parseInt(line.match(/(\d+)/)[1]);
|
||||
} else if (line.includes('Pages wired down:')) {
|
||||
totalPages += parseInt(line.match(/(\d+)/)[1]);
|
||||
}
|
||||
}
|
||||
|
||||
totalPages += freePages;
|
||||
const usedPages = totalPages - freePages - inactivePages;
|
||||
const memoryPercent = (usedPages / totalPages) * 100;
|
||||
|
||||
return Math.round(memoryPercent * 100) / 100;
|
||||
} catch (error) {
|
||||
logger.error('Failed to get memory usage:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 디스크 사용률 수집 (원격)
|
||||
*/
|
||||
async getDiskUsage() {
|
||||
try {
|
||||
const output = await this.executeRemoteCommand("df -h / | tail -1");
|
||||
if (!output) return null;
|
||||
|
||||
// /dev/disk3s1s1 460Gi 10Gi 358Gi 3% 426k 3.8G 0% /
|
||||
const match = output.match(/(\d+)%/);
|
||||
if (match) {
|
||||
return parseFloat(match[1]);
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
logger.error('Failed to get disk usage:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 시스템 업타임 수집 (초 단위) (원격)
|
||||
*/
|
||||
async getUptime() {
|
||||
try {
|
||||
const output = await this.executeRemoteCommand("uptime");
|
||||
if (!output) return null;
|
||||
|
||||
// 11:17 up 2 days, 15 hrs, 1 user, load averages: 2.50 2.32 2.51
|
||||
|
||||
let totalSeconds = 0;
|
||||
|
||||
// days 파싱
|
||||
const daysMatch = output.match(/(\d+) days?/);
|
||||
if (daysMatch) {
|
||||
totalSeconds += parseInt(daysMatch[1]) * 24 * 60 * 60;
|
||||
}
|
||||
|
||||
// hours 파싱
|
||||
const hoursMatch = output.match(/(\d+) hrs?/);
|
||||
if (hoursMatch) {
|
||||
totalSeconds += parseInt(hoursMatch[1]) * 60 * 60;
|
||||
}
|
||||
|
||||
// minutes 파싱 (만약 있다면)
|
||||
const minutesMatch = output.match(/(\d+) mins?/);
|
||||
if (minutesMatch) {
|
||||
totalSeconds += parseInt(minutesMatch[1]) * 60;
|
||||
}
|
||||
|
||||
return totalSeconds;
|
||||
} catch (error) {
|
||||
logger.error('Failed to get uptime:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tapo 플러그에서 전력 데이터 수집 (Mac Mini 전용 플러그)
|
||||
*/
|
||||
async getPowerUsage() {
|
||||
try {
|
||||
// Tapo 수집기에서 Mac Mini 전용 플러그 데이터 가져오기
|
||||
const tapoCollector = require('./tapoCollector');
|
||||
|
||||
// Mac Mini용 플러그 ID 찾기 (설정에서)
|
||||
const devices = tapoCollector.getAllDeviceConfigs();
|
||||
const macMiniPlug = devices.find(device =>
|
||||
device.location === '서재' && device.description &&
|
||||
device.description.includes('Mac Mini')
|
||||
);
|
||||
|
||||
if (macMiniPlug && tapoCollector.apis.has(macMiniPlug.id)) {
|
||||
const api = tapoCollector.apis.get(macMiniPlug.id);
|
||||
const powerData = await tapoCollector.collectFromDevice(macMiniPlug.id, api);
|
||||
return powerData ? powerData.watts : null;
|
||||
}
|
||||
|
||||
return null; // 전력 측정 플러그가 없거나 연결되지 않음
|
||||
} catch (error) {
|
||||
logger.error('Failed to get power usage:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 모든 시스템 데이터 수집 및 저장
|
||||
*/
|
||||
async collectAndSave() {
|
||||
if (!this.isInitialized) {
|
||||
logger.warn('MacMiniCollector not initialized. Skipping data collection.');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info('Collecting Mac Mini system data via SSH...');
|
||||
|
||||
// SSH 연결
|
||||
const connected = await this.connectSSH();
|
||||
if (!connected) {
|
||||
logger.error('Failed to establish SSH connection to Mac Mini');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// 모든 데이터 병렬 수집
|
||||
const [cpuPercent, memoryPercent, diskPercent, powerWatts, uptimeSeconds] = await Promise.all([
|
||||
this.getCpuUsage(),
|
||||
this.getMemoryUsage(),
|
||||
this.getDiskUsage(),
|
||||
this.getPowerUsage(),
|
||||
this.getUptime()
|
||||
]);
|
||||
|
||||
const systemData = {
|
||||
cpu_percent: cpuPercent,
|
||||
memory_percent: memoryPercent,
|
||||
disk_percent: diskPercent,
|
||||
power_watts: powerWatts,
|
||||
uptime_seconds: uptimeSeconds
|
||||
};
|
||||
|
||||
// 데이터베이스에 저장
|
||||
const savedData = await this.MacMiniData.create(systemData);
|
||||
|
||||
logger.info('Mac Mini data collected and saved:', {
|
||||
id: savedData.id,
|
||||
cpu: cpuPercent + '%',
|
||||
memory: memoryPercent + '%',
|
||||
disk: diskPercent + '%',
|
||||
power: powerWatts + 'W',
|
||||
uptime: Math.round(uptimeSeconds / 3600) + 'h'
|
||||
});
|
||||
|
||||
return savedData;
|
||||
|
||||
} finally {
|
||||
// SSH 연결 해제
|
||||
this.disconnectSSH();
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Failed to collect and save Mac Mini data:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 최근 데이터 조회
|
||||
*/
|
||||
async getRecentData(limit = 10) {
|
||||
if (!this.isInitialized) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.MacMiniData.findAll({
|
||||
order: [['timestamp', 'DESC']],
|
||||
limit: limit
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get recent Mac Mini data:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 기간 데이터 조회
|
||||
*/
|
||||
async getDataByDateRange(startDate, endDate) {
|
||||
if (!this.isInitialized) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.MacMiniData.findAll({
|
||||
where: {
|
||||
timestamp: {
|
||||
[Sequelize.Op.between]: [startDate, endDate]
|
||||
}
|
||||
},
|
||||
order: [['timestamp', 'ASC']]
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to get Mac Mini data by date range:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new MacMiniCollector();
|
||||
185
src/collectors/nasCollector.js
Normal file
185
src/collectors/nasCollector.js
Normal file
@@ -0,0 +1,185 @@
|
||||
const axios = require('axios');
|
||||
const logger = require('../utils/logger');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
class NasCollector {
|
||||
constructor() {
|
||||
this.baseUrl = process.env.NAS_HOST || 'http://192.168.1.2:5000';
|
||||
this.account = process.env.NAS_ACCOUNT || '';
|
||||
this.password = process.env.NAS_PASSWORD || '';
|
||||
this.verifyTls = (process.env.NAS_TLS_VERIFY || 'false') === 'true';
|
||||
this.sid = null;
|
||||
this.http = axios.create({
|
||||
baseURL: this.baseUrl,
|
||||
timeout: 10000,
|
||||
validateStatus: (s) => s >= 200 && s < 500,
|
||||
});
|
||||
}
|
||||
|
||||
async login() {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
api: 'SYNO.API.Auth',
|
||||
version: '3',
|
||||
method: 'login',
|
||||
account: this.account,
|
||||
passwd: this.password,
|
||||
session: 'Core',
|
||||
format: 'sid',
|
||||
});
|
||||
const { data } = await this.http.get(`/webapi/auth.cgi?${params.toString()}`);
|
||||
if (!data?.success) throw new Error(JSON.stringify(data));
|
||||
this.sid = data.data.sid;
|
||||
return true;
|
||||
} catch (err) {
|
||||
logger.error('NAS login failed', err.message || err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async logout() {
|
||||
try {
|
||||
if (!this.sid) return;
|
||||
const params = new URLSearchParams({
|
||||
api: 'SYNO.API.Auth',
|
||||
version: '3',
|
||||
method: 'logout',
|
||||
session: 'Core',
|
||||
_sid: this.sid,
|
||||
});
|
||||
await this.http.get(`/webapi/auth.cgi?${params.toString()}`);
|
||||
} catch (err) {
|
||||
// ignore
|
||||
} finally {
|
||||
this.sid = null;
|
||||
}
|
||||
}
|
||||
|
||||
async callEntry(api, method, version, params = {}) {
|
||||
const sp = new URLSearchParams({ api, method, version: String(version), _sid: this.sid, ...params });
|
||||
const { data } = await this.http.get(`/webapi/entry.cgi?${sp.toString()}`);
|
||||
if (!data?.success) throw new Error(`${api}.${method} failed: ${JSON.stringify(data)}`);
|
||||
return data.data;
|
||||
}
|
||||
|
||||
// Collect core NAS stats
|
||||
async collectCore() {
|
||||
const core = {
|
||||
cpu_percent: null,
|
||||
memory_percent: null,
|
||||
storage_percent: null,
|
||||
raid_status: 'healthy',
|
||||
active_sessions: 0,
|
||||
power_watts: null,
|
||||
};
|
||||
|
||||
try {
|
||||
// System utilization
|
||||
// SYNO.Core.System.Utilization
|
||||
const util = await this.callEntry('SYNO.Core.System.Utilization', 'get', 1);
|
||||
if (util?.cpu?.user !== undefined) {
|
||||
const user = util.cpu.user || 0;
|
||||
const system = util.cpu.system || 0;
|
||||
core.cpu_percent = Math.round((user + system) * 100) / 100;
|
||||
}
|
||||
if (util?.memory?.total && util?.memory?.avail_real !== undefined) {
|
||||
const total = util.memory.total;
|
||||
const avail = util.memory.avail_real;
|
||||
const usedPct = ((total - avail) / total) * 100;
|
||||
core.memory_percent = Math.round(usedPct * 100) / 100;
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn('NAS utilization fetch failed (ok in dev):', e.message || e);
|
||||
}
|
||||
|
||||
try {
|
||||
// Storage summary to compute storage_percent & raid status
|
||||
// SYNO.Core.Storage.Volume / Storage.Pool
|
||||
const pools = await this.callEntry('SYNO.Core.Storage.Pool', 'list', 1, { limit: 100, offset: 0 });
|
||||
if (Array.isArray(pools?.pools) && pools.pools.length > 0) {
|
||||
const sizeTotal = pools.pools.reduce((a, p) => a + (p.size?.total || 0), 0);
|
||||
const sizeUsed = pools.pools.reduce((a, p) => a + (p.size?.used || 0), 0);
|
||||
if (sizeTotal > 0) core.storage_percent = Math.round((sizeUsed / sizeTotal) * 10000) / 100;
|
||||
// raid status derive
|
||||
core.raid_status = pools.pools.some((p) => p.status && p.status !== 'healthy') ? 'degraded' : 'healthy';
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn('NAS storage fetch failed (ok in dev):', e.message || e);
|
||||
}
|
||||
|
||||
try {
|
||||
// Sessions
|
||||
const sessions = await this.callEntry('SYNO.Core.User.Session', 'list', 1, { limit: 500, offset: 0 });
|
||||
if (Array.isArray(sessions?.sessions)) core.active_sessions = sessions.sessions.length;
|
||||
} catch (e) {
|
||||
logger.warn('NAS sessions fetch failed (ok in dev):', e.message || e);
|
||||
}
|
||||
|
||||
return core;
|
||||
}
|
||||
|
||||
// Collect disk smart/health per bay
|
||||
async collectDisks() {
|
||||
const results = [];
|
||||
try {
|
||||
// SYNO.Core.Storage.Disk
|
||||
const diskList = await this.callEntry('SYNO.Core.Storage.Disk', 'list', 1, { limit: 100, offset: 0 });
|
||||
const disks = Array.isArray(diskList?.disks) ? diskList.disks : [];
|
||||
for (const d of disks) {
|
||||
results.push({
|
||||
bay_number: d?.slot || d?.bay || null,
|
||||
disk_model: d?.model || null,
|
||||
temperature: d?.temp || d?.temperature || null,
|
||||
health_status: d?.health || d?.status || 'healthy',
|
||||
reallocated_sectors: d?.smart_info?.reallocated_sector_ct || 0,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn('NAS disk list fetch failed (ok in dev):', e.message || e);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
async saveNasData(core) {
|
||||
const sql = `INSERT INTO nas_data (cpu_percent, memory_percent, storage_percent, raid_status, active_sessions, power_watts) VALUES (?, ?, ?, ?, ?, ?)`;
|
||||
const params = [
|
||||
core.cpu_percent,
|
||||
core.memory_percent,
|
||||
core.storage_percent,
|
||||
core.raid_status,
|
||||
core.active_sessions,
|
||||
core.power_watts,
|
||||
];
|
||||
await sequelize.query(sql, { replacements: params });
|
||||
}
|
||||
|
||||
async saveDiskData(disks) {
|
||||
if (!disks || disks.length === 0) return;
|
||||
const sql = `INSERT INTO nas_disk_data (bay_number, disk_model, temperature, health_status, reallocated_sectors) VALUES (?, ?, ?, ?, ?)`;
|
||||
for (const dk of disks) {
|
||||
const params = [dk.bay_number, dk.disk_model, dk.temperature, dk.health_status, dk.reallocated_sectors];
|
||||
await sequelize.query(sql, { replacements: params });
|
||||
}
|
||||
}
|
||||
|
||||
async collectAndSave() {
|
||||
const loggedIn = await this.login();
|
||||
if (!loggedIn) {
|
||||
logger.warn('Skip NAS collect: not logged in');
|
||||
return { saved: false };
|
||||
}
|
||||
try {
|
||||
const [core, disks] = await Promise.all([this.collectCore(), this.collectDisks()]);
|
||||
await this.saveNasData(core);
|
||||
await this.saveDiskData(disks);
|
||||
return { saved: true, coreCount: 1, diskCount: disks.length };
|
||||
} catch (err) {
|
||||
logger.error('NAS collect/save failed', err.message || err);
|
||||
return { saved: false };
|
||||
} finally {
|
||||
await this.logout();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new NasCollector();
|
||||
240
src/controllers/macMiniController.js
Normal file
240
src/controllers/macMiniController.js
Normal file
@@ -0,0 +1,240 @@
|
||||
const macMiniCollector = require('../collectors/macMiniCollector');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
class MacMiniController {
|
||||
constructor() {
|
||||
// MacMiniCollector 초기화는 app.js에서 관리
|
||||
}
|
||||
|
||||
/**
|
||||
* 실시간 시스템 데이터 수집 및 반환
|
||||
*/
|
||||
async getCurrentData(req, res) {
|
||||
try {
|
||||
const data = await macMiniCollector.collectAndSave();
|
||||
if (data) {
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Mac Mini data collected successfully',
|
||||
data: data
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to collect Mac Mini data'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error in getCurrentData:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to collect data',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 최근 데이터 조회
|
||||
*/
|
||||
async getRecentData(req, res) {
|
||||
try {
|
||||
const limit = parseInt(req.query.limit) || 10;
|
||||
const data = await macMiniCollector.getRecentData(limit);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
count: data.length,
|
||||
data: data
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error in getRecentData:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to retrieve recent data',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 기간 데이터 조회
|
||||
*/
|
||||
async getDataByDateRange(req, res) {
|
||||
try {
|
||||
const { startDate, endDate } = req.query;
|
||||
|
||||
if (!startDate || !endDate) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'startDate and endDate are required'
|
||||
});
|
||||
}
|
||||
|
||||
const data = await macMiniCollector.getDataByDateRange(
|
||||
new Date(startDate),
|
||||
new Date(endDate)
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
count: data.length,
|
||||
dateRange: { startDate, endDate },
|
||||
data: data
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error in getDataByDateRange:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to retrieve data by date range',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 시스템 상태 요약
|
||||
*/
|
||||
async getSystemSummary(req, res) {
|
||||
try {
|
||||
const recentData = await macMiniCollector.getRecentData(1);
|
||||
|
||||
if (recentData.length === 0) {
|
||||
return res.json({
|
||||
success: true,
|
||||
message: 'No data available',
|
||||
summary: null
|
||||
});
|
||||
}
|
||||
|
||||
const latest = recentData[0];
|
||||
const summary = {
|
||||
timestamp: latest.timestamp,
|
||||
system_health: this.assessSystemHealth(latest),
|
||||
cpu_status: this.getCpuStatus(latest.cpu_percent),
|
||||
memory_status: this.getMemoryStatus(latest.memory_percent),
|
||||
disk_status: this.getDiskStatus(latest.disk_percent),
|
||||
power_status: this.getPowerStatus(latest.power_watts),
|
||||
uptime_friendly: this.formatUptime(latest.uptime_seconds)
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
summary: summary,
|
||||
raw_data: latest
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error in getSystemSummary:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to generate system summary',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 시스템 상태 평가
|
||||
*/
|
||||
assessSystemHealth(data) {
|
||||
const issues = [];
|
||||
|
||||
if (data.cpu_percent > 80) issues.push('high_cpu');
|
||||
if (data.memory_percent > 85) issues.push('high_memory');
|
||||
if (data.disk_percent > 90) issues.push('high_disk');
|
||||
if (data.power_watts > 100) issues.push('high_power'); // Mac Mini 일반적으로 50W 내외
|
||||
|
||||
if (issues.length === 0) return 'healthy';
|
||||
if (issues.length <= 1) return 'warning';
|
||||
return 'critical';
|
||||
}
|
||||
|
||||
/**
|
||||
* CPU 상태 평가
|
||||
*/
|
||||
getCpuStatus(cpuPercent) {
|
||||
if (!cpuPercent) return 'unknown';
|
||||
if (cpuPercent < 50) return 'normal';
|
||||
if (cpuPercent < 80) return 'moderate';
|
||||
return 'high';
|
||||
}
|
||||
|
||||
/**
|
||||
* 메모리 상태 평가
|
||||
*/
|
||||
getMemoryStatus(memoryPercent) {
|
||||
if (!memoryPercent) return 'unknown';
|
||||
if (memoryPercent < 70) return 'normal';
|
||||
if (memoryPercent < 85) return 'moderate';
|
||||
return 'high';
|
||||
}
|
||||
|
||||
/**
|
||||
* 디스크 상태 평가
|
||||
*/
|
||||
getDiskStatus(diskPercent) {
|
||||
if (!diskPercent) return 'unknown';
|
||||
if (diskPercent < 80) return 'normal';
|
||||
if (diskPercent < 90) return 'moderate';
|
||||
return 'high';
|
||||
}
|
||||
|
||||
/**
|
||||
* 전력 상태 평가
|
||||
*/
|
||||
getPowerStatus(powerWatts) {
|
||||
if (!powerWatts) return 'unknown';
|
||||
if (powerWatts < 60) return 'normal'; // Mac Mini 평상시
|
||||
if (powerWatts < 100) return 'moderate'; // 높은 부하
|
||||
return 'high'; // 비정상적으로 높음
|
||||
}
|
||||
|
||||
/**
|
||||
* 업타임을 친숙한 형태로 변환
|
||||
*/
|
||||
formatUptime(uptimeSeconds) {
|
||||
if (!uptimeSeconds) return 'unknown';
|
||||
|
||||
const days = Math.floor(uptimeSeconds / (24 * 3600));
|
||||
const hours = Math.floor((uptimeSeconds % (24 * 3600)) / 3600);
|
||||
const minutes = Math.floor((uptimeSeconds % 3600) / 60);
|
||||
|
||||
if (days > 0) {
|
||||
return `${days}일 ${hours}시간`;
|
||||
} else if (hours > 0) {
|
||||
return `${hours}시간 ${minutes}분`;
|
||||
} else {
|
||||
return `${minutes}분`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 테스트용 - 시스템 명령어 직접 실행
|
||||
*/
|
||||
async testSystemCommands(req, res) {
|
||||
try {
|
||||
const tests = {
|
||||
cpu: await macMiniCollector.getCpuUsage(),
|
||||
memory: await macMiniCollector.getMemoryUsage(),
|
||||
disk: await macMiniCollector.getDiskUsage(),
|
||||
uptime: await macMiniCollector.getUptime(),
|
||||
power: await macMiniCollector.getPowerUsage()
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'System commands test completed',
|
||||
tests: tests
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error in testSystemCommands:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to test system commands',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new MacMiniController();
|
||||
@@ -11,6 +11,7 @@ router.get('/', (req, res) => {
|
||||
health: '/health',
|
||||
devices: '/api/devices',
|
||||
tapo: '/api/tapo',
|
||||
macMini: '/api/mac-mini',
|
||||
power: '/api/power',
|
||||
network: '/api/network',
|
||||
system: '/api/system',
|
||||
@@ -23,6 +24,7 @@ router.get('/', (req, res) => {
|
||||
// 라우터 연결
|
||||
router.use('/devices', require('./devices'));
|
||||
router.use('/tapo', require('./tapo'));
|
||||
router.use('/mac-mini', require('./macMini'));
|
||||
// router.use('/power', require('./power'));
|
||||
// router.use('/network', require('./network'));
|
||||
// router.use('/system', require('./system'));
|
||||
|
||||
20
src/routes/macMini.js
Normal file
20
src/routes/macMini.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const macMiniController = require('../controllers/macMiniController');
|
||||
|
||||
// 실시간 시스템 데이터 수집
|
||||
router.get('/collect', macMiniController.getCurrentData);
|
||||
|
||||
// 최근 데이터 조회
|
||||
router.get('/recent', macMiniController.getRecentData);
|
||||
|
||||
// 특정 기간 데이터 조회
|
||||
router.get('/history', macMiniController.getDataByDateRange);
|
||||
|
||||
// 시스템 상태 요약
|
||||
router.get('/summary', macMiniController.getSystemSummary);
|
||||
|
||||
// 테스트용 - 시스템 명령어 직접 실행
|
||||
router.get('/test', macMiniController.testSystemCommands);
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user