Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions tests/unit/routes/site.routes.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ test('site routes - route definitions', (t) => {

const routeUrls = routes.map(route => route.url)
t.ok(routeUrls.includes('/auth/site/status/live'), 'should have site status live route')
t.ok(routeUrls.includes('/auth/site/overview/groups'), 'should have site overview groups route')

t.pass()
})
Expand All @@ -24,6 +25,9 @@ test('site routes - HTTP methods', (t) => {
const siteStatusRoute = routes.find(r => r.url === '/auth/site/status/live')
t.is(siteStatusRoute.method, 'GET', 'site status live route should be GET')

const siteOverviewRoute = routes.find(r => r.url === '/auth/site/overview/groups')
t.is(siteOverviewRoute.method, 'GET', 'site overview groups route should be GET')

t.pass()
})

Expand All @@ -35,6 +39,11 @@ test('site routes - schema validation', (t) => {
t.ok(siteStatusRoute.schema.querystring, 'should have querystring schema')
t.is(siteStatusRoute.schema.querystring.properties.overwriteCache.type, 'boolean', 'overwriteCache should be boolean')

const siteOverviewRoute = routes.find(r => r.url === '/auth/site/overview/groups')
t.ok(siteOverviewRoute.schema, 'site overview groups route should have schema')
t.ok(siteOverviewRoute.schema.querystring, 'should have querystring schema')
t.is(siteOverviewRoute.schema.querystring.properties.overwriteCache.type, 'boolean', 'overwriteCache should be boolean')

t.pass()
})

Expand Down
33 changes: 31 additions & 2 deletions workers/lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,9 @@ const ENDPOINTS = {
// Cooling System endpoints
COOLING_SYSTEM: '/auth/dcs/cooling-system',
// Energy System endpoints
ENERGY_SYSTEM: '/auth/dcs/energy-system'
ENERGY_SYSTEM: '/auth/dcs/energy-system',
// Site Overview endpoints
SITE_OVERVIEW_GROUPS: '/auth/site/overview/groups'
}

const HTTP_METHODS = {
Expand Down Expand Up @@ -421,6 +423,31 @@ const ENERGY_SYSTEM_PROJECTIONS = {
}
}

// Site Overview aggregation fields for group-level stats
const SITE_OVERVIEW_AGGR_FIELDS = {
hashrate_mhs_5m_container_group_sum_aggr: 1,
hashrate_mhs_5m_rack_group_sum_aggr: 1,
power_w_container_group_sum_aggr: 1,
power_w_rack_group_sum_aggr: 1,
efficiency_w_ths_container_group_avg_aggr: 1,
efficiency_w_ths_pdu_rack_group_avg_aggr: 1,
offline_cnt: 1,
error_cnt: 1,
not_mining_cnt: 1,
power_mode_sleep_cnt: 1,
power_mode_low_cnt: 1,
power_mode_normal_cnt: 1,
power_mode_high_cnt: 1,
hashrate_mhs_5m_active_container_group_cnt: 1
}

// DCS power meter field projections for site overview
const DCS_POWER_METER_FIELDS = {
'last.snap.stats.dcs_specific.equipment.power_meters': 1,
'last.snap.config.mining': 1,
'last.snap.config.energy_layout': 1
}

const AGGR_FIELDS = {
HASHRATE_SUM: 'hashrate_mhs_5m_sum_aggr',
SITE_POWER: 'site_power_w',
Expand Down Expand Up @@ -618,5 +645,7 @@ module.exports = {
MINER_MAX_LIMIT,
MINER_DEFAULT_LIMIT,
COOLING_SYSTEM_PROJECTIONS,
ENERGY_SYSTEM_PROJECTIONS
ENERGY_SYSTEM_PROJECTIONS,
SITE_OVERVIEW_AGGR_FIELDS,
DCS_POWER_METER_FIELDS
}
102 changes: 101 additions & 1 deletion workers/lib/metrics.utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,99 @@ function extractKeyEntry (orkResult, keyIndex) {
return keyResult[0] || null
}

// Hashrate conversion utilities
function mhsToPhs (mhs) {
return Math.round((mhs / 1000000000) * 100) / 100
}

function mhsToThs (mhs) {
return mhs / 1000000
}

// Rack/Group parsing utilities
function parseRackId (rackKey) {
if (!rackKey || typeof rackKey !== 'string') return null
const idx = rackKey.indexOf('_')
if (idx === -1) return null
return {
group: rackKey.substring(0, idx),
rack: rackKey.substring(idx + 1)
}
}

function getGroupNumber (groupName) {
const match = groupName.match(/group-(\d+)/i)
return match ? parseInt(match[1], 10) : null
}

function mergeGroupedField (target, source, isAverage = false) {
if (!source || typeof source !== 'object') return

for (const [key, value] of Object.entries(source)) {
if (isAverage) {
if (!target[key] || value > target[key]) {
target[key] = value
}
} else {
target[key] = (target[key] || 0) + (value || 0)
}
}
}

// DCS power meter utilities
function getMeterGroupMapping (meterId, energyLayout) {
const branches = energyLayout?.branches || []

for (const branch of branches) {
if (branch.meter === meterId && branch.feeds) {
const match = branch.feeds.match(/Groups?\s+(\d+)-(\d+)/i)
if (match) {
const start = parseInt(match[1], 10)
const end = parseInt(match[2], 10)
const groups = []
for (let i = start; i <= end; i++) {
groups.push(`group-${i}`)
}
return groups
}
}
}
return []
}

function buildGroupPowerFromDCS (powerMeters, hashrateByGroup, energyLayout, miningConfig) {
const groupPower = {}

const rackMeters = (powerMeters || []).filter(pm => pm.role === 'rack')

for (const meter of rackMeters) {
const meterPower = meter.power?.value || 0
const coveredGroups = getMeterGroupMapping(meter.equipment, energyLayout)

if (coveredGroups.length === 0 || meterPower === 0) continue

let totalHashrate = 0
for (const groupName of coveredGroups) {
totalHashrate += hashrateByGroup[groupName] || 0
}

if (totalHashrate > 0) {
for (const groupName of coveredGroups) {
const groupHashrate = hashrateByGroup[groupName] || 0
const proportion = groupHashrate / totalHashrate
groupPower[groupName] = (groupPower[groupName] || 0) + (meterPower * proportion)
}
} else {
const perGroup = meterPower / coveredGroups.length
for (const groupName of coveredGroups) {
groupPower[groupName] = (groupPower[groupName] || 0) + perGroup
}
}
}

return groupPower
}

module.exports = {
parseEntryTs,
validateStartEnd,
Expand All @@ -116,5 +209,12 @@ module.exports = {
extractContainerFromMinerKey,
extractKeyEntry,
resolveInterval,
getIntervalConfig
getIntervalConfig,
mhsToPhs,
mhsToThs,
parseRackId,
getGroupNumber,
mergeGroupedField,
getMeterGroupMapping,
buildGroupPowerFromDCS
}
Loading
Loading