From a34d5b0c0d1faf8e67a33b7da0a813a48b5cc37a Mon Sep 17 00:00:00 2001 From: paragmore Date: Wed, 11 Feb 2026 20:33:51 +0530 Subject: [PATCH 1/2] Add site status live api --- config/common.json.example | 3 +- tests/unit/handlers/site.handlers.test.js | 208 +++++++++++++ tests/unit/routes/site.routes.test.js | 55 ++++ workers/lib/constants.js | 9 +- workers/lib/server/handlers/site.handlers.js | 292 +++++++++++++++++++ workers/lib/server/index.js | 30 +- workers/lib/server/routes/site.routes.js | 26 ++ 7 files changed, 606 insertions(+), 17 deletions(-) create mode 100644 tests/unit/handlers/site.handlers.test.js create mode 100644 tests/unit/routes/site.routes.test.js create mode 100644 workers/lib/server/handlers/site.handlers.js create mode 100644 workers/lib/server/routes/site.routes.js diff --git a/config/common.json.example b/config/common.json.example index d6b1811..832e1b1 100644 --- a/config/common.json.example +++ b/config/common.json.example @@ -13,7 +13,8 @@ "/auth/actions/batch": "30s", "/auth/actions/:type": "30s", "/auth/actions/:type/:id": "30s", - "/auth/global/data": "30s" + "/auth/global/data": "30s", + "/auth/site/status/live": "15s" }, "featureConfig": { "comments": true, diff --git a/tests/unit/handlers/site.handlers.test.js b/tests/unit/handlers/site.handlers.test.js new file mode 100644 index 0000000..f328c7d --- /dev/null +++ b/tests/unit/handlers/site.handlers.test.js @@ -0,0 +1,208 @@ +'use strict' + +const test = require('brittle') +const { getSiteLiveStatus } = require('../../../workers/lib/server/handlers/site.handlers') + +function createMockCtx (tailLogMultiResponse, extDataResponse, globalConfigResponse) { + return { + conf: { + orks: [{ rpcPublicKey: 'key1' }] + }, + net_r0: { + jRequest: async (key, method) => { + if (method === 'tailLogMulti') return tailLogMultiResponse + if (method === 'getWrkExtData') return extDataResponse + if (method === 'getGlobalConfig') return globalConfigResponse + return {} + } + } + } +} + +test('getSiteLiveStatus - returns composed response with correct structure', async (t) => { + const tailLogMultiResponse = [ + // Key 0: miner stats + [{ hashrate_mhs_1m_sum_aggr: 601432498437, nominal_hashrate_mhs_sum_aggr: 741423000000, online_or_minor_error_miners_amount_aggr: 1850, not_mining_miners_amount_aggr: 23, offline_or_sleeping_miners_amount_aggr: 45, hashrate_mhs_1m_cnt_aggr: 1930, alerts_aggr: { critical: 8, high: 12, medium: 39 } }], + // Key 1: powermeter stats + [{ site_power_w: 16701560 }], + // Key 2: container stats + [{ container_nominal_miner_capacity_sum_aggr: 2000 }] + ] + + const extDataResponse = [ + { stats: { hashrate: 279670375560265, active_workers_count: 1823, worker_count: 1930 } } + ] + + const globalConfigResponse = { + nominalHashrate: 741423000000, + nominalPowerAvailability_MW: 22.5 + } + + const ctx = createMockCtx(tailLogMultiResponse, extDataResponse, globalConfigResponse) + const req = { query: {} } + + const result = await getSiteLiveStatus(ctx, req) + + t.ok(result.hashrate, 'should have hashrate') + t.is(result.hashrate.value, 601432498437, 'hashrate value should match') + t.is(result.hashrate.nominal, 741423000000, 'hashrate nominal should match') + t.ok(result.hashrate.utilization > 0, 'hashrate utilization should be > 0') + + t.ok(result.power, 'should have power') + t.is(result.power.value, 16701560, 'power value should match') + t.is(result.power.nominal, 22500000, 'power nominal should be MW * 1000000') + + t.ok(result.efficiency, 'should have efficiency') + t.ok(result.efficiency.value > 0, 'efficiency value should be > 0') + + t.ok(result.miners, 'should have miners') + t.is(result.miners.online, 1850, 'miners online should match') + t.is(result.miners.error, 23, 'miners error should match') + t.is(result.miners.offline, 45, 'miners offline should match') + t.is(result.miners.total, 1930, 'miners total should match') + t.is(result.miners.containerCapacity, 2000, 'container capacity should match') + + t.ok(result.alerts, 'should have alerts') + t.is(result.alerts.critical, 8, 'critical alerts should match') + t.is(result.alerts.high, 12, 'high alerts should match') + t.is(result.alerts.medium, 39, 'medium alerts should match') + t.is(result.alerts.total, 59, 'total alerts should be sum') + + t.ok(result.pools, 'should have pools') + t.is(result.pools.totalHashrate, 279670375560265, 'pool hashrate should match') + t.is(result.pools.activeWorkers, 1823, 'active workers should match') + t.is(result.pools.totalWorkers, 1930, 'total workers should match') + + t.ok(result.ts, 'should have timestamp') + t.pass() +}) + +test('getSiteLiveStatus - handles empty ork responses', async (t) => { + const ctx = createMockCtx([], [], {}) + const req = { query: {} } + + const result = await getSiteLiveStatus(ctx, req) + + t.is(result.hashrate.value, 0, 'hashrate should be 0') + t.is(result.power.value, 0, 'power should be 0') + t.is(result.efficiency.value, 0, 'efficiency should be 0') + t.is(result.miners.total, 0, 'miners total should be 0') + t.is(result.alerts.total, 0, 'alerts total should be 0') + t.is(result.pools.totalHashrate, 0, 'pool hashrate should be 0') + t.pass() +}) + +test('getSiteLiveStatus - computes utilization correctly', async (t) => { + const tailLogMultiResponse = [ + [{ hashrate_mhs_1m_sum_aggr: 500, nominal_hashrate_mhs_sum_aggr: 1000, online_or_minor_error_miners_amount_aggr: 0, not_mining_miners_amount_aggr: 0, offline_or_sleeping_miners_amount_aggr: 0, hashrate_mhs_1m_cnt_aggr: 0, alerts_aggr: {} }], + [{ site_power_w: 750 }], + [{ container_nominal_miner_capacity_sum_aggr: 0 }] + ] + + const ctx = createMockCtx(tailLogMultiResponse, [], { nominalPowerAvailability_MW: 0.001 }) + const req = { query: {} } + + const result = await getSiteLiveStatus(ctx, req) + + t.is(result.hashrate.utilization, 50, 'hashrate utilization should be 50%') + t.is(result.power.utilization, 75, 'power utilization should be 75%') + t.pass() +}) + +test('getSiteLiveStatus - handles zero nominal values gracefully', async (t) => { + const tailLogMultiResponse = [ + [{ hashrate_mhs_1m_sum_aggr: 100 }], + [{ site_power_w: 200 }], + [{}] + ] + + const ctx = createMockCtx(tailLogMultiResponse, [], { nominalPowerAvailability_MW: 0 }) + const req = { query: {} } + + const result = await getSiteLiveStatus(ctx, req) + + t.is(result.hashrate.utilization, 0, 'should return 0 when nominal hashrate is 0') + t.is(result.power.utilization, 0, 'should return 0 when nominal power is 0') + t.pass() +}) + +test('getSiteLiveStatus - aggregates multiple pool accounts', async (t) => { + const tailLogMultiResponse = [ + [{ hashrate_mhs_1m_sum_aggr: 0 }], + [{}], + [{}] + ] + + const extDataResponse = [ + { stats: { hashrate: 100, active_workers_count: 10, worker_count: 15 } }, + { stats: { hashrate: 200, active_workers_count: 20, worker_count: 25 } } + ] + + const ctx = createMockCtx(tailLogMultiResponse, extDataResponse, {}) + const req = { query: {} } + + const result = await getSiteLiveStatus(ctx, req) + + t.is(result.pools.totalHashrate, 300, 'should sum pool hashrates') + t.is(result.pools.activeWorkers, 30, 'should sum active workers') + t.is(result.pools.totalWorkers, 40, 'should sum total workers') + t.pass() +}) + +test('getSiteLiveStatus - computes sleep miners from remainder', async (t) => { + const tailLogMultiResponse = [ + [{ + hashrate_mhs_1m_sum_aggr: 0, + online_or_minor_error_miners_amount_aggr: 80, + not_mining_miners_amount_aggr: 5, + offline_or_sleeping_miners_amount_aggr: 10, + hashrate_mhs_1m_cnt_aggr: 100 + }], + [{}], + [{}] + ] + + const ctx = createMockCtx(tailLogMultiResponse, [], {}) + const req = { query: {} } + + const result = await getSiteLiveStatus(ctx, req) + + t.is(result.miners.online, 80, 'online should match') + t.is(result.miners.error, 5, 'error should match') + t.is(result.miners.offline, 10, 'offline should match') + t.is(result.miners.sleep, 5, 'sleep should be total - online - error - offline') + t.is(result.miners.total, 100, 'total should match') + t.pass() +}) + +test('getSiteLiveStatus - uses nominal_hashrate from taillog over globalConfig', async (t) => { + const tailLogMultiResponse = [ + [{ hashrate_mhs_1m_sum_aggr: 500, nominal_hashrate_mhs_sum_aggr: 1000 }], + [{}], + [{}] + ] + + const ctx = createMockCtx(tailLogMultiResponse, [], { nominalHashrate: 2000 }) + const req = { query: {} } + + const result = await getSiteLiveStatus(ctx, req) + + t.is(result.hashrate.nominal, 1000, 'should prefer nominal from taillog aggr') + t.pass() +}) + +test('getSiteLiveStatus - falls back to globalConfig nominalHashrate', async (t) => { + const tailLogMultiResponse = [ + [{ hashrate_mhs_1m_sum_aggr: 500, nominal_hashrate_mhs_sum_aggr: 0 }], + [{}], + [{}] + ] + + const ctx = createMockCtx(tailLogMultiResponse, [], { nominalHashrate: 2000 }) + const req = { query: {} } + + const result = await getSiteLiveStatus(ctx, req) + + t.is(result.hashrate.nominal, 2000, 'should fall back to globalConfig nominalHashrate') + t.pass() +}) diff --git a/tests/unit/routes/site.routes.test.js b/tests/unit/routes/site.routes.test.js new file mode 100644 index 0000000..db53a88 --- /dev/null +++ b/tests/unit/routes/site.routes.test.js @@ -0,0 +1,55 @@ +'use strict' + +const test = require('brittle') +const { testModuleStructure, testHandlerFunctions } = require('../helpers/routeTestHelpers') +const { createRoutesForTest } = require('../helpers/mockHelpers') + +test('v2 site routes - module structure', (t) => { + testModuleStructure(t, '../../../workers/lib/server/routes/v2/site.routes.js', 'v2/site') + t.pass() +}) + +test('v2 site routes - route definitions', (t) => { + const routes = createRoutesForTest('../../../workers/lib/server/routes/v2/site.routes.js') + + const routeUrls = routes.map(route => route.url) + t.ok(routeUrls.includes('/auth/site/status/live'), 'should have site status live route') + + t.pass() +}) + +test('v2 site routes - HTTP methods', (t) => { + const routes = createRoutesForTest('../../../workers/lib/server/routes/v2/site.routes.js') + + const siteStatusRoute = routes.find(r => r.url === '/auth/site/status/live') + t.is(siteStatusRoute.method, 'GET', 'site status live route should be GET') + + t.pass() +}) + +test('v2 site routes - schema validation', (t) => { + const routes = createRoutesForTest('../../../workers/lib/server/routes/v2/site.routes.js') + + const siteStatusRoute = routes.find(r => r.url === '/auth/site/status/live') + t.ok(siteStatusRoute.schema, 'site status live route should have schema') + t.ok(siteStatusRoute.schema.querystring, 'should have querystring schema') + t.is(siteStatusRoute.schema.querystring.properties.overwriteCache.type, 'boolean', 'overwriteCache should be boolean') + + t.pass() +}) + +test('v2 site routes - handler functions', (t) => { + const routes = createRoutesForTest('../../../workers/lib/server/routes/v2/site.routes.js') + testHandlerFunctions(t, routes, 'v2/site') + t.pass() +}) + +test('v2 site routes - onRequest functions', (t) => { + const routes = createRoutesForTest('../../../workers/lib/server/routes/v2/site.routes.js') + + routes.forEach(route => { + t.ok(typeof route.onRequest === 'function', `v2/site route ${route.url} should have onRequest function`) + }) + + t.pass() +}) diff --git a/workers/lib/constants.js b/workers/lib/constants.js index f1f407e..514c80c 100644 --- a/workers/lib/constants.js +++ b/workers/lib/constants.js @@ -108,7 +108,9 @@ const ENDPOINTS = { THING_CONFIG: '/auth/thing-config', // WebSocket endpoint - WEBSOCKET: '/ws' + WEBSOCKET: '/ws', + + SITE_STATUS_LIVE: '/auth/site/status/live' } const HTTP_METHODS = { @@ -166,7 +168,10 @@ const OPERATIONS = { THING_SETTINGS_READ: 'thing.settings.read', THING_SETTINGS_WRITE: 'thing.settings.write', WORKER_CONFIG_READ: 'worker.config.read', - THING_CONFIG_READ: 'thing.config.read' + THING_CONFIG_READ: 'thing.config.read', + + // Site operations + SITE_STATUS_LIVE_READ: 'site.status.live.read' } const DEFAULTS = { diff --git a/workers/lib/server/handlers/site.handlers.js b/workers/lib/server/handlers/site.handlers.js new file mode 100644 index 0000000..eb679ae --- /dev/null +++ b/workers/lib/server/handlers/site.handlers.js @@ -0,0 +1,292 @@ +"use strict"; + +const { requestRpcMapLimit } = require("../../utils"); + +/** + * Extracts the latest entry from a tail-log key result. + * tailLogMulti returns results per key in order. + * Each ork result is an array of key results, each key result is an array of entries. + * With limit=1, each key result has at most 1 entry. + * + * @param {Array} orkResult - Single ork's tailLogMulti response + * @param {number} keyIndex - Index of the key in the keys array + * @returns {Object|null} The latest entry for that key, or null + */ +function extractKeyEntry(orkResult, keyIndex) { + if (!Array.isArray(orkResult)) return null; + const keyResult = orkResult[keyIndex]; + if (!Array.isArray(keyResult) || keyResult.length === 0) return null; + return keyResult[0] || null; +} + +/** + * Aggregates miner stats from tailLogMulti results across all orks. + * Key index 0 = miner data (stat-rtd, type: miner) + * + * @param {Array} tailLogResults - Array of ork responses from tailLogMulti + * @returns {Object} Aggregated miner stats + */ +function aggregateMinerStats(tailLogResults) { + const stats = { + hashrate: 0, + nominalHashrate: 0, + online: 0, + error: 0, + offline: 0, + total: 0, + alerts: { critical: 0, high: 0, medium: 0 }, + }; + + for (const orkResult of tailLogResults) { + const entry = extractKeyEntry(orkResult, 0); + if (!entry) continue; + + stats.hashrate += entry.hashrate_mhs_1m_sum_aggr || 0; + stats.nominalHashrate += entry.nominal_hashrate_mhs_sum_aggr || 0; + stats.online += entry.online_or_minor_error_miners_amount_aggr || 0; + stats.error += entry.not_mining_miners_amount_aggr || 0; + stats.offline += entry.offline_or_sleeping_miners_amount_aggr || 0; + stats.total += entry.hashrate_mhs_1m_cnt_aggr || 0; + + const alerts = entry.alerts_aggr; + if (alerts && typeof alerts === "object") { + stats.alerts.critical += alerts.critical || 0; + stats.alerts.high += alerts.high || 0; + stats.alerts.medium += alerts.medium || 0; + } + } + + return stats; +} + +/** + * Extracts site power from powermeter tail-log results across all orks. + * Key index 1 = powermeter data (stat-rtd, type: powermeter) + * + * @param {Array} tailLogResults - Array of ork responses from tailLogMulti + * @returns {number} Total site power in Watts + */ +function aggregatePowerStats(tailLogResults) { + let sitePower = 0; + + for (const orkResult of tailLogResults) { + const entry = extractKeyEntry(orkResult, 1); + if (!entry) continue; + sitePower += entry.site_power_w || 0; + } + + return sitePower; +} + +/** + * Extracts container capacity from container tail-log results across all orks. + * Key index 2 = container data (stat-rtd, type: container) + * + * @param {Array} tailLogResults - Array of ork responses from tailLogMulti + * @returns {number} Total container nominal miner capacity + */ +function aggregateContainerCapacity(tailLogResults) { + let capacity = 0; + + for (const orkResult of tailLogResults) { + const entry = extractKeyEntry(orkResult, 2); + if (!entry) continue; + capacity += entry.container_nominal_miner_capacity_sum_aggr || 0; + } + + return capacity; +} + +/** + * Aggregates pool stats from ext-data minerpool results across all orks. + * + * @param {Array} poolDataResults - Array of ork responses from getWrkExtData + * @returns {Object} Aggregated pool stats + */ +function aggregatePoolStats(poolDataResults) { + const stats = { + totalHashrate: 0, + activeWorkers: 0, + totalWorkers: 0, + }; + + for (const orkResult of poolDataResults) { + if (!Array.isArray(orkResult)) continue; + for (const pool of orkResult) { + if (!pool || !pool.stats) continue; + stats.totalHashrate += pool.stats.hashrate || 0; + stats.activeWorkers += pool.stats.active_workers_count || 0; + stats.totalWorkers += pool.stats.worker_count || 0; + } + } + + return stats; +} + +/** + * Extracts nominal values from global config results. + * Merges across orks (typically only 1 ork has global config). + * + * @param {Array} globalConfigResults - Array of ork responses from getGlobalConfig + * @returns {Object} Nominal configuration values + */ +function extractGlobalConfig(globalConfigResults) { + const config = { + nominalHashrate: 0, + nominalPowerAvailability_MW: 0, + }; + + for (const orkResult of globalConfigResults) { + if (!orkResult || typeof orkResult !== "object") continue; + if (orkResult.nominalHashrate) + config.nominalHashrate = orkResult.nominalHashrate; + if (orkResult.nominalPowerAvailability_MW) + config.nominalPowerAvailability_MW = + orkResult.nominalPowerAvailability_MW; + } + + return config; +} + +/** + * Computes utilization percentage safely. + * + * @param {number} value - Current value + * @param {number} nominal - Nominal/max value + * @returns {number} Utilization percentage rounded to 1 decimal, or 0 if nominal is 0 + */ +function computeUtilization(value, nominal) { + if (!nominal || nominal === 0) return 0; + return Math.round((value / nominal) * 1000) / 10; +} + +/** + * Composes the site live status response from all data sources. + * + * @param {Array} tailLogResults - tailLogMulti RPC results + * @param {Array} poolDataResults - getWrkExtData (minerpool) RPC results + * @param {Array} globalConfigResults - getGlobalConfig RPC results + * @returns {Object} Composed site status response + */ +function composeSiteStatus( + tailLogResults, + poolDataResults, + globalConfigResults, +) { + const minerStats = aggregateMinerStats(tailLogResults); + const sitePower = aggregatePowerStats(tailLogResults); + const containerCapacity = aggregateContainerCapacity(tailLogResults); + const poolStats = aggregatePoolStats(poolDataResults); + const globalConfig = extractGlobalConfig(globalConfigResults); + + const nominalPowerW = globalConfig.nominalPowerAvailability_MW * 1000000; + const hashrateNominal = + minerStats.nominalHashrate || globalConfig.nominalHashrate || 0; + + const hashrateValue = minerStats.hashrate; + const hashrateThs = hashrateValue / 1000000; + const efficiencyWPerTh = + hashrateThs > 0 ? Math.round((sitePower / hashrateThs) * 10) / 10 : 0; + + const sleep = Math.max( + 0, + minerStats.total - + minerStats.online - + minerStats.error - + minerStats.offline, + ); + const alertTotal = + minerStats.alerts.critical + + minerStats.alerts.high + + minerStats.alerts.medium; + + return { + hashrate: { + value: hashrateValue, + nominal: hashrateNominal, + utilization: computeUtilization(hashrateValue, hashrateNominal), + }, + power: { + value: sitePower, + nominal: nominalPowerW, + utilization: computeUtilization(sitePower, nominalPowerW), + }, + efficiency: { + value: efficiencyWPerTh, + }, + miners: { + online: minerStats.online, + offline: minerStats.offline, + error: minerStats.error, + sleep, + total: minerStats.total, + containerCapacity, + }, + alerts: { + critical: minerStats.alerts.critical, + high: minerStats.alerts.high, + medium: minerStats.alerts.medium, + total: alertTotal, + }, + pools: poolStats, + ts: Date.now(), + }; +} + +/** + * GET /auth/site/status/live + * + * Returns a composite site status snapshot by aggregating: + * - tailLogMulti (miner hashrate/counts/alerts, powermeter power, container capacity) + * - getWrkExtData (pool hashrate, worker counts) + * - getGlobalConfig (nominal hashrate, nominal power availability) + * + * Replaces 5 separate frontend API calls with a single server-side composition. + */ +async function getSiteLiveStatus(ctx, req) { + const tailLogPayload = { + keys: [ + { key: "stat-rtd", type: "miner", tag: "t-miner" }, + { key: "stat-rtd", type: "powermeter", tag: "t-powermeter" }, + { key: "stat-rtd", type: "container", tag: "t-container" }, + ], + limit: 1, + aggrFields: { + hashrate_mhs_1m_sum_aggr: 1, + nominal_hashrate_mhs_sum_aggr: 1, + alerts_aggr: 1, + online_or_minor_error_miners_amount_aggr: 1, + not_mining_miners_amount_aggr: 1, + offline_or_sleeping_miners_amount_aggr: 1, + hashrate_mhs_1m_cnt_aggr: 1, + site_power_w: 1, + container_nominal_miner_capacity_sum_aggr: 1, + }, + }; + + const poolPayload = { + type: "minerpool", + query: { key: "stats" }, + }; + + const globalConfigPayload = { + fields: { nominalHashrate: 1, nominalPowerAvailability_MW: 1 }, + }; + + const [tailLogResults, poolDataResults, globalConfigResults] = + await Promise.all([ + requestRpcMapLimit(ctx, "tailLogMulti", tailLogPayload), + requestRpcMapLimit(ctx, "getWrkExtData", poolPayload), + requestRpcMapLimit(ctx, "getGlobalConfig", globalConfigPayload), + ]); + + return composeSiteStatus( + tailLogResults, + poolDataResults, + globalConfigResults, + ); +} + +module.exports = { + getSiteLiveStatus, +}; diff --git a/workers/lib/server/index.js b/workers/lib/server/index.js index ac884da..d83371a 100644 --- a/workers/lib/server/index.js +++ b/workers/lib/server/index.js @@ -1,19 +1,20 @@ -'use strict' +"use strict"; -const authRoutes = require('./routes/auth.routes') -const usersRoutes = require('./routes/users.routes') -const actionsRoutes = require('./routes/actions.routes') -const logsRoutes = require('./routes/logs.routes') -const globalRoutes = require('./routes/global.routes') -const thingsRoutes = require('./routes/things.routes') -const settingsRoutes = require('./routes/settings.routes') -const wsRoutes = require('./routes/ws.routes') +const authRoutes = require("./routes/auth.routes"); +const usersRoutes = require("./routes/users.routes"); +const actionsRoutes = require("./routes/actions.routes"); +const logsRoutes = require("./routes/logs.routes"); +const globalRoutes = require("./routes/global.routes"); +const thingsRoutes = require("./routes/things.routes"); +const settingsRoutes = require("./routes/settings.routes"); +const wsRoutes = require("./routes/ws.routes"); +const siteRoutes = require("./routes/site.routes"); /** * Collect all routes into a flat array for server injection. * Each route is a Fastify-style object: { method, url, handler, ... } */ -function routes (ctx) { +function routes(ctx) { return [ ...authRoutes(ctx), ...actionsRoutes(ctx), @@ -22,10 +23,11 @@ function routes (ctx) { ...thingsRoutes(ctx), ...usersRoutes(ctx), ...settingsRoutes(ctx), - ...wsRoutes(ctx) - ] + ...wsRoutes(ctx), + ...siteRoutes(ctx), + ]; } module.exports = { - routes -} + routes, +}; diff --git a/workers/lib/server/routes/site.routes.js b/workers/lib/server/routes/site.routes.js new file mode 100644 index 0000000..bdcde00 --- /dev/null +++ b/workers/lib/server/routes/site.routes.js @@ -0,0 +1,26 @@ +"use strict"; + +const { ENDPOINTS, HTTP_METHODS } = require("../../constants"); +const { getSiteLiveStatus } = require("../handlers/site.handlers"); +const { createCachedAuthRoute } = require("../lib/routeHelpers"); + +module.exports = (ctx) => [ + { + method: HTTP_METHODS.GET, + url: ENDPOINTS.SITE_STATUS_LIVE, + schema: { + querystring: { + type: "object", + properties: { + overwriteCache: { type: "boolean" }, + }, + }, + }, + ...createCachedAuthRoute( + ctx, + ["site-status-live"], + ENDPOINTS.SITE_STATUS_LIVE, + getSiteLiveStatus, + ), + }, +]; From 8a4fb3184a9f7e2297a9b0cb95aa36b24a195ca5 Mon Sep 17 00:00:00 2001 From: Parag More Date: Thu, 12 Feb 2026 12:50:00 +0530 Subject: [PATCH 2/2] Fix tests and lint --- tests/unit/routes/site.routes.test.js | 28 +-- workers/lib/server/handlers/site.handlers.js | 198 +++++++++---------- workers/lib/server/index.js | 30 +-- workers/lib/server/routes/site.routes.js | 26 +-- 4 files changed, 141 insertions(+), 141 deletions(-) diff --git a/tests/unit/routes/site.routes.test.js b/tests/unit/routes/site.routes.test.js index db53a88..9c15794 100644 --- a/tests/unit/routes/site.routes.test.js +++ b/tests/unit/routes/site.routes.test.js @@ -4,13 +4,13 @@ const test = require('brittle') const { testModuleStructure, testHandlerFunctions } = require('../helpers/routeTestHelpers') const { createRoutesForTest } = require('../helpers/mockHelpers') -test('v2 site routes - module structure', (t) => { - testModuleStructure(t, '../../../workers/lib/server/routes/v2/site.routes.js', 'v2/site') +test('site routes - module structure', (t) => { + testModuleStructure(t, '../../../workers/lib/server/routes/site.routes.js', '/site') t.pass() }) -test('v2 site routes - route definitions', (t) => { - const routes = createRoutesForTest('../../../workers/lib/server/routes/v2/site.routes.js') +test('site routes - route definitions', (t) => { + const routes = createRoutesForTest('../../../workers/lib/server/routes/site.routes.js') const routeUrls = routes.map(route => route.url) t.ok(routeUrls.includes('/auth/site/status/live'), 'should have site status live route') @@ -18,8 +18,8 @@ test('v2 site routes - route definitions', (t) => { t.pass() }) -test('v2 site routes - HTTP methods', (t) => { - const routes = createRoutesForTest('../../../workers/lib/server/routes/v2/site.routes.js') +test('site routes - HTTP methods', (t) => { + const routes = createRoutesForTest('../../../workers/lib/server/routes/site.routes.js') const siteStatusRoute = routes.find(r => r.url === '/auth/site/status/live') t.is(siteStatusRoute.method, 'GET', 'site status live route should be GET') @@ -27,8 +27,8 @@ test('v2 site routes - HTTP methods', (t) => { t.pass() }) -test('v2 site routes - schema validation', (t) => { - const routes = createRoutesForTest('../../../workers/lib/server/routes/v2/site.routes.js') +test('site routes - schema validation', (t) => { + const routes = createRoutesForTest('../../../workers/lib/server/routes/site.routes.js') const siteStatusRoute = routes.find(r => r.url === '/auth/site/status/live') t.ok(siteStatusRoute.schema, 'site status live route should have schema') @@ -38,17 +38,17 @@ test('v2 site routes - schema validation', (t) => { t.pass() }) -test('v2 site routes - handler functions', (t) => { - const routes = createRoutesForTest('../../../workers/lib/server/routes/v2/site.routes.js') - testHandlerFunctions(t, routes, 'v2/site') +test('site routes - handler functions', (t) => { + const routes = createRoutesForTest('../../../workers/lib/server/routes/site.routes.js') + testHandlerFunctions(t, routes, '/site') t.pass() }) -test('v2 site routes - onRequest functions', (t) => { - const routes = createRoutesForTest('../../../workers/lib/server/routes/v2/site.routes.js') +test('site routes - onRequest functions', (t) => { + const routes = createRoutesForTest('../../../workers/lib/server/routes/site.routes.js') routes.forEach(route => { - t.ok(typeof route.onRequest === 'function', `v2/site route ${route.url} should have onRequest function`) + t.ok(typeof route.onRequest === 'function', `/site route ${route.url} should have onRequest function`) }) t.pass() diff --git a/workers/lib/server/handlers/site.handlers.js b/workers/lib/server/handlers/site.handlers.js index eb679ae..18066b5 100644 --- a/workers/lib/server/handlers/site.handlers.js +++ b/workers/lib/server/handlers/site.handlers.js @@ -1,6 +1,6 @@ -"use strict"; +'use strict' -const { requestRpcMapLimit } = require("../../utils"); +const { requestRpcMapLimit } = require('../../utils') /** * Extracts the latest entry from a tail-log key result. @@ -12,11 +12,11 @@ const { requestRpcMapLimit } = require("../../utils"); * @param {number} keyIndex - Index of the key in the keys array * @returns {Object|null} The latest entry for that key, or null */ -function extractKeyEntry(orkResult, keyIndex) { - if (!Array.isArray(orkResult)) return null; - const keyResult = orkResult[keyIndex]; - if (!Array.isArray(keyResult) || keyResult.length === 0) return null; - return keyResult[0] || null; +function extractKeyEntry (orkResult, keyIndex) { + if (!Array.isArray(orkResult)) return null + const keyResult = orkResult[keyIndex] + if (!Array.isArray(keyResult) || keyResult.length === 0) return null + return keyResult[0] || null } /** @@ -26,7 +26,7 @@ function extractKeyEntry(orkResult, keyIndex) { * @param {Array} tailLogResults - Array of ork responses from tailLogMulti * @returns {Object} Aggregated miner stats */ -function aggregateMinerStats(tailLogResults) { +function aggregateMinerStats (tailLogResults) { const stats = { hashrate: 0, nominalHashrate: 0, @@ -34,29 +34,29 @@ function aggregateMinerStats(tailLogResults) { error: 0, offline: 0, total: 0, - alerts: { critical: 0, high: 0, medium: 0 }, - }; + alerts: { critical: 0, high: 0, medium: 0 } + } for (const orkResult of tailLogResults) { - const entry = extractKeyEntry(orkResult, 0); - if (!entry) continue; + const entry = extractKeyEntry(orkResult, 0) + if (!entry) continue - stats.hashrate += entry.hashrate_mhs_1m_sum_aggr || 0; - stats.nominalHashrate += entry.nominal_hashrate_mhs_sum_aggr || 0; - stats.online += entry.online_or_minor_error_miners_amount_aggr || 0; - stats.error += entry.not_mining_miners_amount_aggr || 0; - stats.offline += entry.offline_or_sleeping_miners_amount_aggr || 0; - stats.total += entry.hashrate_mhs_1m_cnt_aggr || 0; + stats.hashrate += entry.hashrate_mhs_1m_sum_aggr || 0 + stats.nominalHashrate += entry.nominal_hashrate_mhs_sum_aggr || 0 + stats.online += entry.online_or_minor_error_miners_amount_aggr || 0 + stats.error += entry.not_mining_miners_amount_aggr || 0 + stats.offline += entry.offline_or_sleeping_miners_amount_aggr || 0 + stats.total += entry.hashrate_mhs_1m_cnt_aggr || 0 - const alerts = entry.alerts_aggr; - if (alerts && typeof alerts === "object") { - stats.alerts.critical += alerts.critical || 0; - stats.alerts.high += alerts.high || 0; - stats.alerts.medium += alerts.medium || 0; + const alerts = entry.alerts_aggr + if (alerts && typeof alerts === 'object') { + stats.alerts.critical += alerts.critical || 0 + stats.alerts.high += alerts.high || 0 + stats.alerts.medium += alerts.medium || 0 } } - return stats; + return stats } /** @@ -66,16 +66,16 @@ function aggregateMinerStats(tailLogResults) { * @param {Array} tailLogResults - Array of ork responses from tailLogMulti * @returns {number} Total site power in Watts */ -function aggregatePowerStats(tailLogResults) { - let sitePower = 0; +function aggregatePowerStats (tailLogResults) { + let sitePower = 0 for (const orkResult of tailLogResults) { - const entry = extractKeyEntry(orkResult, 1); - if (!entry) continue; - sitePower += entry.site_power_w || 0; + const entry = extractKeyEntry(orkResult, 1) + if (!entry) continue + sitePower += entry.site_power_w || 0 } - return sitePower; + return sitePower } /** @@ -85,16 +85,16 @@ function aggregatePowerStats(tailLogResults) { * @param {Array} tailLogResults - Array of ork responses from tailLogMulti * @returns {number} Total container nominal miner capacity */ -function aggregateContainerCapacity(tailLogResults) { - let capacity = 0; +function aggregateContainerCapacity (tailLogResults) { + let capacity = 0 for (const orkResult of tailLogResults) { - const entry = extractKeyEntry(orkResult, 2); - if (!entry) continue; - capacity += entry.container_nominal_miner_capacity_sum_aggr || 0; + const entry = extractKeyEntry(orkResult, 2) + if (!entry) continue + capacity += entry.container_nominal_miner_capacity_sum_aggr || 0 } - return capacity; + return capacity } /** @@ -103,24 +103,24 @@ function aggregateContainerCapacity(tailLogResults) { * @param {Array} poolDataResults - Array of ork responses from getWrkExtData * @returns {Object} Aggregated pool stats */ -function aggregatePoolStats(poolDataResults) { +function aggregatePoolStats (poolDataResults) { const stats = { totalHashrate: 0, activeWorkers: 0, - totalWorkers: 0, - }; + totalWorkers: 0 + } for (const orkResult of poolDataResults) { - if (!Array.isArray(orkResult)) continue; + if (!Array.isArray(orkResult)) continue for (const pool of orkResult) { - if (!pool || !pool.stats) continue; - stats.totalHashrate += pool.stats.hashrate || 0; - stats.activeWorkers += pool.stats.active_workers_count || 0; - stats.totalWorkers += pool.stats.worker_count || 0; + if (!pool || !pool.stats) continue + stats.totalHashrate += pool.stats.hashrate || 0 + stats.activeWorkers += pool.stats.active_workers_count || 0 + stats.totalWorkers += pool.stats.worker_count || 0 } } - return stats; + return stats } /** @@ -130,22 +130,22 @@ function aggregatePoolStats(poolDataResults) { * @param {Array} globalConfigResults - Array of ork responses from getGlobalConfig * @returns {Object} Nominal configuration values */ -function extractGlobalConfig(globalConfigResults) { +function extractGlobalConfig (globalConfigResults) { const config = { nominalHashrate: 0, - nominalPowerAvailability_MW: 0, - }; + nominalPowerAvailability_MW: 0 + } for (const orkResult of globalConfigResults) { - if (!orkResult || typeof orkResult !== "object") continue; - if (orkResult.nominalHashrate) - config.nominalHashrate = orkResult.nominalHashrate; - if (orkResult.nominalPowerAvailability_MW) + if (!orkResult || typeof orkResult !== 'object') continue + if (orkResult.nominalHashrate) { config.nominalHashrate = orkResult.nominalHashrate } + if (orkResult.nominalPowerAvailability_MW) { config.nominalPowerAvailability_MW = - orkResult.nominalPowerAvailability_MW; + orkResult.nominalPowerAvailability_MW + } } - return config; + return config } /** @@ -155,9 +155,9 @@ function extractGlobalConfig(globalConfigResults) { * @param {number} nominal - Nominal/max value * @returns {number} Utilization percentage rounded to 1 decimal, or 0 if nominal is 0 */ -function computeUtilization(value, nominal) { - if (!nominal || nominal === 0) return 0; - return Math.round((value / nominal) * 1000) / 10; +function computeUtilization (value, nominal) { + if (!nominal || nominal === 0) return 0 + return Math.round((value / nominal) * 1000) / 10 } /** @@ -168,51 +168,51 @@ function computeUtilization(value, nominal) { * @param {Array} globalConfigResults - getGlobalConfig RPC results * @returns {Object} Composed site status response */ -function composeSiteStatus( +function composeSiteStatus ( tailLogResults, poolDataResults, - globalConfigResults, + globalConfigResults ) { - const minerStats = aggregateMinerStats(tailLogResults); - const sitePower = aggregatePowerStats(tailLogResults); - const containerCapacity = aggregateContainerCapacity(tailLogResults); - const poolStats = aggregatePoolStats(poolDataResults); - const globalConfig = extractGlobalConfig(globalConfigResults); + const minerStats = aggregateMinerStats(tailLogResults) + const sitePower = aggregatePowerStats(tailLogResults) + const containerCapacity = aggregateContainerCapacity(tailLogResults) + const poolStats = aggregatePoolStats(poolDataResults) + const globalConfig = extractGlobalConfig(globalConfigResults) - const nominalPowerW = globalConfig.nominalPowerAvailability_MW * 1000000; + const nominalPowerW = globalConfig.nominalPowerAvailability_MW * 1000000 const hashrateNominal = - minerStats.nominalHashrate || globalConfig.nominalHashrate || 0; + minerStats.nominalHashrate || globalConfig.nominalHashrate || 0 - const hashrateValue = minerStats.hashrate; - const hashrateThs = hashrateValue / 1000000; + const hashrateValue = minerStats.hashrate + const hashrateThs = hashrateValue / 1000000 const efficiencyWPerTh = - hashrateThs > 0 ? Math.round((sitePower / hashrateThs) * 10) / 10 : 0; + hashrateThs > 0 ? Math.round((sitePower / hashrateThs) * 10) / 10 : 0 const sleep = Math.max( 0, minerStats.total - minerStats.online - minerStats.error - - minerStats.offline, - ); + minerStats.offline + ) const alertTotal = minerStats.alerts.critical + minerStats.alerts.high + - minerStats.alerts.medium; + minerStats.alerts.medium return { hashrate: { value: hashrateValue, nominal: hashrateNominal, - utilization: computeUtilization(hashrateValue, hashrateNominal), + utilization: computeUtilization(hashrateValue, hashrateNominal) }, power: { value: sitePower, nominal: nominalPowerW, - utilization: computeUtilization(sitePower, nominalPowerW), + utilization: computeUtilization(sitePower, nominalPowerW) }, efficiency: { - value: efficiencyWPerTh, + value: efficiencyWPerTh }, miners: { online: minerStats.online, @@ -220,17 +220,17 @@ function composeSiteStatus( error: minerStats.error, sleep, total: minerStats.total, - containerCapacity, + containerCapacity }, alerts: { critical: minerStats.alerts.critical, high: minerStats.alerts.high, medium: minerStats.alerts.medium, - total: alertTotal, + total: alertTotal }, pools: poolStats, - ts: Date.now(), - }; + ts: Date.now() + } } /** @@ -243,12 +243,12 @@ function composeSiteStatus( * * Replaces 5 separate frontend API calls with a single server-side composition. */ -async function getSiteLiveStatus(ctx, req) { +async function getSiteLiveStatus (ctx, req) { const tailLogPayload = { keys: [ - { key: "stat-rtd", type: "miner", tag: "t-miner" }, - { key: "stat-rtd", type: "powermeter", tag: "t-powermeter" }, - { key: "stat-rtd", type: "container", tag: "t-container" }, + { key: 'stat-rtd', type: 'miner', tag: 't-miner' }, + { key: 'stat-rtd', type: 'powermeter', tag: 't-powermeter' }, + { key: 'stat-rtd', type: 'container', tag: 't-container' } ], limit: 1, aggrFields: { @@ -260,33 +260,33 @@ async function getSiteLiveStatus(ctx, req) { offline_or_sleeping_miners_amount_aggr: 1, hashrate_mhs_1m_cnt_aggr: 1, site_power_w: 1, - container_nominal_miner_capacity_sum_aggr: 1, - }, - }; + container_nominal_miner_capacity_sum_aggr: 1 + } + } const poolPayload = { - type: "minerpool", - query: { key: "stats" }, - }; + type: 'minerpool', + query: { key: 'stats' } + } const globalConfigPayload = { - fields: { nominalHashrate: 1, nominalPowerAvailability_MW: 1 }, - }; + fields: { nominalHashrate: 1, nominalPowerAvailability_MW: 1 } + } const [tailLogResults, poolDataResults, globalConfigResults] = await Promise.all([ - requestRpcMapLimit(ctx, "tailLogMulti", tailLogPayload), - requestRpcMapLimit(ctx, "getWrkExtData", poolPayload), - requestRpcMapLimit(ctx, "getGlobalConfig", globalConfigPayload), - ]); + requestRpcMapLimit(ctx, 'tailLogMulti', tailLogPayload), + requestRpcMapLimit(ctx, 'getWrkExtData', poolPayload), + requestRpcMapLimit(ctx, 'getGlobalConfig', globalConfigPayload) + ]) return composeSiteStatus( tailLogResults, poolDataResults, - globalConfigResults, - ); + globalConfigResults + ) } module.exports = { - getSiteLiveStatus, -}; + getSiteLiveStatus +} diff --git a/workers/lib/server/index.js b/workers/lib/server/index.js index d83371a..112b509 100644 --- a/workers/lib/server/index.js +++ b/workers/lib/server/index.js @@ -1,20 +1,20 @@ -"use strict"; +'use strict' -const authRoutes = require("./routes/auth.routes"); -const usersRoutes = require("./routes/users.routes"); -const actionsRoutes = require("./routes/actions.routes"); -const logsRoutes = require("./routes/logs.routes"); -const globalRoutes = require("./routes/global.routes"); -const thingsRoutes = require("./routes/things.routes"); -const settingsRoutes = require("./routes/settings.routes"); -const wsRoutes = require("./routes/ws.routes"); -const siteRoutes = require("./routes/site.routes"); +const authRoutes = require('./routes/auth.routes') +const usersRoutes = require('./routes/users.routes') +const actionsRoutes = require('./routes/actions.routes') +const logsRoutes = require('./routes/logs.routes') +const globalRoutes = require('./routes/global.routes') +const thingsRoutes = require('./routes/things.routes') +const settingsRoutes = require('./routes/settings.routes') +const wsRoutes = require('./routes/ws.routes') +const siteRoutes = require('./routes/site.routes') /** * Collect all routes into a flat array for server injection. * Each route is a Fastify-style object: { method, url, handler, ... } */ -function routes(ctx) { +function routes (ctx) { return [ ...authRoutes(ctx), ...actionsRoutes(ctx), @@ -24,10 +24,10 @@ function routes(ctx) { ...usersRoutes(ctx), ...settingsRoutes(ctx), ...wsRoutes(ctx), - ...siteRoutes(ctx), - ]; + ...siteRoutes(ctx) + ] } module.exports = { - routes, -}; + routes +} diff --git a/workers/lib/server/routes/site.routes.js b/workers/lib/server/routes/site.routes.js index bdcde00..461b894 100644 --- a/workers/lib/server/routes/site.routes.js +++ b/workers/lib/server/routes/site.routes.js @@ -1,8 +1,8 @@ -"use strict"; +'use strict' -const { ENDPOINTS, HTTP_METHODS } = require("../../constants"); -const { getSiteLiveStatus } = require("../handlers/site.handlers"); -const { createCachedAuthRoute } = require("../lib/routeHelpers"); +const { ENDPOINTS, HTTP_METHODS } = require('../../constants') +const { getSiteLiveStatus } = require('../handlers/site.handlers') +const { createCachedAuthRoute } = require('../lib/routeHelpers') module.exports = (ctx) => [ { @@ -10,17 +10,17 @@ module.exports = (ctx) => [ url: ENDPOINTS.SITE_STATUS_LIVE, schema: { querystring: { - type: "object", + type: 'object', properties: { - overwriteCache: { type: "boolean" }, - }, - }, + overwriteCache: { type: 'boolean' } + } + } }, ...createCachedAuthRoute( ctx, - ["site-status-live"], + ['site-status-live'], ENDPOINTS.SITE_STATUS_LIVE, - getSiteLiveStatus, - ), - }, -]; + getSiteLiveStatus + ) + } +]