diff --git a/tests/unit/handlers/groups.handlers.test.js b/tests/unit/handlers/groups.handlers.test.js index 42c0920..1cea483 100644 --- a/tests/unit/handlers/groups.handlers.test.js +++ b/tests/unit/handlers/groups.handlers.test.js @@ -3,282 +3,192 @@ const test = require('brittle') const { getGroupStats, - composeGroupStats, - sumGroupedField + mapRackIdToKeys, + formatRackValues } = require('../../../workers/lib/server/handlers/groups.handlers') -const { extractKeyEntry } = require('../../../workers/lib/metrics.utils') -const { withDataProxy } = require('../helpers/mockHelpers') -// ==================== extractKeyEntry Tests ==================== - -test('extractKeyEntry - returns entry at index', (t) => { - const orkResult = [[{ hashrate: 100 }], [{ power: 200 }]] - const entry = extractKeyEntry(orkResult, 0) - t.alike(entry, { hashrate: 100 }, 'should return first key entry') - t.pass() -}) - -test('extractKeyEntry - returns null for non-array', (t) => { - t.is(extractKeyEntry(null, 0), null, 'null input returns null') - t.is(extractKeyEntry({}, 0), null, 'object input returns null') - t.pass() -}) - -test('extractKeyEntry - returns null for empty key result', (t) => { - t.is(extractKeyEntry([[]], 0), null, 'empty array returns null') - t.is(extractKeyEntry([], 0), null, 'missing index returns null') - t.pass() -}) +function createMockTailLogEntry () { + return { + hashrate_mhs_5m_pdu_rack_group_avg_aggr: { + 'group-1_1-1': 5000000000, + 'group-1_2-1': 4000000000, + 'group-2_1-1': 3000000000, + 'group-2_2-1': 6000000000 + }, + power_w_pdu_rack_group_sum_aggr: { + 'group-1_1-1': 500000, + 'group-1_2-1': 400000, + 'group-2_1-1': 300000, + 'group-2_2-1': 600000 + }, + efficiency_w_ths_pdu_rack_group_avg_aggr: { + 'group-1_1-1': 6, + 'group-1_2-1': 6, + 'group-2_1-1': 6, + 'group-2_2-1': 6 + } + } +} + +function createMockDcsThing () { + return { + id: 'dcs-1', + type: 'wrk-dcs-siemens', + tags: ['t-dcs'], + last: { + snap: { + config: { + mining: { total_groups: 2, racks_per_group: 2, miners_per_rack: 20 } + } + } + } + } +} -// ==================== sumGroupedField Tests ==================== +function createMockCtx ({ dcsEnabled = true, tailLogEntry = createMockTailLogEntry(), dcsThing = createMockDcsThing() } = {}) { + return { + conf: { + featureConfig: dcsEnabled ? { centralDCSSetup: { enabled: true, tag: 't-dcs' } } : {} + }, + dataProxy: { + requestDataMap: async (method) => { + if (method === 'tailLogMulti') return [[[tailLogEntry]]] + if (method === 'listThings') return dcsEnabled ? [[dcsThing]] : [[]] + return [] + } + } + } +} -test('sumGroupedField - sums values for matching containers', (t) => { - const grouped = { 'C-01': 100, 'C-02': 200, 'C-03': 300 } - t.is(sumGroupedField(grouped, ['C-01', 'C-03']), 400, 'should sum matching containers') - t.pass() -}) +// ==================== mapRackIdToKeys ==================== -test('sumGroupedField - returns 0 for non-matching containers', (t) => { - const grouped = { 'C-01': 100 } - t.is(sumGroupedField(grouped, ['C-99']), 0, 'should return 0 for missing containers') - t.pass() -}) +test('mapRackIdToKeys - maps synth rack-N ids to Nth real key per group', (t) => { + const racks = [ + { id: 'group-1_rack-1', group: { id: 'group-1' } }, + { id: 'group-1_rack-2', group: { id: 'group-1' } }, + { id: 'group-2_rack-1', group: { id: 'group-2' } } + ] + const stats = { + hashrateByRack: { 'group-1_1-1': 1, 'group-1_2-1': 1, 'group-2_1-1': 1 }, + powerByRack: {}, + efficiencyByRack: {} + } -test('sumGroupedField - handles null/undefined input', (t) => { - t.is(sumGroupedField(null, ['C-01']), 0, 'null returns 0') - t.is(sumGroupedField(undefined, ['C-01']), 0, 'undefined returns 0') + const map = mapRackIdToKeys(racks, stats) + t.is(map.get('group-1_rack-1'), 'group-1_1-1') + t.is(map.get('group-1_rack-2'), 'group-1_2-1') + t.is(map.get('group-2_rack-1'), 'group-2_1-1') t.pass() }) -// ==================== composeGroupStats Tests ==================== - -test('composeGroupStats - aggregates container-grouped data across orks', (t) => { - const results = [ - [ - [{ - hashrate_mhs_1m_container_group_sum_aggr: { 'C-01': 50000, 'C-02': 30000 }, - power_w_container_group_sum_aggr: { 'C-01': 5000, 'C-02': 3000 }, - power_mode_low_cnt: { 'C-01': 2, 'C-02': 1 }, - power_mode_normal_cnt: { 'C-01': 5, 'C-02': 4 }, - power_mode_high_cnt: { 'C-01': 3, 'C-02': 3 }, - offline_cnt: { 'C-01': 1, 'C-02': 0 }, - error_cnt: { 'C-01': 0, 'C-02': 1 }, - not_mining_cnt: { 'C-01': 0, 'C-02': 0 }, - power_mode_sleep_cnt: { 'C-01': 1, 'C-02': 0 } - }] - ] +test('mapRackIdToKeys - undefined when no real key at that position', (t) => { + const racks = [ + { id: 'group-1_rack-1', group: { id: 'group-1' } }, + { id: 'group-1_rack-2', group: { id: 'group-1' } } ] + const stats = { + hashrateByRack: { 'group-1_1-1': 1 }, + powerByRack: {}, + efficiencyByRack: {} + } - const stats = composeGroupStats(results, ['C-01', 'C-02']) - t.is(stats.hashrateMhs, 80000, 'should sum hashrate for both containers') - t.is(stats.powerW, 8000, 'should sum power for both containers') - t.is(stats.onlineCount, 18, 'should sum online miners (low+normal+high)') - t.is(stats.minerCount, 21, 'should sum all miners across all statuses') - t.ok(typeof stats.efficiency === 'number', 'should have efficiency') + const map = mapRackIdToKeys(racks, stats) + t.is(map.get('group-1_rack-1'), 'group-1_1-1') + t.is(map.get('group-1_rack-2'), undefined) t.pass() }) -test('composeGroupStats - filters to requested containers only', (t) => { - const results = [ - [ - [{ - hashrate_mhs_1m_container_group_sum_aggr: { 'C-01': 50000, 'C-02': 30000, 'C-03': 20000 }, - power_w_container_group_sum_aggr: { 'C-01': 5000, 'C-02': 3000, 'C-03': 2000 }, - power_mode_normal_cnt: { 'C-01': 10, 'C-02': 8, 'C-03': 6 }, - power_mode_low_cnt: {}, - power_mode_high_cnt: {}, - offline_cnt: {}, - error_cnt: {}, - not_mining_cnt: {}, - power_mode_sleep_cnt: {} - }] - ] - ] +// ==================== formatRackValues ==================== - const stats = composeGroupStats(results, ['C-01']) - t.is(stats.hashrateMhs, 50000, 'should only include C-01 hashrate') - t.is(stats.powerW, 5000, 'should only include C-01 power') - t.is(stats.onlineCount, 10, 'should only include C-01 miners') +test('formatRackValues - leaves rack unchanged when no real key', (t) => { + const rack = { id: 'x', hashrate: { value: 0, unit: 'PH/s' } } + const out = formatRackValues(rack, undefined, { hashrateByRack: {}, powerByRack: {}, efficiencyByRack: {} }) + t.alike(out, rack) t.pass() }) -test('composeGroupStats - empty results', (t) => { - const stats = composeGroupStats([], ['C-01']) - t.is(stats.hashrateMhs, 0, 'hashrate should be 0') - t.is(stats.powerW, 0, 'power should be 0') - t.is(stats.minerCount, 0, 'miner count should be 0') - t.is(stats.onlineCount, 0, 'online count should be 0') - t.is(stats.efficiency, 0, 'efficiency should be 0 when no hashrate') +test('formatRackValues - overrides stats with values from the real key', (t) => { + const rack = { id: 'group-1_rack-1', hashrate: { value: 0, unit: 'PH/s' } } + const stats = { + hashrateByRack: { 'group-1_1-1': 5000000000 }, + powerByRack: { 'group-1_1-1': 500000 }, + efficiencyByRack: { 'group-1_1-1': 6 } + } + + const out = formatRackValues(rack, 'group-1_1-1', stats) + t.is(out.id, 'group-1_rack-1', 'public id is preserved') + t.ok(out.hashrate.value > 0) + t.is(out.consumption.value, 500, '500000 W → 500 kW') + t.ok(out.efficiency.value > 0) t.pass() }) -test('composeGroupStats - zero hashrate gives zero efficiency', (t) => { - const results = [ - [ - [{ - hashrate_mhs_1m_container_group_sum_aggr: { 'C-01': 0 }, - power_w_container_group_sum_aggr: { 'C-01': 5000 }, - power_mode_low_cnt: {}, - power_mode_normal_cnt: {}, - power_mode_high_cnt: {}, - offline_cnt: { 'C-01': 2 }, - error_cnt: {}, - not_mining_cnt: {}, - power_mode_sleep_cnt: {} - }] - ] - ] +// ==================== getGroupStats ==================== - const stats = composeGroupStats(results, ['C-01']) - t.is(stats.efficiency, 0, 'efficiency should be 0 with zero hashrate') - t.pass() -}) +test('getGroupStats - returns per-rack data with real values mapped to synth ids', async (t) => { + const ctx = createMockCtx() + const result = await getGroupStats(ctx, { query: { racks: 'group-1_rack-1,group-1_rack-2' } }) -test('composeGroupStats - handles missing fields gracefully', (t) => { - const results = [ - [ - [{}] - ] - ] + t.is(result.totalCount, 2) + const [first, second] = result.data + t.is(first.id, 'group-1_rack-1') + t.alike(first.group, { id: 'group-1', name: 'Group 1' }) + t.is(first.miners_count, 20) + t.ok(first.hashrate.value > 0, 'mapped to group-1_1-1 real values') + t.ok(first.consumption.value > 0) + t.ok(first.efficiency.value > 0) - const stats = composeGroupStats(results, ['C-01']) - t.is(stats.hashrateMhs, 0, 'missing fields default to 0') - t.is(stats.powerW, 0, 'missing power defaults to 0') + t.is(second.id, 'group-1_rack-2') + t.ok(second.hashrate.value > 0, 'mapped to group-1_2-1 real values') t.pass() }) -test('composeGroupStats - multi-ork aggregation', (t) => { - const results = [ - [ - [{ - hashrate_mhs_1m_container_group_sum_aggr: { 'C-01': 40000 }, - power_w_container_group_sum_aggr: { 'C-01': 4000 }, - power_mode_normal_cnt: { 'C-01': 8 }, - power_mode_low_cnt: {}, - power_mode_high_cnt: {}, - offline_cnt: { 'C-01': 1 }, - error_cnt: {}, - not_mining_cnt: {}, - power_mode_sleep_cnt: {} - }] - ], - [ - [{ - hashrate_mhs_1m_container_group_sum_aggr: { 'C-01': 20000 }, - power_w_container_group_sum_aggr: { 'C-01': 2000 }, - power_mode_normal_cnt: { 'C-01': 4 }, - power_mode_low_cnt: {}, - power_mode_high_cnt: {}, - offline_cnt: {}, - error_cnt: {}, - not_mining_cnt: {}, - power_mode_sleep_cnt: {} - }] - ] - ] +test('getGroupStats - cross-group selection', async (t) => { + const ctx = createMockCtx() + const result = await getGroupStats(ctx, { query: { racks: 'group-1_rack-1,group-2_rack-2' } }) - const stats = composeGroupStats(results, ['C-01']) - t.is(stats.hashrateMhs, 60000, 'should sum hashrate across orks') - t.is(stats.powerW, 6000, 'should sum power across orks') - t.is(stats.onlineCount, 12, 'should sum online across orks') - t.is(stats.minerCount, 13, 'should sum all miners across orks') + t.is(result.totalCount, 2) + const ids = result.data.map(r => r.id) + t.alike(ids, ['group-1_rack-1', 'group-2_rack-2']) + result.data.forEach(rack => t.ok(rack.hashrate.value > 0)) t.pass() }) -// ==================== getGroupStats Tests ==================== +test('getGroupStats - unknown rack ids are dropped', async (t) => { + const ctx = createMockCtx() + const result = await getGroupStats(ctx, { query: { racks: 'group-1_rack-1,group-99_rack-99' } }) -test('getGroupStats - happy path', async (t) => { - const mockCtx = withDataProxy({ - conf: { - orks: [{ rpcPublicKey: 'key1' }] - }, - net_r0: { - jRequest: async () => { - return [ - [{ - hashrate_mhs_1m_container_group_sum_aggr: { 'C-01': 60000, 'C-02': 40000 }, - power_w_container_group_sum_aggr: { 'C-01': 6000, 'C-02': 4000 }, - power_mode_normal_cnt: { 'C-01': 12, 'C-02': 8 }, - power_mode_low_cnt: {}, - power_mode_high_cnt: {}, - offline_cnt: { 'C-01': 1 }, - error_cnt: {}, - not_mining_cnt: {}, - power_mode_sleep_cnt: {} - }] - ] - } - } - }) - - const mockReq = { query: { containers: 'C-01,C-02' } } - const result = await getGroupStats(mockCtx, mockReq) - - t.is(result.hashrateMhs, 100000, 'should have hashrate for both containers') - t.is(result.powerW, 10000, 'should have power for both containers') - t.is(result.minerCount, 21, 'should have miner count') - t.is(result.onlineCount, 20, 'should have online count') - t.ok(typeof result.efficiency === 'number', 'should have efficiency') + t.is(result.totalCount, 1) + t.is(result.data[0].id, 'group-1_rack-1') t.pass() }) -test('getGroupStats - missing containers throws', async (t) => { - const mockCtx = withDataProxy({ - conf: { orks: [] }, - net_r0: { jRequest: async () => ({}) } - }) - +test('getGroupStats - missing racks throws', async (t) => { + const ctx = createMockCtx() try { - await getGroupStats(mockCtx, { query: {} }) + await getGroupStats(ctx, { query: {} }) t.fail('should have thrown') } catch (err) { - t.is(err.message, 'ERR_MISSING_CONTAINERS', 'should throw missing containers error') + t.is(err.message, 'ERR_MISSING_RACKS') } t.pass() }) -test('getGroupStats - empty containers string throws', async (t) => { - const mockCtx = withDataProxy({ - conf: { orks: [] }, - net_r0: { jRequest: async () => ({}) } - }) - +test('getGroupStats - empty racks string throws', async (t) => { + const ctx = createMockCtx() try { - await getGroupStats(mockCtx, { query: { containers: '' } }) + await getGroupStats(ctx, { query: { racks: '' } }) t.fail('should have thrown') } catch (err) { - t.is(err.message, 'ERR_MISSING_CONTAINERS', 'should throw for empty containers') + t.is(err.message, 'ERR_MISSING_RACKS') } t.pass() }) -test('getGroupStats - filters to requested containers', async (t) => { - const mockCtx = withDataProxy({ - conf: { - orks: [{ rpcPublicKey: 'key1' }] - }, - net_r0: { - jRequest: async () => { - return [ - [{ - hashrate_mhs_1m_container_group_sum_aggr: { 'C-01': 50000, 'C-02': 30000, 'C-03': 20000 }, - power_w_container_group_sum_aggr: { 'C-01': 5000, 'C-02': 3000, 'C-03': 2000 }, - power_mode_normal_cnt: { 'C-01': 10, 'C-02': 8, 'C-03': 6 }, - power_mode_low_cnt: {}, - power_mode_high_cnt: {}, - offline_cnt: {}, - error_cnt: {}, - not_mining_cnt: {}, - power_mode_sleep_cnt: {} - }] - ] - } - } - }) - - const result = await getGroupStats(mockCtx, { query: { containers: 'C-01' } }) - t.is(result.hashrateMhs, 50000, 'should only include C-01 hashrate') - t.is(result.powerW, 5000, 'should only include C-01 power') - t.is(result.onlineCount, 10, 'should only include C-01 miners') +test('getGroupStats - returns empty data when DCS disabled', async (t) => { + const ctx = createMockCtx({ dcsEnabled: false }) + const result = await getGroupStats(ctx, { query: { racks: 'group-1_rack-1' } }) + t.is(result.totalCount, 0) + t.alike(result.data, []) t.pass() }) diff --git a/tests/unit/lib/queryUtils.test.js b/tests/unit/lib/queryUtils.test.js index 7e09896..34ea718 100644 --- a/tests/unit/lib/queryUtils.test.js +++ b/tests/unit/lib/queryUtils.test.js @@ -8,7 +8,7 @@ const { buildSearchQuery, flattenOrkResults, sortItems, - parseContainers, + parseRacks, paginateResults } = require('../../../workers/lib/server/lib/queryUtils') @@ -277,28 +277,28 @@ test('paginateResults - offset beyond total', (t) => { t.pass() }) -// ==================== parseContainers Tests ==================== +// ==================== parseRacks Tests ==================== -test('parseContainers - parses comma-separated containers', (t) => { - const result = parseContainers({ query: { containers: 'C-01,C-02,C-03' } }) - t.alike(result, ['C-01', 'C-02', 'C-03'], 'should split on commas') +test('parseRacks - parses comma-separated racks', (t) => { + const result = parseRacks({ query: { racks: 'group-1,group-2,group-3' } }) + t.alike(result, ['group-1', 'group-2', 'group-3'], 'should split on commas') t.pass() }) -test('parseContainers - trims whitespace', (t) => { - const result = parseContainers({ query: { containers: 'C-01 , C-02 , C-03' } }) - t.alike(result, ['C-01', 'C-02', 'C-03'], 'should trim spaces') +test('parseRacks - trims whitespace', (t) => { + const result = parseRacks({ query: { racks: 'group-1 , group-2 , group-3' } }) + t.alike(result, ['group-1', 'group-2', 'group-3'], 'should trim spaces') t.pass() }) -test('parseContainers - returns undefined when no containers', (t) => { - t.is(parseContainers({ query: {} }), undefined, 'missing containers returns undefined') - t.is(parseContainers({ query: { containers: '' } }), undefined, 'empty string returns undefined') +test('parseRacks - returns undefined when no racks', (t) => { + t.is(parseRacks({ query: {} }), undefined, 'missing racks returns undefined') + t.is(parseRacks({ query: { racks: '' } }), undefined, 'empty string returns undefined') t.pass() }) -test('parseContainers - single container', (t) => { - const result = parseContainers({ query: { containers: 'C-01' } }) - t.alike(result, ['C-01'], 'single container returns array with one element') +test('parseRacks - single rack', (t) => { + const result = parseRacks({ query: { racks: 'group-1' } }) + t.alike(result, ['group-1'], 'single rack returns array with one element') t.pass() }) diff --git a/workers/lib/constants.js b/workers/lib/constants.js index 182a00a..e929af9 100644 --- a/workers/lib/constants.js +++ b/workers/lib/constants.js @@ -494,14 +494,7 @@ const AGGR_FIELDS = { OFFLINE_CNT: 'offline_cnt', SLEEP_CNT: 'power_mode_sleep_cnt', MAINTENANCE_CNT: 'maintenance_type_cnt', - CONTAINER_SPECIFIC_STATS: 'container_specific_stats_group_aggr', - HASHRATE_1M_CONTAINER_GROUP_SUM: 'hashrate_mhs_1m_container_group_sum_aggr', - POWER_W_CONTAINER_GROUP_SUM: 'power_w_container_group_sum_aggr', - POWER_MODE_LOW_CNT: 'power_mode_low_cnt', - POWER_MODE_NORMAL_CNT: 'power_mode_normal_cnt', - POWER_MODE_HIGH_CNT: 'power_mode_high_cnt', - ERROR_CNT: 'error_cnt', - NOT_MINING_CNT: 'not_mining_cnt' + CONTAINER_SPECIFIC_STATS: 'container_specific_stats_group_aggr' } const PERIOD_TYPES = { diff --git a/workers/lib/server/handlers/groups.handlers.js b/workers/lib/server/handlers/groups.handlers.js index f530841..7120557 100644 --- a/workers/lib/server/handlers/groups.handlers.js +++ b/workers/lib/server/handlers/groups.handlers.js @@ -4,24 +4,25 @@ const { LOG_KEYS, WORKER_TYPES, WORKER_TAGS, - AGGR_FIELDS + EXPLORER_RACK_AGGR_FIELDS, + DCS_POWER_METER_FIELDS } = require('../../constants') -const { extractKeyEntry } = require('../../metrics.utils') -const { parseContainers } = require('../lib/queryUtils') - -function sumGroupedField (grouped, containers) { - if (!grouped || typeof grouped !== 'object') return 0 - let total = 0 - for (const id of containers) { - total += grouped[id] || 0 - } - return total -} +const { + aggregateRackStats, + buildRackList +} = require('./explorer.handlers') +const { mhsToPhs, mhsToThs, parseRackId } = require('../../metrics.utils') +const { + isCentralDCSEnabled, + getDCSTag, + extractDcsThing +} = require('../../dcs.utils') +const { parseRacks } = require('../lib/queryUtils') async function getGroupStats (ctx, req) { - const containers = parseContainers(req) - if (!containers || !containers.length) { - throw new Error('ERR_MISSING_CONTAINERS') + const requestedRacks = parseRacks(req) + if (!requestedRacks || !requestedRacks.length) { + throw new Error('ERR_MISSING_RACKS') } const tailLogPayload = { @@ -29,64 +30,91 @@ async function getGroupStats (ctx, req) { { key: LOG_KEYS.STAT_RTD, type: WORKER_TYPES.MINER, tag: WORKER_TAGS.MINER } ], limit: 1, - aggrFields: { - [AGGR_FIELDS.HASHRATE_1M_CONTAINER_GROUP_SUM]: 1, - [AGGR_FIELDS.POWER_W_CONTAINER_GROUP_SUM]: 1, - [AGGR_FIELDS.POWER_MODE_LOW_CNT]: 1, - [AGGR_FIELDS.POWER_MODE_NORMAL_CNT]: 1, - [AGGR_FIELDS.POWER_MODE_HIGH_CNT]: 1, - [AGGR_FIELDS.OFFLINE_CNT]: 1, - [AGGR_FIELDS.ERROR_CNT]: 1, - [AGGR_FIELDS.NOT_MINING_CNT]: 1, - [AGGR_FIELDS.SLEEP_CNT]: 1 + aggrFields: EXPLORER_RACK_AGGR_FIELDS + } + + const dcsEnabled = isCentralDCSEnabled(ctx) + let dcsPayload = null + if (dcsEnabled) { + const dcsTag = getDCSTag(ctx) + dcsPayload = { + query: { tags: { $in: [dcsTag] } }, + status: 1, + fields: { id: 1, code: 1, type: 1, tags: 1, ...DCS_POWER_METER_FIELDS } } } - const results = await ctx.dataProxy.requestDataMap('tailLogMulti', tailLogPayload) - return composeGroupStats(results, containers) + const [tailLogResults, dcsResults] = await Promise.all([ + ctx.dataProxy.requestDataMap('tailLogMulti', tailLogPayload), + dcsEnabled ? ctx.dataProxy.requestDataMap('listThings', dcsPayload) : Promise.resolve(null) + ]) + + const rackStats = aggregateRackStats(tailLogResults) + const dcsThing = dcsResults ? extractDcsThing(dcsResults) : null + const miningConfig = dcsThing?.last?.snap?.config?.mining || {} + + const allRacks = buildRackList(miningConfig, rackStats) + const realKeyById = mapRackIdToKeys(allRacks, rackStats) + + const requestedSet = new Set(requestedRacks) + const data = allRacks + .filter(rack => requestedSet.has(rack.id)) + .map(rack => formatRackValues(rack, realKeyById.get(rack.id), rackStats)) + + return { + data, + totalCount: data.length + } } -function composeGroupStats (results, containers) { - let hashrateMhs = 0 - let powerW = 0 - let onlineCount = 0 - let minerCount = 0 - - for (const orkResult of results) { - const minerEntry = extractKeyEntry(orkResult, 0) - if (!minerEntry) continue - - hashrateMhs += sumGroupedField(minerEntry[AGGR_FIELDS.HASHRATE_1M_CONTAINER_GROUP_SUM], containers) - powerW += sumGroupedField(minerEntry[AGGR_FIELDS.POWER_W_CONTAINER_GROUP_SUM], containers) - - const low = sumGroupedField(minerEntry[AGGR_FIELDS.POWER_MODE_LOW_CNT], containers) - const normal = sumGroupedField(minerEntry[AGGR_FIELDS.POWER_MODE_NORMAL_CNT], containers) - const high = sumGroupedField(minerEntry[AGGR_FIELDS.POWER_MODE_HIGH_CNT], containers) - const offline = sumGroupedField(minerEntry[AGGR_FIELDS.OFFLINE_CNT], containers) - const error = sumGroupedField(minerEntry[AGGR_FIELDS.ERROR_CNT], containers) - const notMining = sumGroupedField(minerEntry[AGGR_FIELDS.NOT_MINING_CNT], containers) - const sleep = sumGroupedField(minerEntry[AGGR_FIELDS.SLEEP_CNT], containers) - - onlineCount += low + normal + high - minerCount += low + normal + high + offline + error + notMining + sleep +function mapRackIdToKeys (racks, rackStats) { + const allRealKeys = new Set([ + ...Object.keys(rackStats.hashrateByRack), + ...Object.keys(rackStats.powerByRack), + ...Object.keys(rackStats.efficiencyByRack) + ]) + + const sortedByGroup = {} + for (const key of allRealKeys) { + const parsed = parseRackId(key) + if (!parsed) continue + ;(sortedByGroup[parsed.group] ||= []).push(key) + } + for (const list of Object.values(sortedByGroup)) { + list.sort((a, b) => a.localeCompare(b, undefined, { numeric: true })) } - const hashrateThs = hashrateMhs / 1000000 + const map = new Map() + const cursor = {} + for (const rack of racks) { + const groupId = rack.group.id + const pos = (cursor[groupId] = (cursor[groupId] ?? -1) + 1) + map.set(rack.id, sortedByGroup[groupId]?.[pos]) + } + return map +} + +function formatRackValues (rack, realKey, rackStats) { + if (!realKey) return rack + + const hashrateMhs = rackStats.hashrateByRack[realKey] || 0 + const powerW = rackStats.powerByRack[realKey] || 0 + const powerKw = Math.round(powerW / 10) / 100 + const hashrateThs = mhsToThs(hashrateMhs) const efficiency = hashrateThs > 0 ? Math.round((powerW / hashrateThs) * 10) / 10 - : 0 + : rackStats.efficiencyByRack[realKey] || 0 return { - efficiency, - hashrateMhs, - powerW, - minerCount, - onlineCount + ...rack, + efficiency: { value: efficiency, unit: 'W/TH/s' }, + hashrate: { value: mhsToPhs(hashrateMhs), unit: 'PH/s' }, + consumption: { value: powerKw, unit: 'kW' } } } module.exports = { getGroupStats, - composeGroupStats, - sumGroupedField + mapRackIdToKeys, + formatRackValues } diff --git a/workers/lib/server/lib/queryUtils.js b/workers/lib/server/lib/queryUtils.js index 37280cd..95f0550 100644 --- a/workers/lib/server/lib/queryUtils.js +++ b/workers/lib/server/lib/queryUtils.js @@ -172,10 +172,10 @@ function paginateResults (items, offset, limit) { } } -function parseContainers (req) { - const raw = req.query.containers +function parseRacks (req) { + const raw = req.query.racks if (!raw) return undefined - return raw.split(',').map(c => c.trim()).filter(Boolean) + return raw.split(',').map(r => r.trim()).filter(Boolean) } module.exports = { @@ -186,5 +186,5 @@ module.exports = { flattenOrkResults, sortItems, paginateResults, - parseContainers + parseRacks } diff --git a/workers/lib/server/routes/groups.routes.js b/workers/lib/server/routes/groups.routes.js index 75d1fe5..3c993b0 100644 --- a/workers/lib/server/routes/groups.routes.js +++ b/workers/lib/server/routes/groups.routes.js @@ -23,7 +23,7 @@ module.exports = (ctx) => { ctx, (req) => [ 'miners/groups/stats', - req.query.containers + req.query.racks ], ENDPOINTS.MINERS_GROUPS_STATS, getGroupStats, diff --git a/workers/lib/server/schemas/groups.schemas.js b/workers/lib/server/schemas/groups.schemas.js index 62054e2..fb782ba 100644 --- a/workers/lib/server/schemas/groups.schemas.js +++ b/workers/lib/server/schemas/groups.schemas.js @@ -5,10 +5,10 @@ const schemas = { groupsStats: { type: 'object', properties: { - containers: { type: 'string' }, + racks: { type: 'string' }, overwriteCache: { type: 'boolean' } }, - required: ['containers'] + required: ['racks'] } } }