init repo

This commit is contained in:
2026-02-01 16:48:09 +07:00
commit e16a4b2ce1
5 changed files with 1618 additions and 0 deletions

4
.env Normal file
View File

@@ -0,0 +1,4 @@
PORT=3000
MINER_IP=192.168.31.174
MINER_PORT=4028
NODE_ENV=production

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/node_modules

352
app.js Normal file
View File

@@ -0,0 +1,352 @@
// server.js
const express = require('express')
const cors = require('cors')
const { WhatsMiner, ResponseError } = require('@thermocline-labs/whats-miner')
const app = express()
const PORT = process.env.PORT || 3000
const MINER_IP = process.env.MINER_IP || '192.168.31.174'
const MINER_PORT = parseInt(process.env.MINER_PORT || '4028')
// Middleware
app.use(cors())
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
// Логирование запросов
app.use((req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`)
next()
})
// Инициализация клиента майнера
const client = new WhatsMiner(MINER_IP, MINER_PORT)
// Обработчик ошибок
const errorHandler = (err, res, customMessage = 'Internal server error') => {
console.error('Error:', err)
if (err instanceof ResponseError) {
return res.status(502).json({
success: false,
error: {
type: 'ResponseError',
message: err.message,
code: err.code
}
})
}
if (err.name === 'AbortError') {
return res.status(504).json({
success: false,
error: {
type: 'TimeoutError',
message: 'Request timeout'
}
})
}
return res.status(500).json({
success: false,
error: {
type: err.name || 'UnknownError',
message: customMessage,
details: process.env.NODE_ENV === 'development' ? err.message : undefined
}
})
}
// Эндпоинт: получение общей информации
app.get('/api/summary', async (req, res) => {
const abortController = new AbortController()
const timeout = setTimeout(() => abortController.abort(), 5000) // 5 сек таймаут
try {
const summary = await client.summary(abortController.signal)
clearTimeout(timeout)
res.json({
success: true,
timestamp: new Date().toISOString(),
data: summary
})
} catch (err) {
clearTimeout(timeout)
errorHandler(err, res, 'Failed to fetch summary')
}
})
// Эндпоинт: статистика майнера
app.get('/api/stats', async (req, res) => {
const abortController = new AbortController()
const timeout = setTimeout(() => abortController.abort(), 5000)
try {
const stats = await client.stats(abortController.signal)
clearTimeout(timeout)
res.json({
success: true,
timestamp: new Date().toISOString(),
data: stats
})
} catch (err) {
clearTimeout(timeout)
errorHandler(err, res, 'Failed to fetch stats')
}
})
// Эндпоинт: информация об устройствах (чипах)
app.get('/api/devs', async (req, res) => {
const abortController = new AbortController()
const timeout = setTimeout(() => abortController.abort(), 5000)
try {
const devs = await client.devs(abortController.signal)
clearTimeout(timeout)
res.json({
success: true,
timestamp: new Date().toISOString(),
data: devs
})
} catch (err) {
clearTimeout(timeout)
errorHandler(err, res, 'Failed to fetch devices')
}
})
// Эндпоинт: информация о пулах
app.get('/api/pools', async (req, res) => {
const abortController = new AbortController()
const timeout = setTimeout(() => abortController.abort(), 5000)
try {
const pools = await client.pools(abortController.signal)
clearTimeout(timeout)
res.json({
success: true,
timestamp: new Date().toISOString(),
data: pools
})
} catch (err) {
clearTimeout(timeout)
errorHandler(err, res, 'Failed to fetch pools')
}
})
// Эндпоинт: температура и вентиляторы
app.get('/api/temperature', async (req, res) => {
const abortController = new AbortController()
const timeout = setTimeout(() => abortController.abort(), 5000)
try {
const summary = await client.summary(abortController.signal)
clearTimeout(timeout)
// Извлечение температурных данных
const temperatureData = {
board_temp: summary.BoardTemp || summary.temp || null,
chip_temp: summary.ChipTemp || summary.temp2 || null,
fan_speed: summary.FanSpeed || summary.fan || null,
fan_percent: summary.FanPercent || null
}
res.json({
success: true,
timestamp: new Date().toISOString(),
data: temperatureData
})
} catch (err) {
clearTimeout(timeout)
errorHandler(err, res, 'Failed to fetch temperature data')
}
})
// Эндпоинт: хешрейт
app.get('/api/hashrate', async (req, res) => {
const abortController = new AbortController()
const timeout = setTimeout(() => abortController.abort(), 5000)
try {
const summary = await client.summary(abortController.signal)
clearTimeout(timeout)
const hashrateData = {
current: summary.GHS_5s || summary.hashrate || 0,
average: summary.GHS_av || summary.hashrate_avg || 0,
realtime: summary.GHS_rt || summary.hashrate_rt || 0,
unit: 'GH/s'
}
res.json({
success: true,
timestamp: new Date().toISOString(),
data: hashrateData
})
} catch (err) {
clearTimeout(timeout)
errorHandler(err, res, 'Failed to fetch hashrate')
}
})
// Эндпоинт: перезагрузка майнера
app.post('/api/reboot', async (req, res) => {
const abortController = new AbortController()
const timeout = setTimeout(() => abortController.abort(), 10000)
try {
const result = await client.reboot(abortController.signal)
clearTimeout(timeout)
res.json({
success: true,
message: 'Miner reboot initiated',
data: result
})
} catch (err) {
clearTimeout(timeout)
errorHandler(err, res, 'Failed to reboot miner')
}
})
// Эндпоинт: проверка состояния
app.get('/api/health', async (req, res) => {
const abortController = new AbortController()
const timeout = setTimeout(() => abortController.abort(), 3000)
try {
const summary = await client.summary(abortController.signal)
clearTimeout(timeout)
res.json({
success: true,
status: 'healthy',
miner: {
ip: MINER_IP,
port: MINER_PORT,
connected: true,
uptime: summary.Uptime || null,
hashrate: summary.GHS_5s || 0
},
timestamp: new Date().toISOString()
})
} catch (err) {
clearTimeout(timeout)
res.status(503).json({
success: false,
status: 'unhealthy',
miner: {
ip: MINER_IP,
port: MINER_PORT,
connected: false
},
error: err.message,
timestamp: new Date().toISOString()
})
}
})
// Эндпоинт: все данные (агрегированный)
app.get('/api/all', async (req, res) => {
const abortController = new AbortController()
const timeout = setTimeout(() => abortController.abort(), 10000)
try {
const [summary, stats, devs, pools] = await Promise.all([
client.summary(abortController.signal),
client.stats(abortController.signal),
client.devs(abortController.signal),
client.pools(abortController.signal)
])
clearTimeout(timeout)
const aggregatedData = {
summary,
stats,
devs,
pools,
quick_stats: {
hashrate: summary.GHS_5s || 0,
temperature: summary.BoardTemp || summary.temp || null,
uptime: summary.Uptime || null,
chips_active: devs?.DEVS?.reduce((sum, dev) => sum + (dev.ChipsActive || 0), 0) || null
}
}
res.json({
success: true,
timestamp: new Date().toISOString(),
data: aggregatedData
})
} catch (err) {
clearTimeout(timeout)
errorHandler(err, res, 'Failed to fetch all data')
}
})
// Главная страница (документация)
app.get('/', (req, res) => {
res.json({
api: 'WhatsMiner REST API',
version: '1.0.0',
miner: {
ip: MINER_IP,
port: MINER_PORT
},
endpoints: {
'GET /api/summary': 'Общая информация о майнере',
'GET /api/stats': 'Статистика майнера',
'GET /api/devs': 'Информация об устройствах (чипах)',
'GET /api/pools': 'Информация о пулах',
'GET /api/temperature': 'Температура и вентиляторы',
'GET /api/hashrate': 'Текущий хешрейт',
'GET /api/health': 'Проверка состояния',
'GET /api/all': 'Все данные (агрегированные)',
'POST /api/reboot': 'Перезагрузка майнера'
},
documentation: 'https://github.com/thermocline-labs/whats-miner'
})
})
// Обработчик 404
app.use((req, res) => {
res.status(404).json({
success: false,
error: {
type: 'NotFoundError',
message: 'Endpoint not found',
path: req.path
}
})
})
// Запуск сервера
app.listen(PORT, () => {
console.log(`🚀 WhatsMiner API запущен на порту ${PORT}`)
console.log(`📍 IP майнера: ${MINER_IP}:${MINER_PORT}`)
console.log(`📚 Документация: http://localhost:${PORT}`)
})
// Обработка завершения
process.on('SIGTERM', () => {
console.log('SIGTERM received, shutting down gracefully')
process.exit(0)
})
process.on('SIGINT', () => {
console.log('SIGINT received, shutting down gracefully')
process.exit(0)
})

1240
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

21
package.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "whatsminer-api",
"version": "1.0.0",
"description": "REST API для мониторинга WhatsMiner",
"main": "app.js",
"scripts": {
"start": "node app.js",
"dev": "nodemon app.js"
},
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"@thermocline-labs/whats-miner": "^1.0.0"
},
"devDependencies": {
"nodemon": "^3.0.1"
},
"engines": {
"node": ">=14.0.0"
}
}