// 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/devs', async (req, res) => { const abortController = new AbortController() const timeout = setTimeout(() => abortController.abort(), 5000) try { const devs = await client.edevs(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/devdetails', async (req, res) => { const abortController = new AbortController() const timeout = setTimeout(() => abortController.abort(), 5000) try { const devs = await client.devDetails(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 devs = await client.edevs(abortController.signal) clearTimeout(timeout) // Извлечение температурных данных const temperatureData = { chip1: { temp_current: devs[0]['Temperature'] || null, temp_awg: devs[0]['Chip Temp Avg'] || null, temp_min: devs[0]['Chip Temp Min'] || null, temp_max: devs[0]['Chip Temp Max'] || null }, chip2: { temp_current: devs[1]['Temperature'] || null, temp_awg: devs[1]['Chip Temp Avg'] || null, temp_min: devs[1]['Chip Temp Min'] || null, temp_max: devs[1]['Chip Temp Max'] || 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) function format(num) { const del = 1_000_000 return Math.round(num / del * 100) / 100 } try { const summary = await client.summary(abortController.signal) clearTimeout(timeout) const hashrateData = { current: format(summary[0]['HS RT']), average: format(summary[0]['MHS av']), '1_sec': format(summary[0]['MHS 5s']), '1_min': format(summary[0]['MHS 1m']), '5_min': format(summary[0]['MHS 5m']), '15_min': format(summary[0]['MHS 15m']), unit: 'TH/s', power_watt: summary[0]['Power'], 'watt/th': summary[0]['Power Rate'] } 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/devs': 'Информация об устройствах (чипах)', 'GET /api/devdetails': 'Информация об устройствах', 'GET /api/pools': 'Информация о пулах', 'GET /api/temperature': 'Температура и вентиляторы', 'GET /api/hashrate': 'Текущий хешрейт', 'GET /api/health': 'Проверка состояния', 'GET /api/all': 'Все данные (агрегированные)', 'POST /api/reboot': 'Перезагрузка майнера' } }) }) // Обработчик 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) })