Skip to content

Support site overview endpoints for dcs and miner group wise overview - #55

Merged
mukama merged 2 commits into
tetherto:developfrom
paragmore:feat/overview-group-api
Apr 16, 2026
Merged

mukama merged 2 commits into
tetherto:developfrom
paragmore:feat/overview-group-api

Conversation

@paragmore

Copy link
Copy Markdown
Contributor

No description provided.

Comment thread workers/lib/constants.js Resolved
@mukama
mukama merged commit b6f1076 into tetherto:develop Apr 16, 2026
6 checks passed
paragmore added a commit that referenced this pull request Jun 9, 2026
* Feature - Site status live api (#4)

* feat: add API v2 endpoints (finance, pools, pool-stats, pool-manager) (#11)

* feat: add GET /auth/finance/energy-balance endpoint

Add energy balance API endpoint that aggregates power consumption,
pool transactions, BTC prices, and production costs into a unified
time-series response with period-based aggregation (daily/monthly/yearly).

Includes shared infrastructure: period.utils, constants for RPC methods,
worker types, aggregation fields, and utility functions (getStartOfDay,
safeDiv, runParallel).

* feat: add GET /auth/pools/:pool/balance-history endpoint

Add pool balance history API endpoint that fetches pool balance
snapshots via tailLog RPC, groups them into time buckets (1D/1W/1M),
and returns time-series data with balance and revenue per bucket.

* tiny improvements

* feat: add GET /auth/finance/ebitda endpoint

Add EBITDA API endpoint that aggregates pool transactions, power/hashrate
data, BTC prices, production costs, and block data to calculate selling
and HODL EBITDA metrics with period-based aggregation.

Includes shared infrastructure: period.utils, constants, and utility
functions required for finance endpoints.

* feat: add GET /auth/finance/cost-summary endpoint

Add cost summary API endpoint that aggregates production costs, BTC
prices, and power consumption to calculate energy and operational cost
breakdowns with period-based aggregation.

* feat: add GET /auth/pools endpoint

Add pools API endpoint that aggregates pool device lists and stats
from ORK clusters, supports mingo-based filtering, sorting, and
field selection, and returns pool summary with totals.

* feat: add GET /auth/pool-stats/aggregate endpoint

Add pool stats aggregate API endpoint that fetches pool stats history
and transactions, processes daily stats and revenue, and aggregates
by period (daily/weekly/monthly) with optional pool filtering.

* Fix costs bugs

* feat: add curtailment, operational issues, and power utilization to energy-balance

- Add ELECTRICITY worker type, ENERGY_AGGR/ACTIVE_ENERGY_IN/UTE_ENERGY aggr fields,
  and GLOBAL_CONFIG RPC method to constants
- Add 3 new parallel RPC calls: electricity stats-history (active_energy_in,
  ute_energy) and getGlobalConfig (nominalPowerAvailability_MW)
- Calculate curtailmentMWh, curtailmentRate, operationalIssuesRate, and
  powerUtilization per log entry
- Add avgCurtailmentRate, avgOperationalIssuesRate, avgPowerUtilization to summary
- Fix getProductionCosts to use ctx.globalDataLib instead of direct Hyperbee access
- Fix processCostsData to return daily costs (energyCostPerDay, operationalCostPerDay)
- Update tests to match new cost format and globalDataLib mock

* feat: remove block data, fix costs processing, add site param to ebitda

- Remove block data RPC call and processBlockData (not in spec)
- Remove blockCount, difficulty, ebitdaMarginSelling, ebitdaMarginHodl from log
- Fix processCostsData to handle flat objects from globalDataLib with daily costs
- Replace direct globalDataLib.getGlobalData range query with getProductionCosts helper
- Add site query param to schema, cache key, and handler for site-specific costs

* feat: fix costs processing and add site param to cost-summary

- Fix processCostsData to handle flat objects from globalDataLib with daily costs
- Replace direct globalDataLib range query with getProductionCosts helper
- Add site query param to schema, cache key, and handler for site-specific costs
- Update tests to match new cost format

* feat: add hashrate and remove snapshotCount from balance-history log entries

Align pool balance-history endpoint with API v2 spec which requires
balance, hashrate, and revenue time-series data.

* feat: remove snapshotCount from pool-stats-aggregate log entries

Align pool stats aggregate endpoint with API v2 spec which does not
include snapshotCount in the response.

* fix: use ext-data transactions instead of tail-log for pool stats aggregate

tail-log returns empty for type=minerpool. Switch to ext-data with
transactions key which has daily revenue and hashrate data. Also fix
changed_balance handling (values are BTC, not satoshis).

* fix: use ext-data transactions instead of tail-log for balance history

tail-log returns empty for type=minerpool. Switch to ext-data with
transactions key which provides daily revenue (changed_balance) and
hashrate (mining_extra.hash_rate). Also treat 'all' pool param as
no filter.

* fix: use ext-data stats array instead of list-things for pools endpoint

list-things for t-minerpool returns empty; pool data lives in ext-data
stats responses. Replaced flattenResults/flattenStatsResults/mergePoolData
with flattenPoolStats that parses the actual stats array format.

* code cleanup

* cleanup

* cleanup

* Ports pool manager from moria

* Removes regions concept to align with site-focused nature of API v2

* chore: remove comments from pool manager files

* Remove unncecessaey sute param and add integration tests

* removes site

* fix: remove site param from ebitda endpoint and add integration test

Data is fetched for a single site, so the site parameter is unnecessary.
Added security integration test for finance/ebitda endpoint.

* fix: remove site param from cost-summary endpoint and add integration test

Data is fetched for a single site, so the site parameter is unnecessary.
Added security integration test for finance/cost-summary endpoint.

* fix: rename filter to query param and use mingo projection for pools

Align with list-things pattern by renaming filter→query. Replace manual
sort and field filtering with mingo cursor methods for consistency.

* fix: move pool filter to RPC payload for balance-history endpoint

Pool name filter should be handled by the RPC worker, not filtered
client-side. The tx username is the account id.

* fix: move pool filter to RPC payload for pool-stats-aggregate endpoint

Pool name filter should be handled by the RPC worker, not filtered
client-side. The tx username is the account id.

* Address PR comments

* fix failing tests

* Removes tags from projections

* Fix integration tests

* requestRpcMapAllPages to fetch all pages from orks

* fix linting

* Fix failing tests

* fix: Improves user and roles ux (#12)

* return the created user

* fix: require users:r permission for GET /auth/roles/permissions

* fix: update getRolesPermissions route to require users:w permission

* fix: enhance getRolesPermissions to filter roles based on user role

* fix: update getRolesPermissions to exclude roleManagement for super admin

* Cleanup

* feat: add GET /auth/finance/subsidy-fees endpoint (#14)

* feat: add GET /auth/finance/subsidy-fees endpoint

Port Bitcoin network block data (subsidy rewards + transaction fees) from
legacy dashboard to API v2. Fetches HISTORICAL_BLOCKSIZES from mempool
worker, aggregates by period (daily/weekly/monthly).

* fix: remove unused site parameter from getProductionCosts

* Adding CI (#23)

* feat: add alerts API v2 endpoints (#24)

* resolve conflicts

* Feature: Alerts

* refactor: use RPC constants and explicit field mapping in alerts handlers

* Fix code smells

* fix failing tests

* Remove metrics

* Address comments

* feat: add metrics API v2 endpoints (#21)

* feat: add finance revenue and revenue-summary endpoints (#22)

* feat: add GET /auth/finance/revenue endpoint

Port pool transaction revenue data from legacy dashboard to API v2.
Separates revenue from pool fees, supporting both F2Pool (changed_balance
+ tx_fee) and Ocean (satoshis_net_earned + fees_colected_satoshis) formats.
Aggregates by period with optional pool filter.

* feat: add GET /auth/finance/revenue-summary endpoint

Most impactful endpoint — replaces 9 API calls + ~1500 LOC of frontend
processing. Merges transaction data, BTC prices, costs, power, hashrate,
block data, and electricity data into comprehensive daily rows with EBITDA,
production cost, hash revenue, curtailment, and power utilization metrics.

* fix: include pool name in worker type for revenue endpoint

* refactor: extract shared finance utils and consolidate duplicated logic

- Create finance.utils.js with shared utilities:
  - validateStartEnd: replaces duplicated validation in every handler
  - normalizeTimestampMs: moved from finance.handlers.js
  - processTransactions: consolidates processTransactionData and
    processEbitdaTransactions (fixes missing normalizeTimestampMs and
    incorrect BTC_SATS division bugs), supports trackFees option
  - extractCurrentPrice: merges flat and nested EBITDA price formats
  - processBlockData: extracted for reuse across revenue endpoints
- Refactor finance.handlers.js to use shared utils
- Add comprehensive unit tests for all utils (24 tests)
- Update handler tests to remove tests for deleted functions

* Remove spec doc

* feat: add device listing API v2 endpoints (#25)

* feat: add device listing API v2 endpoints

* fix: paginate listThings RPC and add field projections for device endpoints

Address PR review: use requestRpcMapAllPages to fetch all items beyond
the default 100-item RPC limit, and pass DEVICE_LIST_FIELDS projection
to reduce response size when status=1.

* fix: use requestRpcMapLimit with limit/offset and remove status

- Switch getMiners/getContainers from requestRpcMapAllPages to
  requestRpcMapLimit, passing limit/offset to RPC instead of
  fetching all objects
- Remove status:1 from all listThings calls as DEVICE_LIST_FIELDS
  don't have snap properties
- Update pagination test to verify RPC payload

* Add maximium to limit

* Remove /auth/miners

* Feat/add get configs route (#26)

* Add get configs route

* Add tests and lint

* Remove old pool manager apis (#29)

* feat: add GET /auth/finance/hash-revenue endpoint (#27)

* non rpc method calls to ork (#28)

* fix: address OWASP security review findings for v2 API endpoints (#30)

* ci: enforce coverage (#31)

* feat: pool config stats (#32)

* feat: pool config stats

* var name typo fix

* feat: add GET /auth/miners with getThingsCount (#33)

* fix: config routes read permissions fix (#34)

* ci: improve coverage summary (NYC table) (#35)

* feat: containers pool stats (#36)

* fix: add schema bounds to prevent negative limit/offset bypass (#37)

* ci: move away from pull_request_target (#38)

* feat: add groups stats endpoint and racks filter for metrics (#47)

* feat: add groups stats endpoint and racks filter for metrics

Add GET /auth/groups/stats endpoint for live rack-group aggregation
(efficiency, hashrate, power, miner counts). Extend hashrate and
consumption metrics with optional racks query parameter for filtering
by rack groups via ORK RPC.

* feat: add groups stats endpoint and racks filter for metrics

Add GET /auth/groups/stats endpoint for live rack-group aggregation
(efficiency, hashrate, power, miner counts). Extend hashrate and
consumption metrics with optional racks query parameter for filtering
by rack groups via ORK RPC.

* fix: rename racks filter to containers for metrics endpoints

* refactor: deduplicate parseContainers, extractKeyEntry and use AGGR_FIELDS constants

* fix: remove non-functional containers filter from hashrate/consumption

tailLogCustomRangeAggr RPC method does not support containers param,
so remove the dead code from getHashrate/getConsumption handlers,
their schemas, route cache keys, and related tests.

* fix: handle noAuth mode in capCheck

capCheck crashes in noAuth mode when routes pass permissions because
ctx.authLib is undefined. Add early return guard matching authCheck.

* Revert "fix: handle noAuth mode in capCheck"

This reverts commit 2d4d992.

* test: improve branch coverage for metrics handlers

Add tests for uncovered branches: temperature rolling avg,
container history prefix match, non-object guard branches,
and groupRange config paths in getPowerMode/getTemperature.

* chore: remove unnecessary comment

* Add new auth/cooling system api (#51)

* Add new auth/cooling system api

* Fix review comments, make the tag generic for dcs device and update endpoint path

* Fix

* remove device name

* fix

* fix: sanitize queryActions input to prevent injection and DoS (#49)

* fix: sanitize queryActions input to prevent injection and DoS (TDEBT-38)

Add maxLength constraints to suffix (200) and queries (10000) in route
schemas, validate queries array type and cap length at 50 in handler.

* fix: tighten queryActions limits and extract to constants

Reduce queries string maxLength from 10000 to 1000 and cap the array at
10 items (previously 50). Extract both limits to named constants.

* Add energy views endpoint support (#52)

* Add energy views endpoint support

* move outside to dir with other utils

* fix imports

* feat: microsoft oauth (#54)

* Support site overview endpoints for dcs and miner group wise overview (#55)

* Support site overview endpoints for dcs and miner group wise overview

* update move to site handlers

* Support new dcs device tags update and fix cooling system apis (#56)

* Support new dcs device tags update and fix cooling system apis

* update the attributes for rack group

* Tests

* tests

* Add hvac circuit 1 and layout api updates (#58)

* Add hvac circuit 1 and layout api updates

* Update hvac circuit2 api

* Add site effciency api (#57)

* Add site effciency api

* Add site effciency api routes

* tests

* Add explorer racks api (#59)

* (improvement) Explorer handlers, Racks list, Search filtering flow improved (#60)

* improv: workers/lib/server/handlers - explorer - 'filterBySearch' flow improved.

* improv: workers/lib/server/handlers - explorer - 'filterBySearch' flow & usage improved.

* fix: workers/lib/server/handlers - explorer - 'filterBySearch' flow fixed.

* test: tests/unit/handlers - explorer - 'filterBySearch' cases coverage extended.

* test: tests/unit/handlers - coolingSystem - 'createMockConfig' result generation fixed.

* feat: group hashrate by miner and container (#62)

* fix: finance v2 handler RPC regressions (#67)

* fix: processBlockData parses HISTORICAL_BLOCKSIZES flat-array shape

The mempool worker's getWrkExtData returns a flat array of block records
[{ts, blockSize, blockReward, blockTotalFees}, ...], but processBlockData
only handled nested .data/.blocks wrappers - it iterated entry's own field
names, Number('ts') returned NaN, and produced an empty daily map. The
SubsidyFee page then rendered "No data available".

Detect the flat shape (entry has .ts/.timestamp/.time) and process it
directly. Also surface blockSize on getSubsidyFees and getRevenueSummary
log entries (and totalBlockSize/avgBlockSize on the summary) - needed for
SubsidyFee's "Avg Fees in Sats/vByte" chart to render after this fix.

* fix: cost-summary propagates per-day btcPrice

Two compounding bugs in getCostSummary caused btcPrice=0 for every log
entry, leaving ProductionCostPriceChart's overlay line at 0:

1. Wrong RPC key. The handler queried mempool with key: 'prices', which
   the worker doesn't recognize and so returns the live snapshot
   ({currentPrice, blockHeight, ...}). Switched to 'HISTORICAL_PRICES',
   which returns [{ts, priceUSD}, ...].

2. processEbitdaPrices couldn't parse the production shape. Same class
   as the processBlockData fix (Bug C): the mempool worker returns a flat
   per-ORK array of records, not a wrapper. Detect when entry has
   .ts/.timestamp/.time and process it as the item directly; also accept
   priceUSD (the actual upstream field name) alongside price.

The processEbitdaPrices fix incidentally makes getRevenueSummary and
getHashRevenue use real per-day prices instead of falling back to
currentBtcPrice. getEbitda still uses the wrong key: 'prices' (line 344)
and is left for a follow-up.

* fix: processTailLogData drills into .val for nested TAIL_LOG_RANGE_AGGR

The TAIL_LOG_RANGE_AGGR RPC returns per-day items as
{ts, val: {site_power_w, hashrate_mhs_5m_sum_aggr}}, but
processTailLogData read item[AGGR_FIELDS.SITE_POWER] directly without
drilling into .val, so powerW and hashrateMhs were 0 for every entry.
RevenueSummary then showed 0 for Avg Power, Avg Hashrate, and Hashrate
Capacity Factor; EBITDA's per-day power/hashrate were similarly zeroed.

Mirror the .val/.flat fallback pattern that processConsumptionData
already uses (workers/lib/server/handlers/finance.handlers.js:172).

* fix: hash-revenue hashrate and network-hashrate populate from real RPC

Two bugs in getHashRevenue caused hashrateMhs and networkHashrateMhs
to be 0 for every log entry, leaving HashBalance entirely zeroed:

1. processHashrateData read item[AGGR_FIELDS.HASHRATE_SUM] directly,
   but TAIL_LOG_RANGE_AGGR wraps measurements as
   {ts, val: {hashrate_mhs_5m_sum_aggr}}. Same .val drilling fix as
   processTailLogData.

2. processNetworkHashrateData expected entries wrapped under .data,
   but the HISTORICAL_HASHRATE RPC returns a flat per-ORK array of
   {ts, avgHashrateMHs} records. Same flat-shape detection as
   processBlockData (Bug C).

* Fix memory leak in authCheck by implementing LRU caching and removing in-memory cache (#61)

* Support summary for grouped by in hashrate metrics api (#68)

* Support summary for grouped by in hashrate metrics api

* update groupKey

* fix: groups stats accepts rack ids and returns rack objects (#69)

* Add groupBy for metrics consumption api (#70)

* Update the cooling system handlers to fix flow readings (#71)

* fix(finance): correct energy-balance period aggregation (#72)

The shared aggregateByPeriod summed every numeric field, which is
correct for totals but produced nonsense for rates/means (curtailment
rate, btcPrice, sitePowerMW, etc.) on weekly/monthly buckets.

- Add an optional `meanKeys` arg to aggregateByPeriod that averages
  listed fields instead of summing. Default behavior is unchanged for
  the 6 existing callers.
- getEnergyBalance now passes meanKeys for sitePowerMW, btcPrice and
  the rate fields, then post-processes to recompute energyRevenue*_MW
  against the (correctly meaned) sitePowerMW denominator.
- Add sitePowerMW + energyRevenueBTC_MW/energyRevenueUSD_MW to log
  entries; add avgPowerConsumption + avgEnergyCostPerMWh +
  avgOperationalCostPerMWh to summary.
- Schema: add 'weekly' to the period enum.

* chore: facs version update (#73)

* Update the dcs apis to support the new tags (#74)

* develop sync (#76)

* feat: adopt JWT-based svc-facs-auth (#53)

* feat: forecast endpoints (#80)

* feat: add rack grouping to metrics/hashrate endpoint (#82)

Extend GET /auth/metrics/hashrate with `groupBy=rack` and an optional
`racks` CSV filter so charts can render a per-rack hashrate time series
for a specific set of racks.

- groupBy=rack reads the hashrate_mhs_5m_pdu_rack_group_sum aggregation
  from stat-1D, the same obj_concat aggregation family already used for
  type/container group grouping
- racks=group-1_rack-1,group-2_rack-1 narrows each log entry's hashrate
  object and the summary breakdown to the requested racks
- racks is cache-keyed and only applied when groupBy=rack

* feat: add rack grouping to metrics/consumption endpoint (#83)

Extend GET /auth/metrics/consumption with `groupBy=rack` and an optional
`racks` CSV filter, mirroring the rack grouping added to metrics/hashrate
in #82.

- groupBy=rack reads the power_w_pdu_rack_group_sum aggregation from
  stat-1D, the same obj_concat aggregation family already used for
  type/container group grouping
- racks=group-1_rack-1,group-2_rack-1 narrows each log entry's powerW
  object (and the derived consumptionMWh) and the summary to those racks
- racks is cache-keyed and only applied when groupBy=rack

* feat: work order + spare parts HTTP API (#79)

* feat: expose Work Order HTTP API + RBAC

Adds the Work Order HTTP surface as thin wrappers over the existing
things RPC plumbing:
  POST   /auth/work-orders                create  (registerThing)
  GET    /auth/work-orders                list    (listThings + type filter)
  GET    /auth/work-orders/:id            get     (listThings + id+type filter)
  PATCH  /auth/work-orders/:id            update  (updateThing)
  POST   /auth/work-orders/:id/close      close   (updateThing status=closed)
  POST   /auth/work-orders/:id/cancel     cancel  (updateThing status=cancelled)
  POST   /auth/work-orders/:id/assign     assign  (updateThing assignedTo)
  GET    /auth/work-orders/:id/audit      history (getHistoricalLogs)

Introduces a new RBAC resource work_order (r/rw) granted to
operator/admin/repair_technician roles for write and to viewer
roles for read. Strips the duplicated [HRPC_ERR]= prefix and
bubbles structured rack errors verbatim. Skips capCheck when the
gateway runs with --noauth so local curl-testing works through
permissioned routes.

* feat: searchable Work Order list with pagination envelope

Adds filter shortcuts (q, assignee, creator, partId, status, type,
from, to) to GET /auth/work-orders alongside the existing mingo
?query passthrough; q is a case-insensitive regex $or against code
and info.issue, the rest map to info.* paths and run on the rack
the same way every other list does. Pages now return the
{data, totalCount, offset, limit, hasMore} envelope by calling
listThings and getThingsCount in parallel, and every shortcut is
wired into the cache key so cached pages do not bleed across
filter combinations.

* feat: spare-part PUT gated by an open Work Order + repair history

Adds PUT /auth/spare-parts/:id which validates the supplied
workOrderId resolves to an open or in_progress WO, pushes the
underlying updateThing on the part rack with workOrderId injected
into info, and appends a denormalised entry to the WO's
partsMoves[] so the audit trail stays on the Work Order itself.
GET /auth/spare-parts/:id/repair-history mingo-queries every WO
that ever touched the part and returns each move row hydrated with
workOrderCode in the standard {data, totalCount, offset, limit,
hasMore} envelope.

* feat: Work Order log entries + file upload/download/delete routes

Adds POST /auth/work-orders/:id/log which forwards to the existing
saveThingComment RPC so the WO comments[] doubles as the work log.
Adds POST/GET/DELETE /auth/work-orders/:id/files routes backed by
@fastify/multipart on the gateway and the new
storeWorkOrderFile/loadWorkOrderFile/removeWorkOrderFile pass-
throughs on the ork; uploads validate mime + size, append metadata
to WO.info.files via pushAction, and reject mutations on closed or
cancelled WOs.

* feat: spare-part register flow + Type-1/Type-2 Work Order dispatch

POST /auth/spare-parts now creates the part and a paired Type-1 WO in
one call, returning {partId, workOrderId, workOrderCode}. POST
/auth/work-orders resolves the target part for both flows from
deviceIdentifier (id, code, serialNum, or macAddress) and rejects
unknown deviceTypes with ERR_INVALID_DEVICE_TYPE / unresolved parts
with ERR_PART_NOT_FOUND.

* feat: accept warranty payload on Work Order create/update

Adds an open `warranty: { vendor, fields }` shape to the create and
update body schemas; vendor-specific field validation lives in the
inventory worker so app-node stays agnostic of vendor differences.

* feat: GET /auth/work-orders/:id/export with PDF + CSV output

Export accepts either the WO uuid or its IVI-* code in the path and a
required ?format=pdf|csv|docx querystring. CSV flattens one row per
parts-movement entry (WO header columns repeated). PDF is server-rendered
via PDFKit and includes header, triage, work log, parts movements, file
references, and a vendor-specific warranty section. format=docx returns
501 with ERR_EXPORT_FORMAT_NOT_IMPLEMENTED until phase 2.

* refactor: ship CSV only; pdf + docx return 501 (deferred to FE/phase 2)

Drops the pdfkit dependency and the PDF renderer. The export endpoint
now serves CSV directly; pdf and docx both 501 with
ERR_EXPORT_FORMAT_NOT_IMPLEMENTED, keeping the route shape stable for
when the frontend renders PDF client-side.

* feat: align SPARE_PART_INITIAL_LOCATION to canonical 'Site Warehouse'

Mirrors the new MINER_LOCATIONS enum from the inventory worker.

* refactor: camelCase MINER_LOCATIONS values + SPARE_PART_INITIAL_LOCATION

Mirrors the inventory worker constant.

* feat: GET /auth/spare-parts with mingo-side location / status / q filters

Querystring shortcuts (location, status, q) translate to a mingo query
forwarded to the rack via listThings + getThingsCount — no app-node
post-filtering. q is a case-insensitive regex against code, info.serialNum,
and info.macAddress. WO things are excluded by default via type:{\$ne:...},
overridable by an explicit query.type. Cache key includes every filter
shortcut so combinations don't collide.

* refactor: extract escapeRegex, listThingsWithCount, stableJsonString to utils

Both list handlers (work-orders + spare-parts) hand-rolled the same
regex-escape + listThings/getThingsCount/pagination skeleton; collapses
to a single utils helper used by both. Cache keys for the two GET list
routes now canonicalize their JSON inputs via stableJsonString so
semantically-equal queries share a cache slot instead of missing each
other on key-order differences.

* refactor: drop waitForThing — registerSparePart returns action ids, no polling

App-node was polling listThings after each pushAction to surface the
rack-assigned thing code in the response — exactly the
'submit-and-poll-in-app-node' anti-pattern the codebase had already
rejected. registerSparePart now pre-generates partId/woId, fires both
registerThing pushActions in parallel, and returns
{partId, workOrderId, partActionId, workOrderActionId, errors}. Clients
read the eventually-assigned codes from GET /auth/actions/done/:id like
every other write in the app.

Removes waitForThing + sleep from utils and the two timeout constants
that only existed to tune it.

* refactor: address peer-review #3 #6 #8 #10 #12 #20

#3 (atomicity) — updateSparePart now checks the part pushAction's
   errors[] before firing the WO partsMoves append. If the part write
   was rejected at push time, throws ERR_PART_UPDATE_PUSH_FAILED:<msg>
   with a 502 instead of silently enqueueing the WO append. Response
   shape also gains partActionId / workOrderActionId / workOrderAppendErrors
   so the FE can detect a post-push WO failure and trigger a manual
   reconcile.
#6 (status casing) — WO status enum normalised to 'inProgress' so
   every info.* enum value follows the camelCase convention adopted
   for MINER_LOCATIONS.
#8 (registerSparePart field sprawl) — drops the
   info.model→info.deviceModel→'unknown' fallback chain and the
   serialNum→macAddress→partId fallback. deviceModel and serialNum
   are now both required up front with explicit ERR_*_REQUIRED errors.
#10 (action latency) — register/update responses include
   expectedActionLatencyMs sourced from ctx.conf.expectedActionLatencyMs
   (default 1000) so the FE can pace its /auth/actions/done/:id polling
   instead of guessing.
#12 (export 501) — error message now carries the requested format
   (ERR_EXPORT_FORMAT_NOT_IMPLEMENTED:pdf / :docx) so the FE can tell
   which format was rejected without re-reading the request.
#20 (pagination) — listThingsWithCount now slices the dedupe'd union
   down to the requested limit so multi-ork fan-out can't return more
   rows than the caller asked for. hasMore is now derived from
   offset + limit rather than offset + data.length. Multi-rack
   offset-pagination remains best-effort because each rack applies
   the offset locally; documented inline.

* revert: restore 'in_progress' in WO status schema enum

Mirrors the inventory rack revert — keeps existing 'in_progress' WO
records valid for transitions.

* revert: restore human-readable MINER_LOCATIONS values

Mirrors the inventory rack revert.

* refactor: call generic storeFile / loadFile / removeFile with file type

Updates the work-order file handlers to use the renamed generic file
RPCs and tags each call with FILE_TYPES.WORK_ORDER so the rack can
dispatch on it.

* fix: pass workOrderId + fileId to scoped file RPCs, surface blobCleared

loadFile / removeFile are now called with { workOrderId, fileId } so the
rack resolves the blob descriptor from the work order it belongs to —
app-node no longer forwards a raw blobRef. deleteWorkOrderFile surfaces
the rack's { cleared } result as blobCleared so the HTTP caller can tell
whether the underlying blob was actually removed.

* refactor: address PR #79 review feedback

- resolve the work_order rack from the ork rack registry via listRacks
  (getWorkOrderRackId, cached on ctx) and drop the workOrderRackId config
  key — the rack id is discovered, not deployment config
- relocate submitWorkOrderAction out of generic utils into the new
  server/lib/workOrders module alongside getWorkOrderRackId
- add WORK_ORDER_TERMINAL_STATUSES constant, replacing the duplicated
  ['closed', 'cancelled'] literals across the WO handlers
- workOrderExport: derive the CSV header and rows from one [name,
  extractor] schema instead of two hand-synced lists
- rename pushOne -> pushSingleAction in registerSparePart

* refactor: derive WO export CSV columns from json keys

renderWorkOrderCsv builds the header from the work order's own properties
— top-level code plus every info field, then each parts-movement entry's
keys — so the CSV tracks the worker response schema with no hand-kept
column list. Drops the CSV_HEADERS / WO_COLUMNS / MOVE_COLUMNS definitions.

* refactor: remove non-functional comments from WO export and rack helpers

* refactor: promote _csvEscape to a generic csvEscape util

* refactor: rename WO and spare-parts modules to dot-separated file names

Align the work order and spare parts file names with the repo convention
(cooling.system.routes.js, energy.system.routes.js, etc.): camelCase
basenames become dot-separated — spareParts.handlers.js ->
spare.parts.handlers.js, workOrders.js -> work.orders.js,
workOrderExport.js -> work.order.export.js. Updates every require() path,
route registration in index.js, and test references.

* (fix) NPM audit, Dependencies invulnerability fixed (#84)

* chore: root - package-lock - dependency tree updated.

* fix: root - package-lock - dependency tree packages invulnerability fixed.

* feat: list firmwares endpoint (#86)

* feat: miner log download via Hypercore P2P streaming (#85)

* feat: add miner log download REST endpoints and unit tests

* add LogDownloader for P2P miner log streaming

Introduces a LogDownloader class that fetches miner log files directly
from wrk-miner via Hypercore/Hyperswarm, bypassing the HRPC pipeline.
Wired up in the http worker after facilities are ready.

* trim verbose comments in log download handlers

* Fix LOGS_PEER_NOT_FOUND

* feat: forecast history endpoint (#87)

* feat: forecast settings (#89)

* feat: forecast settings

* get forecast settings endpoint

* fix: add info params + fix WO auth (#88)

* feat: add info params (notes, remarks, site, location) to work order schema

Accept optional info object with notes, remarks, site, and location
on POST /auth/work-orders and PATCH /auth/work-orders/:id. The handler
merges these into the thing info alongside the top-level fields.

* fix: auto-qualify bare permission names in tokenHasPerms

tokenHasPerms received bare resource names (e.g. 'work_order') from
route perms, but _permsMatch expects 'resource:level' format. Splitting
a bare name by ':' left `required` as undefined, crashing on spread.

* Chore/main sync (#91)

* Ci pr trigger change main (#39)

* chore: package json updates for publishing (#41)

* chore: package json updates for publishing

* update deps

* ci: package publishing (#40)

* ci: adding release pr process and version bump (#42)

* Release: v1.0.0 (#44)

* ci: release pr local jobs (#46)

* ci: persistents credentials to allow tag creation (#48)

* Release: v1.0.0 (#50)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* chore: facs version update (#77)

* refactor: logs cleanup (#78)

* ci: CI Checks — dependency review, audits, and workflow improvements (#81)

- Introduce security dependency review on pull requests
- Improve audit reporting (dependency review, npm/pnpm audit, audit-ci on core)
- Improve overall CI checks: single install + node_modules cache, lockfile gate, parallel jobs, summary

Fleet: core. Suggested PR title matches commit subject.

* reset ci updates

---------

Co-authored-by: andretetherio <andre.mendes@tether.io>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

---------

Co-authored-by: Parag More <34959548+paragmore@users.noreply.github.com>
Co-authored-by: Caesar Mukama <mcaesar88@gmail.com>
Co-authored-by: andretetherio <andre.mendes@tether.io>
Co-authored-by: borik91 <9007515+boris91@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
mukama added a commit that referenced this pull request Jun 23, 2026
* Feature - Site status live api (#4)

* feat: add API v2 endpoints (finance, pools, pool-stats, pool-manager) (#11)

* feat: add GET /auth/finance/energy-balance endpoint

Add energy balance API endpoint that aggregates power consumption,
pool transactions, BTC prices, and production costs into a unified
time-series response with period-based aggregation (daily/monthly/yearly).

Includes shared infrastructure: period.utils, constants for RPC methods,
worker types, aggregation fields, and utility functions (getStartOfDay,
safeDiv, runParallel).

* feat: add GET /auth/pools/:pool/balance-history endpoint

Add pool balance history API endpoint that fetches pool balance
snapshots via tailLog RPC, groups them into time buckets (1D/1W/1M),
and returns time-series data with balance and revenue per bucket.

* tiny improvements

* feat: add GET /auth/finance/ebitda endpoint

Add EBITDA API endpoint that aggregates pool transactions, power/hashrate
data, BTC prices, production costs, and block data to calculate selling
and HODL EBITDA metrics with period-based aggregation.

Includes shared infrastructure: period.utils, constants, and utility
functions required for finance endpoints.

* feat: add GET /auth/finance/cost-summary endpoint

Add cost summary API endpoint that aggregates production costs, BTC
prices, and power consumption to calculate energy and operational cost
breakdowns with period-based aggregation.

* feat: add GET /auth/pools endpoint

Add pools API endpoint that aggregates pool device lists and stats
from ORK clusters, supports mingo-based filtering, sorting, and
field selection, and returns pool summary with totals.

* feat: add GET /auth/pool-stats/aggregate endpoint

Add pool stats aggregate API endpoint that fetches pool stats history
and transactions, processes daily stats and revenue, and aggregates
by period (daily/weekly/monthly) with optional pool filtering.

* Fix costs bugs

* feat: add curtailment, operational issues, and power utilization to energy-balance

- Add ELECTRICITY worker type, ENERGY_AGGR/ACTIVE_ENERGY_IN/UTE_ENERGY aggr fields,
  and GLOBAL_CONFIG RPC method to constants
- Add 3 new parallel RPC calls: electricity stats-history (active_energy_in,
  ute_energy) and getGlobalConfig (nominalPowerAvailability_MW)
- Calculate curtailmentMWh, curtailmentRate, operationalIssuesRate, and
  powerUtilization per log entry
- Add avgCurtailmentRate, avgOperationalIssuesRate, avgPowerUtilization to summary
- Fix getProductionCosts to use ctx.globalDataLib instead of direct Hyperbee access
- Fix processCostsData to return daily costs (energyCostPerDay, operationalCostPerDay)
- Update tests to match new cost format and globalDataLib mock

* feat: remove block data, fix costs processing, add site param to ebitda

- Remove block data RPC call and processBlockData (not in spec)
- Remove blockCount, difficulty, ebitdaMarginSelling, ebitdaMarginHodl from log
- Fix processCostsData to handle flat objects from globalDataLib with daily costs
- Replace direct globalDataLib.getGlobalData range query with getProductionCosts helper
- Add site query param to schema, cache key, and handler for site-specific costs

* feat: fix costs processing and add site param to cost-summary

- Fix processCostsData to handle flat objects from globalDataLib with daily costs
- Replace direct globalDataLib range query with getProductionCosts helper
- Add site query param to schema, cache key, and handler for site-specific costs
- Update tests to match new cost format

* feat: add hashrate and remove snapshotCount from balance-history log entries

Align pool balance-history endpoint with API v2 spec which requires
balance, hashrate, and revenue time-series data.

* feat: remove snapshotCount from pool-stats-aggregate log entries

Align pool stats aggregate endpoint with API v2 spec which does not
include snapshotCount in the response.

* fix: use ext-data transactions instead of tail-log for pool stats aggregate

tail-log returns empty for type=minerpool. Switch to ext-data with
transactions key which has daily revenue and hashrate data. Also fix
changed_balance handling (values are BTC, not satoshis).

* fix: use ext-data transactions instead of tail-log for balance history

tail-log returns empty for type=minerpool. Switch to ext-data with
transactions key which provides daily revenue (changed_balance) and
hashrate (mining_extra.hash_rate). Also treat 'all' pool param as
no filter.

* fix: use ext-data stats array instead of list-things for pools endpoint

list-things for t-minerpool returns empty; pool data lives in ext-data
stats responses. Replaced flattenResults/flattenStatsResults/mergePoolData
with flattenPoolStats that parses the actual stats array format.

* code cleanup

* cleanup

* cleanup

* Ports pool manager from moria

* Removes regions concept to align with site-focused nature of API v2

* chore: remove comments from pool manager files

* Remove unncecessaey sute param and add integration tests

* removes site

* fix: remove site param from ebitda endpoint and add integration test

Data is fetched for a single site, so the site parameter is unnecessary.
Added security integration test for finance/ebitda endpoint.

* fix: remove site param from cost-summary endpoint and add integration test

Data is fetched for a single site, so the site parameter is unnecessary.
Added security integration test for finance/cost-summary endpoint.

* fix: rename filter to query param and use mingo projection for pools

Align with list-things pattern by renaming filter→query. Replace manual
sort and field filtering with mingo cursor methods for consistency.

* fix: move pool filter to RPC payload for balance-history endpoint

Pool name filter should be handled by the RPC worker, not filtered
client-side. The tx username is the account id.

* fix: move pool filter to RPC payload for pool-stats-aggregate endpoint

Pool name filter should be handled by the RPC worker, not filtered
client-side. The tx username is the account id.

* Address PR comments

* fix failing tests

* Removes tags from projections

* Fix integration tests

* requestRpcMapAllPages to fetch all pages from orks

* fix linting

* Fix failing tests

* fix: Improves user and roles ux (#12)

* return the created user

* fix: require users:r permission for GET /auth/roles/permissions

* fix: update getRolesPermissions route to require users:w permission

* fix: enhance getRolesPermissions to filter roles based on user role

* fix: update getRolesPermissions to exclude roleManagement for super admin

* Cleanup

* feat: add GET /auth/finance/subsidy-fees endpoint (#14)

* feat: add GET /auth/finance/subsidy-fees endpoint

Port Bitcoin network block data (subsidy rewards + transaction fees) from
legacy dashboard to API v2. Fetches HISTORICAL_BLOCKSIZES from mempool
worker, aggregates by period (daily/weekly/monthly).

* fix: remove unused site parameter from getProductionCosts

* Adding CI (#23)

* feat: add alerts API v2 endpoints (#24)

* resolve conflicts

* Feature: Alerts

* refactor: use RPC constants and explicit field mapping in alerts handlers

* Fix code smells

* fix failing tests

* Remove metrics

* Address comments

* feat: add metrics API v2 endpoints (#21)

* feat: add finance revenue and revenue-summary endpoints (#22)

* feat: add GET /auth/finance/revenue endpoint

Port pool transaction revenue data from legacy dashboard to API v2.
Separates revenue from pool fees, supporting both F2Pool (changed_balance
+ tx_fee) and Ocean (satoshis_net_earned + fees_colected_satoshis) formats.
Aggregates by period with optional pool filter.

* feat: add GET /auth/finance/revenue-summary endpoint

Most impactful endpoint — replaces 9 API calls + ~1500 LOC of frontend
processing. Merges transaction data, BTC prices, costs, power, hashrate,
block data, and electricity data into comprehensive daily rows with EBITDA,
production cost, hash revenue, curtailment, and power utilization metrics.

* fix: include pool name in worker type for revenue endpoint

* refactor: extract shared finance utils and consolidate duplicated logic

- Create finance.utils.js with shared utilities:
  - validateStartEnd: replaces duplicated validation in every handler
  - normalizeTimestampMs: moved from finance.handlers.js
  - processTransactions: consolidates processTransactionData and
    processEbitdaTransactions (fixes missing normalizeTimestampMs and
    incorrect BTC_SATS division bugs), supports trackFees option
  - extractCurrentPrice: merges flat and nested EBITDA price formats
  - processBlockData: extracted for reuse across revenue endpoints
- Refactor finance.handlers.js to use shared utils
- Add comprehensive unit tests for all utils (24 tests)
- Update handler tests to remove tests for deleted functions

* Remove spec doc

* feat: add device listing API v2 endpoints (#25)

* feat: add device listing API v2 endpoints

* fix: paginate listThings RPC and add field projections for device endpoints

Address PR review: use requestRpcMapAllPages to fetch all items beyond
the default 100-item RPC limit, and pass DEVICE_LIST_FIELDS projection
to reduce response size when status=1.

* fix: use requestRpcMapLimit with limit/offset and remove status

- Switch getMiners/getContainers from requestRpcMapAllPages to
  requestRpcMapLimit, passing limit/offset to RPC instead of
  fetching all objects
- Remove status:1 from all listThings calls as DEVICE_LIST_FIELDS
  don't have snap properties
- Update pagination test to verify RPC payload

* Add maximium to limit

* Remove /auth/miners

* Feat/add get configs route (#26)

* Add get configs route

* Add tests and lint

* Remove old pool manager apis (#29)

* feat: add GET /auth/finance/hash-revenue endpoint (#27)

* non rpc method calls to ork (#28)

* fix: address OWASP security review findings for v2 API endpoints (#30)

* ci: enforce coverage (#31)

* feat: pool config stats (#32)

* feat: pool config stats

* var name typo fix

* feat: add GET /auth/miners with getThingsCount (#33)

* fix: config routes read permissions fix (#34)

* ci: improve coverage summary (NYC table) (#35)

* feat: containers pool stats (#36)

* fix: add schema bounds to prevent negative limit/offset bypass (#37)

* ci: move away from pull_request_target (#38)

* feat: add groups stats endpoint and racks filter for metrics (#47)

* feat: add groups stats endpoint and racks filter for metrics

Add GET /auth/groups/stats endpoint for live rack-group aggregation
(efficiency, hashrate, power, miner counts). Extend hashrate and
consumption metrics with optional racks query parameter for filtering
by rack groups via ORK RPC.

* feat: add groups stats endpoint and racks filter for metrics

Add GET /auth/groups/stats endpoint for live rack-group aggregation
(efficiency, hashrate, power, miner counts). Extend hashrate and
consumption metrics with optional racks query parameter for filtering
by rack groups via ORK RPC.

* fix: rename racks filter to containers for metrics endpoints

* refactor: deduplicate parseContainers, extractKeyEntry and use AGGR_FIELDS constants

* fix: remove non-functional containers filter from hashrate/consumption

tailLogCustomRangeAggr RPC method does not support containers param,
so remove the dead code from getHashrate/getConsumption handlers,
their schemas, route cache keys, and related tests.

* fix: handle noAuth mode in capCheck

capCheck crashes in noAuth mode when routes pass permissions because
ctx.authLib is undefined. Add early return guard matching authCheck.

* Revert "fix: handle noAuth mode in capCheck"

This reverts commit 2d4d992.

* test: improve branch coverage for metrics handlers

Add tests for uncovered branches: temperature rolling avg,
container history prefix match, non-object guard branches,
and groupRange config paths in getPowerMode/getTemperature.

* chore: remove unnecessary comment

* Add new auth/cooling system api (#51)

* Add new auth/cooling system api

* Fix review comments, make the tag generic for dcs device and update endpoint path

* Fix

* remove device name

* fix

* fix: sanitize queryActions input to prevent injection and DoS (#49)

* fix: sanitize queryActions input to prevent injection and DoS (TDEBT-38)

Add maxLength constraints to suffix (200) and queries (10000) in route
schemas, validate queries array type and cap length at 50 in handler.

* fix: tighten queryActions limits and extract to constants

Reduce queries string maxLength from 10000 to 1000 and cap the array at
10 items (previously 50). Extract both limits to named constants.

* Add energy views endpoint support (#52)

* Add energy views endpoint support

* move outside to dir with other utils

* fix imports

* feat: microsoft oauth (#54)

* Support site overview endpoints for dcs and miner group wise overview (#55)

* Support site overview endpoints for dcs and miner group wise overview

* update move to site handlers

* Support new dcs device tags update and fix cooling system apis (#56)

* Support new dcs device tags update and fix cooling system apis

* update the attributes for rack group

* Tests

* tests

* Add hvac circuit 1 and layout api updates (#58)

* Add hvac circuit 1 and layout api updates

* Update hvac circuit2 api

* Add site effciency api (#57)

* Add site effciency api

* Add site effciency api routes

* tests

* Add explorer racks api (#59)

* (improvement) Explorer handlers, Racks list, Search filtering flow improved (#60)

* improv: workers/lib/server/handlers - explorer - 'filterBySearch' flow improved.

* improv: workers/lib/server/handlers - explorer - 'filterBySearch' flow & usage improved.

* fix: workers/lib/server/handlers - explorer - 'filterBySearch' flow fixed.

* test: tests/unit/handlers - explorer - 'filterBySearch' cases coverage extended.

* test: tests/unit/handlers - coolingSystem - 'createMockConfig' result generation fixed.

* feat: group hashrate by miner and container (#62)

* fix: finance v2 handler RPC regressions (#67)

* fix: processBlockData parses HISTORICAL_BLOCKSIZES flat-array shape

The mempool worker's getWrkExtData returns a flat array of block records
[{ts, blockSize, blockReward, blockTotalFees}, ...], but processBlockData
only handled nested .data/.blocks wrappers - it iterated entry's own field
names, Number('ts') returned NaN, and produced an empty daily map. The
SubsidyFee page then rendered "No data available".

Detect the flat shape (entry has .ts/.timestamp/.time) and process it
directly. Also surface blockSize on getSubsidyFees and getRevenueSummary
log entries (and totalBlockSize/avgBlockSize on the summary) - needed for
SubsidyFee's "Avg Fees in Sats/vByte" chart to render after this fix.

* fix: cost-summary propagates per-day btcPrice

Two compounding bugs in getCostSummary caused btcPrice=0 for every log
entry, leaving ProductionCostPriceChart's overlay line at 0:

1. Wrong RPC key. The handler queried mempool with key: 'prices', which
   the worker doesn't recognize and so returns the live snapshot
   ({currentPrice, blockHeight, ...}). Switched to 'HISTORICAL_PRICES',
   which returns [{ts, priceUSD}, ...].

2. processEbitdaPrices couldn't parse the production shape. Same class
   as the processBlockData fix (Bug C): the mempool worker returns a flat
   per-ORK array of records, not a wrapper. Detect when entry has
   .ts/.timestamp/.time and process it as the item directly; also accept
   priceUSD (the actual upstream field name) alongside price.

The processEbitdaPrices fix incidentally makes getRevenueSummary and
getHashRevenue use real per-day prices instead of falling back to
currentBtcPrice. getEbitda still uses the wrong key: 'prices' (line 344)
and is left for a follow-up.

* fix: processTailLogData drills into .val for nested TAIL_LOG_RANGE_AGGR

The TAIL_LOG_RANGE_AGGR RPC returns per-day items as
{ts, val: {site_power_w, hashrate_mhs_5m_sum_aggr}}, but
processTailLogData read item[AGGR_FIELDS.SITE_POWER] directly without
drilling into .val, so powerW and hashrateMhs were 0 for every entry.
RevenueSummary then showed 0 for Avg Power, Avg Hashrate, and Hashrate
Capacity Factor; EBITDA's per-day power/hashrate were similarly zeroed.

Mirror the .val/.flat fallback pattern that processConsumptionData
already uses (workers/lib/server/handlers/finance.handlers.js:172).

* fix: hash-revenue hashrate and network-hashrate populate from real RPC

Two bugs in getHashRevenue caused hashrateMhs and networkHashrateMhs
to be 0 for every log entry, leaving HashBalance entirely zeroed:

1. processHashrateData read item[AGGR_FIELDS.HASHRATE_SUM] directly,
   but TAIL_LOG_RANGE_AGGR wraps measurements as
   {ts, val: {hashrate_mhs_5m_sum_aggr}}. Same .val drilling fix as
   processTailLogData.

2. processNetworkHashrateData expected entries wrapped under .data,
   but the HISTORICAL_HASHRATE RPC returns a flat per-ORK array of
   {ts, avgHashrateMHs} records. Same flat-shape detection as
   processBlockData (Bug C).

* Fix memory leak in authCheck by implementing LRU caching and removing in-memory cache (#61)

* Support summary for grouped by in hashrate metrics api (#68)

* Support summary for grouped by in hashrate metrics api

* update groupKey

* fix: groups stats accepts rack ids and returns rack objects (#69)

* Add groupBy for metrics consumption api (#70)

* Update the cooling system handlers to fix flow readings (#71)

* fix(finance): correct energy-balance period aggregation (#72)

The shared aggregateByPeriod summed every numeric field, which is
correct for totals but produced nonsense for rates/means (curtailment
rate, btcPrice, sitePowerMW, etc.) on weekly/monthly buckets.

- Add an optional `meanKeys` arg to aggregateByPeriod that averages
  listed fields instead of summing. Default behavior is unchanged for
  the 6 existing callers.
- getEnergyBalance now passes meanKeys for sitePowerMW, btcPrice and
  the rate fields, then post-processes to recompute energyRevenue*_MW
  against the (correctly meaned) sitePowerMW denominator.
- Add sitePowerMW + energyRevenueBTC_MW/energyRevenueUSD_MW to log
  entries; add avgPowerConsumption + avgEnergyCostPerMWh +
  avgOperationalCostPerMWh to summary.
- Schema: add 'weekly' to the period enum.

* chore: facs version update (#73)

* Update the dcs apis to support the new tags (#74)

* develop sync (#76)

* feat: adopt JWT-based svc-facs-auth (#53)

* feat: forecast endpoints (#80)

* feat: add rack grouping to metrics/hashrate endpoint (#82)

Extend GET /auth/metrics/hashrate with `groupBy=rack` and an optional
`racks` CSV filter so charts can render a per-rack hashrate time series
for a specific set of racks.

- groupBy=rack reads the hashrate_mhs_5m_pdu_rack_group_sum aggregation
  from stat-1D, the same obj_concat aggregation family already used for
  type/container group grouping
- racks=group-1_rack-1,group-2_rack-1 narrows each log entry's hashrate
  object and the summary breakdown to the requested racks
- racks is cache-keyed and only applied when groupBy=rack

* feat: add rack grouping to metrics/consumption endpoint (#83)

Extend GET /auth/metrics/consumption with `groupBy=rack` and an optional
`racks` CSV filter, mirroring the rack grouping added to metrics/hashrate
in #82.

- groupBy=rack reads the power_w_pdu_rack_group_sum aggregation from
  stat-1D, the same obj_concat aggregation family already used for
  type/container group grouping
- racks=group-1_rack-1,group-2_rack-1 narrows each log entry's powerW
  object (and the derived consumptionMWh) and the summary to those racks
- racks is cache-keyed and only applied when groupBy=rack

* feat: work order + spare parts HTTP API (#79)

* feat: expose Work Order HTTP API + RBAC

Adds the Work Order HTTP surface as thin wrappers over the existing
things RPC plumbing:
  POST   /auth/work-orders                create  (registerThing)
  GET    /auth/work-orders                list    (listThings + type filter)
  GET    /auth/work-orders/:id            get     (listThings + id+type filter)
  PATCH  /auth/work-orders/:id            update  (updateThing)
  POST   /auth/work-orders/:id/close      close   (updateThing status=closed)
  POST   /auth/work-orders/:id/cancel     cancel  (updateThing status=cancelled)
  POST   /auth/work-orders/:id/assign     assign  (updateThing assignedTo)
  GET    /auth/work-orders/:id/audit      history (getHistoricalLogs)

Introduces a new RBAC resource work_order (r/rw) granted to
operator/admin/repair_technician roles for write and to viewer
roles for read. Strips the duplicated [HRPC_ERR]= prefix and
bubbles structured rack errors verbatim. Skips capCheck when the
gateway runs with --noauth so local curl-testing works through
permissioned routes.

* feat: searchable Work Order list with pagination envelope

Adds filter shortcuts (q, assignee, creator, partId, status, type,
from, to) to GET /auth/work-orders alongside the existing mingo
?query passthrough; q is a case-insensitive regex $or against code
and info.issue, the rest map to info.* paths and run on the rack
the same way every other list does. Pages now return the
{data, totalCount, offset, limit, hasMore} envelope by calling
listThings and getThingsCount in parallel, and every shortcut is
wired into the cache key so cached pages do not bleed across
filter combinations.

* feat: spare-part PUT gated by an open Work Order + repair history

Adds PUT /auth/spare-parts/:id which validates the supplied
workOrderId resolves to an open or in_progress WO, pushes the
underlying updateThing on the part rack with workOrderId injected
into info, and appends a denormalised entry to the WO's
partsMoves[] so the audit trail stays on the Work Order itself.
GET /auth/spare-parts/:id/repair-history mingo-queries every WO
that ever touched the part and returns each move row hydrated with
workOrderCode in the standard {data, totalCount, offset, limit,
hasMore} envelope.

* feat: Work Order log entries + file upload/download/delete routes

Adds POST /auth/work-orders/:id/log which forwards to the existing
saveThingComment RPC so the WO comments[] doubles as the work log.
Adds POST/GET/DELETE /auth/work-orders/:id/files routes backed by
@fastify/multipart on the gateway and the new
storeWorkOrderFile/loadWorkOrderFile/removeWorkOrderFile pass-
throughs on the ork; uploads validate mime + size, append metadata
to WO.info.files via pushAction, and reject mutations on closed or
cancelled WOs.

* feat: spare-part register flow + Type-1/Type-2 Work Order dispatch

POST /auth/spare-parts now creates the part and a paired Type-1 WO in
one call, returning {partId, workOrderId, workOrderCode}. POST
/auth/work-orders resolves the target part for both flows from
deviceIdentifier (id, code, serialNum, or macAddress) and rejects
unknown deviceTypes with ERR_INVALID_DEVICE_TYPE / unresolved parts
with ERR_PART_NOT_FOUND.

* feat: accept warranty payload on Work Order create/update

Adds an open `warranty: { vendor, fields }` shape to the create and
update body schemas; vendor-specific field validation lives in the
inventory worker so app-node stays agnostic of vendor differences.

* feat: GET /auth/work-orders/:id/export with PDF + CSV output

Export accepts either the WO uuid or its IVI-* code in the path and a
required ?format=pdf|csv|docx querystring. CSV flattens one row per
parts-movement entry (WO header columns repeated). PDF is server-rendered
via PDFKit and includes header, triage, work log, parts movements, file
references, and a vendor-specific warranty section. format=docx returns
501 with ERR_EXPORT_FORMAT_NOT_IMPLEMENTED until phase 2.

* refactor: ship CSV only; pdf + docx return 501 (deferred to FE/phase 2)

Drops the pdfkit dependency and the PDF renderer. The export endpoint
now serves CSV directly; pdf and docx both 501 with
ERR_EXPORT_FORMAT_NOT_IMPLEMENTED, keeping the route shape stable for
when the frontend renders PDF client-side.

* feat: align SPARE_PART_INITIAL_LOCATION to canonical 'Site Warehouse'

Mirrors the new MINER_LOCATIONS enum from the inventory worker.

* refactor: camelCase MINER_LOCATIONS values + SPARE_PART_INITIAL_LOCATION

Mirrors the inventory worker constant.

* feat: GET /auth/spare-parts with mingo-side location / status / q filters

Querystring shortcuts (location, status, q) translate to a mingo query
forwarded to the rack via listThings + getThingsCount — no app-node
post-filtering. q is a case-insensitive regex against code, info.serialNum,
and info.macAddress. WO things are excluded by default via type:{\$ne:...},
overridable by an explicit query.type. Cache key includes every filter
shortcut so combinations don't collide.

* refactor: extract escapeRegex, listThingsWithCount, stableJsonString to utils

Both list handlers (work-orders + spare-parts) hand-rolled the same
regex-escape + listThings/getThingsCount/pagination skeleton; collapses
to a single utils helper used by both. Cache keys for the two GET list
routes now canonicalize their JSON inputs via stableJsonString so
semantically-equal queries share a cache slot instead of missing each
other on key-order differences.

* refactor: drop waitForThing — registerSparePart returns action ids, no polling

App-node was polling listThings after each pushAction to surface the
rack-assigned thing code in the response — exactly the
'submit-and-poll-in-app-node' anti-pattern the codebase had already
rejected. registerSparePart now pre-generates partId/woId, fires both
registerThing pushActions in parallel, and returns
{partId, workOrderId, partActionId, workOrderActionId, errors}. Clients
read the eventually-assigned codes from GET /auth/actions/done/:id like
every other write in the app.

Removes waitForThing + sleep from utils and the two timeout constants
that only existed to tune it.

* refactor: address peer-review #3 #6 #8 #10 #12 #20

#3 (atomicity) — updateSparePart now checks the part pushAction's
   errors[] before firing the WO partsMoves append. If the part write
   was rejected at push time, throws ERR_PART_UPDATE_PUSH_FAILED:<msg>
   with a 502 instead of silently enqueueing the WO append. Response
   shape also gains partActionId / workOrderActionId / workOrderAppendErrors
   so the FE can detect a post-push WO failure and trigger a manual
   reconcile.
#6 (status casing) — WO status enum normalised to 'inProgress' so
   every info.* enum value follows the camelCase convention adopted
   for MINER_LOCATIONS.
#8 (registerSparePart field sprawl) — drops the
   info.model→info.deviceModel→'unknown' fallback chain and the
   serialNum→macAddress→partId fallback. deviceModel and serialNum
   are now both required up front with explicit ERR_*_REQUIRED errors.
#10 (action latency) — register/update responses include
   expectedActionLatencyMs sourced from ctx.conf.expectedActionLatencyMs
   (default 1000) so the FE can pace its /auth/actions/done/:id polling
   instead of guessing.
#12 (export 501) — error message now carries the requested format
   (ERR_EXPORT_FORMAT_NOT_IMPLEMENTED:pdf / :docx) so the FE can tell
   which format was rejected without re-reading the request.
#20 (pagination) — listThingsWithCount now slices the dedupe'd union
   down to the requested limit so multi-ork fan-out can't return more
   rows than the caller asked for. hasMore is now derived from
   offset + limit rather than offset + data.length. Multi-rack
   offset-pagination remains best-effort because each rack applies
   the offset locally; documented inline.

* revert: restore 'in_progress' in WO status schema enum

Mirrors the inventory rack revert — keeps existing 'in_progress' WO
records valid for transitions.

* revert: restore human-readable MINER_LOCATIONS values

Mirrors the inventory rack revert.

* refactor: call generic storeFile / loadFile / removeFile with file type

Updates the work-order file handlers to use the renamed generic file
RPCs and tags each call with FILE_TYPES.WORK_ORDER so the rack can
dispatch on it.

* fix: pass workOrderId + fileId to scoped file RPCs, surface blobCleared

loadFile / removeFile are now called with { workOrderId, fileId } so the
rack resolves the blob descriptor from the work order it belongs to —
app-node no longer forwards a raw blobRef. deleteWorkOrderFile surfaces
the rack's { cleared } result as blobCleared so the HTTP caller can tell
whether the underlying blob was actually removed.

* refactor: address PR #79 review feedback

- resolve the work_order rack from the ork rack registry via listRacks
  (getWorkOrderRackId, cached on ctx) and drop the workOrderRackId config
  key — the rack id is discovered, not deployment config
- relocate submitWorkOrderAction out of generic utils into the new
  server/lib/workOrders module alongside getWorkOrderRackId
- add WORK_ORDER_TERMINAL_STATUSES constant, replacing the duplicated
  ['closed', 'cancelled'] literals across the WO handlers
- workOrderExport: derive the CSV header and rows from one [name,
  extractor] schema instead of two hand-synced lists
- rename pushOne -> pushSingleAction in registerSparePart

* refactor: derive WO export CSV columns from json keys

renderWorkOrderCsv builds the header from the work order's own properties
— top-level code plus every info field, then each parts-movement entry's
keys — so the CSV tracks the worker response schema with no hand-kept
column list. Drops the CSV_HEADERS / WO_COLUMNS / MOVE_COLUMNS definitions.

* refactor: remove non-functional comments from WO export and rack helpers

* refactor: promote _csvEscape to a generic csvEscape util

* refactor: rename WO and spare-parts modules to dot-separated file names

Align the work order and spare parts file names with the repo convention
(cooling.system.routes.js, energy.system.routes.js, etc.): camelCase
basenames become dot-separated — spareParts.handlers.js ->
spare.parts.handlers.js, workOrders.js -> work.orders.js,
workOrderExport.js -> work.order.export.js. Updates every require() path,
route registration in index.js, and test references.

* (fix) NPM audit, Dependencies invulnerability fixed (#84)

* chore: root - package-lock - dependency tree updated.

* fix: root - package-lock - dependency tree packages invulnerability fixed.

* feat: list firmwares endpoint (#86)

* feat: miner log download via Hypercore P2P streaming (#85)

* feat: add miner log download REST endpoints and unit tests

* add LogDownloader for P2P miner log streaming

Introduces a LogDownloader class that fetches miner log files directly
from wrk-miner via Hypercore/Hyperswarm, bypassing the HRPC pipeline.
Wired up in the http worker after facilities are ready.

* trim verbose comments in log download handlers

* Fix LOGS_PEER_NOT_FOUND

* feat: forecast history endpoint (#87)

* feat: forecast settings (#89)

* feat: forecast settings

* get forecast settings endpoint

* fix: add info params + fix WO auth (#88)

* feat: add info params (notes, remarks, site, location) to work order schema

Accept optional info object with notes, remarks, site, and location
on POST /auth/work-orders and PATCH /auth/work-orders/:id. The handler
merges these into the thing info alongside the top-level fields.

* fix: auto-qualify bare permission names in tokenHasPerms

tokenHasPerms received bare resource names (e.g. 'work_order') from
route perms, but _permsMatch expects 'resource:level' format. Splitting
a bare name by ':' left `required` as undefined, crashing on spread.

* Chore/main sync (#91)

* Ci pr trigger change main (#39)

* chore: package json updates for publishing (#41)

* chore: package json updates for publishing

* update deps

* ci: package publishing (#40)

* ci: adding release pr process and version bump (#42)

* Release: v1.0.0 (#44)

* ci: release pr local jobs (#46)

* ci: persistents credentials to allow tag creation (#48)

* Release: v1.0.0 (#50)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* chore: facs version update (#77)

* refactor: logs cleanup (#78)

* ci: CI Checks — dependency review, audits, and workflow improvements (#81)

- Introduce security dependency review on pull requests
- Improve audit reporting (dependency review, npm/pnpm audit, audit-ci on core)
- Improve overall CI checks: single install + node_modules cache, lockfile gate, parallel jobs, summary

Fleet: core. Suggested PR title matches commit subject.

* reset ci updates

---------

Co-authored-by: andretetherio <andre.mendes@tether.io>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* fix: harden miner download-logs API and P2P log streaming (#94)

- LogDownloader: replicate cores onto already-open Hyperswarm connections
  (reused peer sockets get no 'connection' event), fixing 503
  ERR_LOG_PEER_NOT_FOUND on every download after the first
- accept empty JSON bodies on POST (body-less download-logs start request)
- status endpoint: include a human-readable message with worker error codes

* feat: align site/status/live with header UI logic (#93)

- source consumption by featureConfig (DCS site_main meter, transformer
  meters, or site power meter) and expose power alert/error
- fix minerpool stats parsing and convert pool hashrate to MH/s
- sum alerts across miner, powermeter and container entries
- limit tail-log query to a 10 minute freshness window
- drop derived miners.sleep, add unit metadata to response fields
- extract aggregation helpers to site.utils.js

* feat: GET /site/power-consumption history API (#96)

* Feat: Inventory v3 additions (#95)

* Feat: add ACME Container inventory location

Insert 'ACME Container' between Vendor and Scrapped in MINER_LOCATIONS
per Inventory v3 spec (Site overview locations table).

Asana: T1

* Feat: expand work order types to 1/2/3/4

Redefine WORK_ORDER_TYPES = REGISTER(1), MOVE(2), MICROBT_MINER(3),
MICROBT_NON_MINER(4) per Inventory v3 numbering. Widen the create/list
type enum to [1,2,3,4] and require 'issue' for the MicroBT repair types
(3,4) instead of the old type 2. The IVI-<type>-<seq> numbering already
supports any numeric type. Update REGULAR references (issue rule,
diagnosis parts-seed) to the MicroBT types.

Asana: T2

* Feat: seed a move parts-move for Move Devices work orders

Add the Move(2) branch to createWorkOrder: resolve the part and seed a
role:'move' parts-move capturing fromLocation (part's current location)
and toLocation (requested info.location). Register(1) and MicroBT(3,4)
seeds unchanged; the issue-required remap landed with the type change.

Asana: T4

* Feat: stamp closedAt when a work order is closed

Set info.closedAt on closeWorkOrder so the WO explorer can render a
'date closed' column from listThings (status/assignedTo/deviceType
already present). Auto-closed Register/Move WOs stamp closedAt in the
rack on creation.

Asana: T5

* Feat: add Export RMA (multi-select CSV, MicroBT Miner only)

New GET /auth/work-orders/export/rma?ids=<comma-separated> endpoint that
loads the selected WOs, keeps only MicroBT Miner (type 3) ones, and emits
a CSV with the fixed RMA columns (Ticket, Repaired type, Repaired Miner
Sn, Repaired/Replaced part SN, Repaired Analyze/Treatment, Remark, Miner
Model, Repair Date, Engineer). renderRmaCsv reuses csvEscape and reads
repaired/replacement parts from partsMoves roles. The per-WO PDF
('Export Selected') is deferred until its format is finalized.

Asana: T7

* Fix: skip capCheck under --noauth so dev/local mode can serve /auth routes

authLib is only constructed when not in noAuth mode, but createAuthOnRequest
still ran capCheck (which calls ctx.authLib.tokenHasPerms) for any route with
perms, so every /auth route 500'd under --noauth. Guard capCheck with
!ctx.noAuth. Production 'start' never sets noAuth, so permission enforcement
is unchanged there. Enables local curl verification of the work-order routes.

Not part of the inventory tickets — separate so it can be dropped/owned independently.

* Refactor: move RMA_COLUMNS to constants

Move the fixed RMA_COLUMNS list to workers/lib/constants.js and import it
in work.order.export.js. Repair-date formatting is inlined in renderRmaCsv
(single use, no shared helper). No behaviour change.

* Style: remove explanatory comments

* Refactor: align MINER_LOCATIONS with FE dotted-lowercase format

Per FE/BE alignment on PR #95: switch all location values from
Title Case display strings (e.g. 'Site Warehouse') to dotted-lowercase
keys (e.g. 'site.warehouse'). The set is the union of the spec's
locations and the FE's existing constant — 11 values total.

- MINER_LOCATIONS: workshop.warehouse, workshop.lab, site.warehouse,
  site.lab, site.container, miner.room, vendor, acme.container,
  scrapped, disposed, unknown
- SPARE_PART_INITIAL_LOCATION: 'Site Warehouse' -> 'site.warehouse'
- Update fixture strings in work-orders + spare-parts tests

NOTE: miningos-wrk-inventory needs the matching change (constants +
_validateLocation set) or the inventory worker will reject these
values with ERR_INVALID_LOCATION. Existing stored rows ('Site
Warehouse', etc.) need a one-time migration to the new keys.

* feat(alerts): filter & search alerts by device tag; fix site deviceId filter (#97)

- expose `message` (device/equipment tag) on historical alerts and whitelist it
  for filter+search on both site and history endpoints
- expose `deviceId` on site alerts so the existing deviceId filter works (alert
  objects previously only carried `id`); aligns site with history
- add a Postman collection for the alerts endpoints

* Feat: batch create endpoints for work orders and spare parts (#98)

* Feat: batch create endpoints for work orders and spare parts

* refactor: use WORK_ORDER_TYPES constants in work order schemas

Replace magic enum values [3, 4] and [1, 2, 3, 4] with the existing
WORK_ORDER_TYPES constant, addressing PR #98 review feedback.

* Feat: move work order relocates the device atomically

A type-2 (move) work order auto-closes on creation, so it must perform the relocation itself — otherwise the closed WO records a move that never happened. createWorkOrder and createWorkOrdersBatch reuse submitWorkOrderAction (now taking an optional rackId) to push an updateThing to the part's own rack before recording the WO.

Claude-Session: https://claude.ai/code/session_01Hk3GYz4TPiRmSs3Qmvv27V

* feat(cooling): redesigned Operations Centre cooling/energy data (#100)

cooling-system: explicit fan-coil valve_tag/temperature_tag map; binary cooling_towers[].vibration_switch (replaces numeric vibration); HVAC C1 control_valves.pressure_bypass.pressure (PIT-7501); miners C1 lines[].differential_pressure[] plus summary inlet/outlet/delta_p averages; mining_room.groups[].rack_statuses (online = live rack power); ambient averaging over real readings (drop only missing).

energy-system: site_pm.current.total used for the SLD aggregate.

metrics: new GET /auth/metrics/cooling historical endpoint (hourly/daily) backed by DCS stat ops; WORKER_TYPES.DCS set to the actual thing type 'dcs-siemens'.

Tests added/updated.

---------

Co-authored-by: Parag More <34959548+paragmore@users.noreply.github.com>
Co-authored-by: Caesar Mukama <mcaesar88@gmail.com>
Co-authored-by: andretetherio <andre.mendes@tether.io>
Co-authored-by: borik91 <9007515+boris91@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
paragmore added a commit that referenced this pull request Aug 3, 2026
* Feature - Site status live api (#4)

* feat: add API v2 endpoints (finance, pools, pool-stats, pool-manager) (#11)

* feat: add GET /auth/finance/energy-balance endpoint

Add energy balance API endpoint that aggregates power consumption,
pool transactions, BTC prices, and production costs into a unified
time-series response with period-based aggregation (daily/monthly/yearly).

Includes shared infrastructure: period.utils, constants for RPC methods,
worker types, aggregation fields, and utility functions (getStartOfDay,
safeDiv, runParallel).

* feat: add GET /auth/pools/:pool/balance-history endpoint

Add pool balance history API endpoint that fetches pool balance
snapshots via tailLog RPC, groups them into time buckets (1D/1W/1M),
and returns time-series data with balance and revenue per bucket.

* tiny improvements

* feat: add GET /auth/finance/ebitda endpoint

Add EBITDA API endpoint that aggregates pool transactions, power/hashrate
data, BTC prices, production costs, and block data to calculate selling
and HODL EBITDA metrics with period-based aggregation.

Includes shared infrastructure: period.utils, constants, and utility
functions required for finance endpoints.

* feat: add GET /auth/finance/cost-summary endpoint

Add cost summary API endpoint that aggregates production costs, BTC
prices, and power consumption to calculate energy and operational cost
breakdowns with period-based aggregation.

* feat: add GET /auth/pools endpoint

Add pools API endpoint that aggregates pool device lists and stats
from ORK clusters, supports mingo-based filtering, sorting, and
field selection, and returns pool summary with totals.

* feat: add GET /auth/pool-stats/aggregate endpoint

Add pool stats aggregate API endpoint that fetches pool stats history
and transactions, processes daily stats and revenue, and aggregates
by period (daily/weekly/monthly) with optional pool filtering.

* Fix costs bugs

* feat: add curtailment, operational issues, and power utilization to energy-balance

- Add ELECTRICITY worker type, ENERGY_AGGR/ACTIVE_ENERGY_IN/UTE_ENERGY aggr fields,
  and GLOBAL_CONFIG RPC method to constants
- Add 3 new parallel RPC calls: electricity stats-history (active_energy_in,
  ute_energy) and getGlobalConfig (nominalPowerAvailability_MW)
- Calculate curtailmentMWh, curtailmentRate, operationalIssuesRate, and
  powerUtilization per log entry
- Add avgCurtailmentRate, avgOperationalIssuesRate, avgPowerUtilization to summary
- Fix getProductionCosts to use ctx.globalDataLib instead of direct Hyperbee access
- Fix processCostsData to return daily costs (energyCostPerDay, operationalCostPerDay)
- Update tests to match new cost format and globalDataLib mock

* feat: remove block data, fix costs processing, add site param to ebitda

- Remove block data RPC call and processBlockData (not in spec)
- Remove blockCount, difficulty, ebitdaMarginSelling, ebitdaMarginHodl from log
- Fix processCostsData to handle flat objects from globalDataLib with daily costs
- Replace direct globalDataLib.getGlobalData range query with getProductionCosts helper
- Add site query param to schema, cache key, and handler for site-specific costs

* feat: fix costs processing and add site param to cost-summary

- Fix processCostsData to handle flat objects from globalDataLib with daily costs
- Replace direct globalDataLib range query with getProductionCosts helper
- Add site query param to schema, cache key, and handler for site-specific costs
- Update tests to match new cost format

* feat: add hashrate and remove snapshotCount from balance-history log entries

Align pool balance-history endpoint with API v2 spec which requires
balance, hashrate, and revenue time-series data.

* feat: remove snapshotCount from pool-stats-aggregate log entries

Align pool stats aggregate endpoint with API v2 spec which does not
include snapshotCount in the response.

* fix: use ext-data transactions instead of tail-log for pool stats aggregate

tail-log returns empty for type=minerpool. Switch to ext-data with
transactions key which has daily revenue and hashrate data. Also fix
changed_balance handling (values are BTC, not satoshis).

* fix: use ext-data transactions instead of tail-log for balance history

tail-log returns empty for type=minerpool. Switch to ext-data with
transactions key which provides daily revenue (changed_balance) and
hashrate (mining_extra.hash_rate). Also treat 'all' pool param as
no filter.

* fix: use ext-data stats array instead of list-things for pools endpoint

list-things for t-minerpool returns empty; pool data lives in ext-data
stats responses. Replaced flattenResults/flattenStatsResults/mergePoolData
with flattenPoolStats that parses the actual stats array format.

* code cleanup

* cleanup

* cleanup

* Ports pool manager from moria

* Removes regions concept to align with site-focused nature of API v2

* chore: remove comments from pool manager files

* Remove unncecessaey sute param and add integration tests

* removes site

* fix: remove site param from ebitda endpoint and add integration test

Data is fetched for a single site, so the site parameter is unnecessary.
Added security integration test for finance/ebitda endpoint.

* fix: remove site param from cost-summary endpoint and add integration test

Data is fetched for a single site, so the site parameter is unnecessary.
Added security integration test for finance/cost-summary endpoint.

* fix: rename filter to query param and use mingo projection for pools

Align with list-things pattern by renaming filter→query. Replace manual
sort and field filtering with mingo cursor methods for consistency.

* fix: move pool filter to RPC payload for balance-history endpoint

Pool name filter should be handled by the RPC worker, not filtered
client-side. The tx username is the account id.

* fix: move pool filter to RPC payload for pool-stats-aggregate endpoint

Pool name filter should be handled by the RPC worker, not filtered
client-side. The tx username is the account id.

* Address PR comments

* fix failing tests

* Removes tags from projections

* Fix integration tests

* requestRpcMapAllPages to fetch all pages from orks

* fix linting

* Fix failing tests

* fix: Improves user and roles ux (#12)

* return the created user

* fix: require users:r permission for GET /auth/roles/permissions

* fix: update getRolesPermissions route to require users:w permission

* fix: enhance getRolesPermissions to filter roles based on user role

* fix: update getRolesPermissions to exclude roleManagement for super admin

* Cleanup

* feat: add GET /auth/finance/subsidy-fees endpoint (#14)

* feat: add GET /auth/finance/subsidy-fees endpoint

Port Bitcoin network block data (subsidy rewards + transaction fees) from
legacy dashboard to API v2. Fetches HISTORICAL_BLOCKSIZES from mempool
worker, aggregates by period (daily/weekly/monthly).

* fix: remove unused site parameter from getProductionCosts

* Adding CI (#23)

* feat: add alerts API v2 endpoints (#24)

* resolve conflicts

* Feature: Alerts

* refactor: use RPC constants and explicit field mapping in alerts handlers

* Fix code smells

* fix failing tests

* Remove metrics

* Address comments

* feat: add metrics API v2 endpoints (#21)

* feat: add finance revenue and revenue-summary endpoints (#22)

* feat: add GET /auth/finance/revenue endpoint

Port pool transaction revenue data from legacy dashboard to API v2.
Separates revenue from pool fees, supporting both F2Pool (changed_balance
+ tx_fee) and Ocean (satoshis_net_earned + fees_colected_satoshis) formats.
Aggregates by period with optional pool filter.

* feat: add GET /auth/finance/revenue-summary endpoint

Most impactful endpoint — replaces 9 API calls + ~1500 LOC of frontend
processing. Merges transaction data, BTC prices, costs, power, hashrate,
block data, and electricity data into comprehensive daily rows with EBITDA,
production cost, hash revenue, curtailment, and power utilization metrics.

* fix: include pool name in worker type for revenue endpoint

* refactor: extract shared finance utils and consolidate duplicated logic

- Create finance.utils.js with shared utilities:
  - validateStartEnd: replaces duplicated validation in every handler
  - normalizeTimestampMs: moved from finance.handlers.js
  - processTransactions: consolidates processTransactionData and
    processEbitdaTransactions (fixes missing normalizeTimestampMs and
    incorrect BTC_SATS division bugs), supports trackFees option
  - extractCurrentPrice: merges flat and nested EBITDA price formats
  - processBlockData: extracted for reuse across revenue endpoints
- Refactor finance.handlers.js to use shared utils
- Add comprehensive unit tests for all utils (24 tests)
- Update handler tests to remove tests for deleted functions

* Remove spec doc

* feat: add device listing API v2 endpoints (#25)

* feat: add device listing API v2 endpoints

* fix: paginate listThings RPC and add field projections for device endpoints

Address PR review: use requestRpcMapAllPages to fetch all items beyond
the default 100-item RPC limit, and pass DEVICE_LIST_FIELDS projection
to reduce response size when status=1.

* fix: use requestRpcMapLimit with limit/offset and remove status

- Switch getMiners/getContainers from requestRpcMapAllPages to
  requestRpcMapLimit, passing limit/offset to RPC instead of
  fetching all objects
- Remove status:1 from all listThings calls as DEVICE_LIST_FIELDS
  don't have snap properties
- Update pagination test to verify RPC payload

* Add maximium to limit

* Remove /auth/miners

* Feat/add get configs route (#26)

* Add get configs route

* Add tests and lint

* Remove old pool manager apis (#29)

* feat: add GET /auth/finance/hash-revenue endpoint (#27)

* non rpc method calls to ork (#28)

* fix: address OWASP security review findings for v2 API endpoints (#30)

* ci: enforce coverage (#31)

* feat: pool config stats (#32)

* feat: pool config stats

* var name typo fix

* feat: add GET /auth/miners with getThingsCount (#33)

* fix: config routes read permissions fix (#34)

* ci: improve coverage summary (NYC table) (#35)

* feat: containers pool stats (#36)

* fix: add schema bounds to prevent negative limit/offset bypass (#37)

* ci: move away from pull_request_target (#38)

* feat: add groups stats endpoint and racks filter for metrics (#47)

* feat: add groups stats endpoint and racks filter for metrics

Add GET /auth/groups/stats endpoint for live rack-group aggregation
(efficiency, hashrate, power, miner counts). Extend hashrate and
consumption metrics with optional racks query parameter for filtering
by rack groups via ORK RPC.

* feat: add groups stats endpoint and racks filter for metrics

Add GET /auth/groups/stats endpoint for live rack-group aggregation
(efficiency, hashrate, power, miner counts). Extend hashrate and
consumption metrics with optional racks query parameter for filtering
by rack groups via ORK RPC.

* fix: rename racks filter to containers for metrics endpoints

* refactor: deduplicate parseContainers, extractKeyEntry and use AGGR_FIELDS constants

* fix: remove non-functional containers filter from hashrate/consumption

tailLogCustomRangeAggr RPC method does not support containers param,
so remove the dead code from getHashrate/getConsumption handlers,
their schemas, route cache keys, and related tests.

* fix: handle noAuth mode in capCheck

capCheck crashes in noAuth mode when routes pass permissions because
ctx.authLib is undefined. Add early return guard matching authCheck.

* Revert "fix: handle noAuth mode in capCheck"

This reverts commit 2d4d9929545e3791a6cfe3f15263bfeb27fb0b8d.

* test: improve branch coverage for metrics handlers

Add tests for uncovered branches: temperature rolling avg,
container history prefix match, non-object guard branches,
and groupRange config paths in getPowerMode/getTemperature.

* chore: remove unnecessary comment

* Add new auth/cooling system api (#51)

* Add new auth/cooling system api

* Fix review comments, make the tag generic for dcs device and update endpoint path

* Fix

* remove device name

* fix

* fix: sanitize queryActions input to prevent injection and DoS (#49)

* fix: sanitize queryActions input to prevent injection and DoS (TDEBT-38)

Add maxLength constraints to suffix (200) and queries (10000) in route
schemas, validate queries array type and cap length at 50 in handler.

* fix: tighten queryActions limits and extract to constants

Reduce queries string maxLength from 10000 to 1000 and cap the array at
10 items (previously 50). Extract both limits to named constants.

* Add energy views endpoint support (#52)

* Add energy views endpoint support

* move outside to dir with other utils

* fix imports

* feat: microsoft oauth (#54)

* Support site overview endpoints for dcs and miner group wise overview (#55)

* Support site overview endpoints for dcs and miner group wise overview

* update move to site handlers

* Support new dcs device tags update and fix cooling system apis (#56)

* Support new dcs device tags update and fix cooling system apis

* update the attributes for rack group

* Tests

* tests

* Add hvac circuit 1 and layout api updates (#58)

* Add hvac circuit 1 and layout api updates

* Update hvac circuit2 api

* Add site effciency api (#57)

* Add site effciency api

* Add site effciency api routes

* tests

* Add explorer racks api (#59)

* (improvement) Explorer handlers, Racks list, Search filtering flow improved (#60)

* improv: workers/lib/server/handlers - explorer - 'filterBySearch' flow improved.

* improv: workers/lib/server/handlers - explorer - 'filterBySearch' flow & usage improved.

* fix: workers/lib/server/handlers - explorer - 'filterBySearch' flow fixed.

* test: tests/unit/handlers - explorer - 'filterBySearch' cases coverage extended.

* test: tests/unit/handlers - coolingSystem - 'createMockConfig' result generation fixed.

* feat: group hashrate by miner and container (#62)

* fix: finance v2 handler RPC regressions (#67)

* fix: processBlockData parses HISTORICAL_BLOCKSIZES flat-array shape

The mempool worker's getWrkExtData returns a flat array of block records
[{ts, blockSize, blockReward, blockTotalFees}, ...], but processBlockData
only handled nested .data/.blocks wrappers - it iterated entry's own field
names, Number('ts') returned NaN, and produced an empty daily map. The
SubsidyFee page then rendered "No data available".

Detect the flat shape (entry has .ts/.timestamp/.time) and process it
directly. Also surface blockSize on getSubsidyFees and getRevenueSummary
log entries (and totalBlockSize/avgBlockSize on the summary) - needed for
SubsidyFee's "Avg Fees in Sats/vByte" chart to render after this fix.

* fix: cost-summary propagates per-day btcPrice

Two compounding bugs in getCostSummary caused btcPrice=0 for every log
entry, leaving ProductionCostPriceChart's overlay line at 0:

1. Wrong RPC key. The handler queried mempool with key: 'prices', which
   the worker doesn't recognize and so returns the live snapshot
   ({currentPrice, blockHeight, ...}). Switched to 'HISTORICAL_PRICES',
   which returns [{ts, priceUSD}, ...].

2. processEbitdaPrices couldn't parse the production shape. Same class
   as the processBlockData fix (Bug C): the mempool worker returns a flat
   per-ORK array of records, not a wrapper. Detect when entry has
   .ts/.timestamp/.time and process it as the item directly; also accept
   priceUSD (the actual upstream field name) alongside price.

The processEbitdaPrices fix incidentally makes getRevenueSummary and
getHashRevenue use real per-day prices instead of falling back to
currentBtcPrice. getEbitda still uses the wrong key: 'prices' (line 344)
and is left for a follow-up.

* fix: processTailLogData drills into .val for nested TAIL_LOG_RANGE_AGGR

The TAIL_LOG_RANGE_AGGR RPC returns per-day items as
{ts, val: {site_power_w, hashrate_mhs_5m_sum_aggr}}, but
processTailLogData read item[AGGR_FIELDS.SITE_POWER] directly without
drilling into .val, so powerW and hashrateMhs were 0 for every entry.
RevenueSummary then showed 0 for Avg Power, Avg Hashrate, and Hashrate
Capacity Factor; EBITDA's per-day power/hashrate were similarly zeroed.

Mirror the .val/.flat fallback pattern that processConsumptionData
already uses (workers/lib/server/handlers/finance.handlers.js:172).

* fix: hash-revenue hashrate and network-hashrate populate from real RPC

Two bugs in getHashRevenue caused hashrateMhs and networkHashrateMhs
to be 0 for every log entry, leaving HashBalance entirely zeroed:

1. processHashrateData read item[AGGR_FIELDS.HASHRATE_SUM] directly,
   but TAIL_LOG_RANGE_AGGR wraps measurements as
   {ts, val: {hashrate_mhs_5m_sum_aggr}}. Same .val drilling fix as
   processTailLogData.

2. processNetworkHashrateData expected entries wrapped under .data,
   but the HISTORICAL_HASHRATE RPC returns a flat per-ORK array of
   {ts, avgHashrateMHs} records. Same flat-shape detection as
   processBlockData (Bug C).

* Fix memory leak in authCheck by implementing LRU caching and removing in-memory cache (#61)

* Support summary for grouped by in hashrate metrics api (#68)

* Support summary for grouped by in hashrate metrics api

* update groupKey

* fix: groups stats accepts rack ids and returns rack objects (#69)

* Add groupBy for metrics consumption api (#70)

* Update the cooling system handlers to fix flow readings (#71)

* fix(finance): correct energy-balance period aggregation (#72)

The shared aggregateByPeriod summed every numeric field, which is
correct for totals but produced nonsense for rates/means (curtailment
rate, btcPrice, sitePowerMW, etc.) on weekly/monthly buckets.

- Add an optional `meanKeys` arg to aggregateByPeriod that averages
  listed fields instead of summing. Default behavior is unchanged for
  the 6 existing callers.
- getEnergyBalance now passes meanKeys for sitePowerMW, btcPrice and
  the rate fields, then post-processes to recompute energyRevenue*_MW
  against the (correctly meaned) sitePowerMW denominator.
- Add sitePowerMW + energyRevenueBTC_MW/energyRevenueUSD_MW to log
  entries; add avgPowerConsumption + avgEnergyCostPerMWh +
  avgOperationalCostPerMWh to summary.
- Schema: add 'weekly' to the period enum.

* chore: facs version update (#73)

* Update the dcs apis to support the new tags (#74)

* develop sync (#76)

* feat: adopt JWT-based svc-facs-auth (#53)

* feat: forecast endpoints (#80)

* feat: add rack grouping to metrics/hashrate endpoint (#82)

Extend GET /auth/metrics/hashrate with `groupBy=rack` and an optional
`racks` CSV filter so charts can render a per-rack hashrate time series
for a specific set of racks.

- groupBy=rack reads the hashrate_mhs_5m_pdu_rack_group_sum aggregation
  from stat-1D, the same obj_concat aggregation family already used for
  type/container group grouping
- racks=group-1_rack-1,group-2_rack-1 narrows each log entry's hashrate
  object and the summary breakdown to the requested racks
- racks is cache-keyed and only applied when groupBy=rack

* feat: add rack grouping to metrics/consumption endpoint (#83)

Extend GET /auth/metrics/consumption with `groupBy=rack` and an optional
`racks` CSV filter, mirroring the rack grouping added to metrics/hashrate
in #82.

- groupBy=rack reads the power_w_pdu_rack_group_sum aggregation from
  stat-1D, the same obj_concat aggregation family already used for
  type/container group grouping
- racks=group-1_rack-1,group-2_rack-1 narrows each log entry's powerW
  object (and the derived consumptionMWh) and the summary to those racks
- racks is cache-keyed and only applied when groupBy=rack

* feat: work order + spare parts HTTP API (#79)

* feat: expose Work Order HTTP API + RBAC

Adds the Work Order HTTP surface as thin wrappers over the existing
things RPC plumbing:
  POST   /auth/work-orders                create  (registerThing)
  GET    /auth/work-orders                list    (listThings + type filter)
  GET    /auth/work-orders/:id            get     (listThings + id+type filter)
  PATCH  /auth/work-orders/:id            update  (updateThing)
  POST   /auth/work-orders/:id/close      close   (updateThing status=closed)
  POST   /auth/work-orders/:id/cancel     cancel  (updateThing status=cancelled)
  POST   /auth/work-orders/:id/assign     assign  (updateThing assignedTo)
  GET    /auth/work-orders/:id/audit      history (getHistoricalLogs)

Introduces a new RBAC resource work_order (r/rw) granted to
operator/admin/repair_technician roles for write and to viewer
roles for read. Strips the duplicated [HRPC_ERR]= prefix and
bubbles structured rack errors verbatim. Skips capCheck when the
gateway runs with --noauth so local curl-testing works through
permissioned routes.

* feat: searchable Work Order list with pagination envelope

Adds filter shortcuts (q, assignee, creator, partId, status, type,
from, to) to GET /auth/work-orders alongside the existing mingo
?query passthrough; q is a case-insensitive regex $or against code
and info.issue, the rest map to info.* paths and run on the rack
the same way every other list does. Pages now return the
{data, totalCount, offset, limit, hasMore} envelope by calling
listThings and getThingsCount in parallel, and every shortcut is
wired into the cache key so cached pages do not bleed across
filter combinations.

* feat: spare-part PUT gated by an open Work Order + repair history

Adds PUT /auth/spare-parts/:id which validates the supplied
workOrderId resolves to an open or in_progress WO, pushes the
underlying updateThing on the part rack with workOrderId injected
into info, and appends a denormalised entry to the WO's
partsMoves[] so the audit trail stays on the Work Order itself.
GET /auth/spare-parts/:id/repair-history mingo-queries every WO
that ever touched the part and returns each move row hydrated with
workOrderCode in the standard {data, totalCount, offset, limit,
hasMore} envelope.

* feat: Work Order log entries + file upload/download/delete routes

Adds POST /auth/work-orders/:id/log which forwards to the existing
saveThingComment RPC so the WO comments[] doubles as the work log.
Adds POST/GET/DELETE /auth/work-orders/:id/files routes backed by
@fastify/multipart on the gateway and the new
storeWorkOrderFile/loadWorkOrderFile/removeWorkOrderFile pass-
throughs on the ork; uploads validate mime + size, append metadata
to WO.info.files via pushAction, and reject mutations on closed or
cancelled WOs.

* feat: spare-part register flow + Type-1/Type-2 Work Order dispatch

POST /auth/spare-parts now creates the part and a paired Type-1 WO in
one call, returning {partId, workOrderId, workOrderCode}. POST
/auth/work-orders resolves the target part for both flows from
deviceIdentifier (id, code, serialNum, or macAddress) and rejects
unknown deviceTypes with ERR_INVALID_DEVICE_TYPE / unresolved parts
with ERR_PART_NOT_FOUND.

* feat: accept warranty payload on Work Order create/update

Adds an open `warranty: { vendor, fields }` shape to the create and
update body schemas; vendor-specific field validation lives in the
inventory worker so app-node stays agnostic of vendor differences.

* feat: GET /auth/work-orders/:id/export with PDF + CSV output

Export accepts either the WO uuid or its IVI-* code in the path and a
required ?format=pdf|csv|docx querystring. CSV flattens one row per
parts-movement entry (WO header columns repeated). PDF is server-rendered
via PDFKit and includes header, triage, work log, parts movements, file
references, and a vendor-specific warranty section. format=docx returns
501 with ERR_EXPORT_FORMAT_NOT_IMPLEMENTED until phase 2.

* refactor: ship CSV only; pdf + docx return 501 (deferred to FE/phase 2)

Drops the pdfkit dependency and the PDF renderer. The export endpoint
now serves CSV directly; pdf and docx both 501 with
ERR_EXPORT_FORMAT_NOT_IMPLEMENTED, keeping the route shape stable for
when the frontend renders PDF client-side.

* feat: align SPARE_PART_INITIAL_LOCATION to canonical 'Site Warehouse'

Mirrors the new MINER_LOCATIONS enum from the inventory worker.

* refactor: camelCase MINER_LOCATIONS values + SPARE_PART_INITIAL_LOCATION

Mirrors the inventory worker constant.

* feat: GET /auth/spare-parts with mingo-side location / status / q filters

Querystring shortcuts (location, status, q) translate to a mingo query
forwarded to the rack via listThings + getThingsCount — no app-node
post-filtering. q is a case-insensitive regex against code, info.serialNum,
and info.macAddress. WO things are excluded by default via type:{\$ne:...},
overridable by an explicit query.type. Cache key includes every filter
shortcut so combinations don't collide.

* refactor: extract escapeRegex, listThingsWithCount, stableJsonString to utils

Both list handlers (work-orders + spare-parts) hand-rolled the same
regex-escape + listThings/getThingsCount/pagination skeleton; collapses
to a single utils helper used by both. Cache keys for the two GET list
routes now canonicalize their JSON inputs via stableJsonString so
semantically-equal queries share a cache slot instead of missing each
other on key-order differences.

* refactor: drop waitForThing — registerSparePart returns action ids, no polling

App-node was polling listThings after each pushAction to surface the
rack-assigned thing code in the response — exactly the
'submit-and-poll-in-app-node' anti-pattern the codebase had already
rejected. registerSparePart now pre-generates partId/woId, fires both
registerThing pushActions in parallel, and returns
{partId, workOrderId, partActionId, workOrderActionId, errors}. Clients
read the eventually-assigned codes from GET /auth/actions/done/:id like
every other write in the app.

Removes waitForThing + sleep from utils and the two timeout constants
that only existed to tune it.

* refactor: address peer-review #3 #6 #8 #10 #12 #20

#3 (atomicity) — updateSparePart now checks the part pushAction's
   errors[] before firing the WO partsMoves append. If the part write
   was rejected at push time, throws ERR_PART_UPDATE_PUSH_FAILED:<msg>
   with a 502 instead of silently enqueueing the WO append. Response
   shape also gains partActionId / workOrderActionId / workOrderAppendErrors
   so the FE can detect a post-push WO failure and trigger a manual
   reconcile.
#6 (status casing) — WO status enum normalised to 'inProgress' so
   every info.* enum value follows the camelCase convention adopted
   for MINER_LOCATIONS.
#8 (registerSparePart field sprawl) — drops the
   info.model→info.deviceModel→'unknown' fallback chain and the
   serialNum→macAddress→partId fallback. deviceModel and serialNum
   are now both required up front with explicit ERR_*_REQUIRED errors.
#10 (action latency) — register/update responses include
   expectedActionLatencyMs sourced from ctx.conf.expectedActionLatencyMs
   (default 1000) so the FE can pace its /auth/actions/done/:id polling
   instead of guessing.
#12 (export 501) — error message now carries the requested format
   (ERR_EXPORT_FORMAT_NOT_IMPLEMENTED:pdf / :docx) so the FE can tell
   which format was rejected without re-reading the request.
#20 (pagination) — listThingsWithCount now slices the dedupe'd union
   down to the requested limit so multi-ork fan-out can't return more
   rows than the caller asked for. hasMore is now derived from
   offset + limit rather than offset + data.length. Multi-rack
   offset-pagination remains best-effort because each rack applies
   the offset locally; documented inline.

* revert: restore 'in_progress' in WO status schema enum

Mirrors the inventory rack revert — keeps existing 'in_progress' WO
records valid for transitions.

* revert: restore human-readable MINER_LOCATIONS values

Mirrors the inventory rack revert.

* refactor: call generic storeFile / loadFile / removeFile with file type

Updates the work-order file handlers to use the renamed generic file
RPCs and tags each call with FILE_TYPES.WORK_ORDER so the rack can
dispatch on it.

* fix: pass workOrderId + fileId to scoped file RPCs, surface blobCleared

loadFile / removeFile are now called with { workOrderId, fileId } so the
rack resolves the blob descriptor from the work order it belongs to —
app-node no longer forwards a raw blobRef. deleteWorkOrderFile surfaces
the rack's { cleared } result as blobCleared so the HTTP caller can tell
whether the underlying blob was actually removed.

* refactor: address PR #79 review feedback

- resolve the work_order rack from the ork rack registry via listRacks
  (getWorkOrderRackId, cached on ctx) and drop the workOrderRackId config
  key — the rack id is discovered, not deployment config
- relocate submitWorkOrderAction out of generic utils into the new
  server/lib/workOrders module alongside getWorkOrderRackId
- add WORK_ORDER_TERMINAL_STATUSES constant, replacing the duplicated
  ['closed', 'cancelled'] literals across the WO handlers
- workOrderExport: derive the CSV header and rows from one [name,
  extractor] schema instead of two hand-synced lists
- rename pushOne -> pushSingleAction in registerSparePart

* refactor: derive WO export CSV columns from json keys

renderWorkOrderCsv builds the header from the work order's own properties
— top-level code plus every info field, then each parts-movement entry's
keys — so the CSV tracks the worker response schema with no hand-kept
column list. Drops the CSV_HEADERS / WO_COLUMNS / MOVE_COLUMNS definitions.

* refactor: remove non-functional comments from WO export and rack helpers

* refactor: promote _csvEscape to a generic csvEscape util

* refactor: rename WO and spare-parts modules to dot-separated file names

Align the work order and spare parts file names with the repo convention
(cooling.system.routes.js, energy.system.routes.js, etc.): camelCase
basenames become dot-separated — spareParts.handlers.js ->
spare.parts.handlers.js, workOrders.js -> work.orders.js,
workOrderExport.js -> work.order.export.js. Updates every require() path,
route registration in index.js, and test references.

* (fix) NPM audit, Dependencies invulnerability fixed (#84)

* chore: root - package-lock - dependency tree updated.

* fix: root - package-lock - dependency tree packages invulnerability fixed.

* feat: list firmwares endpoint (#86)

* feat: miner log download via Hypercore P2P streaming (#85)

* feat: add miner log download REST endpoints and unit tests

* add LogDownloader for P2P miner log streaming

Introduces a LogDownloader class that fetches miner log files directly
from wrk-miner via Hypercore/Hyperswarm, bypassing the HRPC pipeline.
Wired up in the http worker after facilities are ready.

* trim verbose comments in log download handlers

* Fix LOGS_PEER_NOT_FOUND

* feat: forecast history endpoint (#87)

* feat: forecast settings (#89)

* feat: forecast settings

* get forecast settings endpoint

* fix: add info params + fix WO auth (#88)

* feat: add info params (notes, remarks, site, location) to work order schema

Accept optional info object with notes, remarks, site, and location
on POST /auth/work-orders and PATCH /auth/work-orders/:id. The handler
merges these into the thing info alongside the top-level fields.

* fix: auto-qualify bare permission names in tokenHasPerms

tokenHasPerms received bare resource names (e.g. 'work_order') from
route perms, but _permsMatch expects 'resource:level' format. Splitting
a bare name by ':' left `required` as undefined, crashing on spread.

* Chore/main sync (#91)

* Ci pr trigger change main (#39)

* chore: package json updates for publishing (#41)

* chore: package json updates for publishing

* update deps

* ci: package publishing (#40)

* ci: adding release pr process and version bump (#42)

* Release: v1.0.0 (#44)

* ci: release pr local jobs (#46)

* ci: persistents credentials to allow tag creation (#48)

* Release: v1.0.0 (#50)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* chore: facs version update (#77)

* refactor: logs cleanup (#78)

* ci: CI Checks — dependency review, audits, and workflow improvements (#81)

- Introduce security dependency review on pull requests
- Improve audit reporting (dependency review, npm/pnpm audit, audit-ci on core)
- Improve overall CI checks: single install + node_modules cache, lockfile gate, parallel jobs, summary

Fleet: core. Suggested PR title matches commit subject.

* reset ci updates

---------

Co-authored-by: andretetherio <andre.mendes@tether.io>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* fix: harden miner download-logs API and P2P log streaming (#94)

- LogDownloader: replicate cores onto already-open Hyperswarm connections
  (reused peer sockets get no 'connection' event), fixing 503
  ERR_LOG_PEER_NOT_FOUND on every download after the first
- accept empty JSON bodies on POST (body-less download-logs start request)
- status endpoint: include a human-readable message with worker error codes

* feat: align site/status/live with header UI logic (#93)

- source consumption by featureConfig (DCS site_main meter, transformer
  meters, or site power meter) and expose power alert/error
- fix minerpool stats parsing and convert pool hashrate to MH/s
- sum alerts across miner, powermeter and container entries
- limit tail-log query to a 10 minute freshness window
- drop derived miners.sleep, add unit metadata to response fields
- extract aggregation helpers to site.utils.js

* feat: GET /site/power-consumption history API (#96)

* Feat: Inventory v3 additions (#95)

* Feat: add ACME Container inventory location

Insert 'ACME Container' between Vendor and Scrapped in MINER_LOCATIONS
per Inventory v3 spec (Site overview locations table).

Asana: T1

* Feat: expand work order types to 1/2/3/4

Redefine WORK_ORDER_TYPES = REGISTER(1), MOVE(2), MICROBT_MINER(3),
MICROBT_NON_MINER(4) per Inventory v3 numbering. Widen the create/list
type enum to [1,2,3,4] and require 'issue' for the MicroBT repair types
(3,4) instead of the old type 2. The IVI-<type>-<seq> numbering already
supports any numeric type. Update REGULAR references (issue rule,
diagnosis parts-seed) to the MicroBT types.

Asana: T2

* Feat: seed a move parts-move for Move Devices work orders

Add the Move(2) branch to createWorkOrder: resolve the part and seed a
role:'move' parts-move capturing fromLocation (part's current location)
and toLocation (requested info.location). Register(1) and MicroBT(3,4)
seeds unchanged; the issue-required remap landed with the type change.

Asana: T4

* Feat: stamp closedAt when a work order is closed

Set info.closedAt on closeWorkOrder so the WO explorer can render a
'date closed' column from listThings (status/assignedTo/deviceType
already present). Auto-closed Register/Move WOs stamp closedAt in the
rack on creation.

Asana: T5

* Feat: add Export RMA (multi-select CSV, MicroBT Miner only)

New GET /auth/work-orders/export/rma?ids=<comma-separated> endpoint that
loads the selected WOs, keeps only MicroBT Miner (type 3) ones, and emits
a CSV with the fixed RMA columns (Ticket, Repaired type, Repaired Miner
Sn, Repaired/Replaced part SN, Repaired Analyze/Treatment, Remark, Miner
Model, Repair Date, Engineer). renderRmaCsv reuses csvEscape and reads
repaired/replacement parts from partsMoves roles. The per-WO PDF
('Export Selected') is deferred until its format is finalized.

Asana: T7

* Fix: skip capCheck under --noauth so dev/local mode can serve /auth routes

authLib is only constructed when not in noAuth mode, but createAuthOnRequest
still ran capCheck (which calls ctx.authLib.tokenHasPerms) for any route with
perms, so every /auth route 500'd under --noauth. Guard capCheck with
!ctx.noAuth. Production 'start' never sets noAuth, so permission enforcement
is unchanged there. Enables local curl verification of the work-order routes.

Not part of the inventory tickets — separate so it can be dropped/owned independently.

* Refactor: move RMA_COLUMNS to constants

Move the fixed RMA_COLUMNS list to workers/lib/constants.js and import it
in work.order.export.js. Repair-date formatting is inlined in renderRmaCsv
(single use, no shared helper). No behaviour change.

* Style: remove explanatory comments

* Refactor: align MINER_LOCATIONS with FE dotted-lowercase format

Per FE/BE alignment on PR #95: switch all location values from
Title Case display strings (e.g. 'Site Warehouse') to dotted-lowercase
keys (e.g. 'site.warehouse'). The set is the union of the spec's
locations and the FE's existing constant — 11 values total.

- MINER_LOCATIONS: workshop.warehouse, workshop.lab, site.warehouse,
  site.lab, site.container, miner.room, vendor, acme.container,
  scrapped, disposed, unknown
- SPARE_PART_INITIAL_LOCATION: 'Site Warehouse' -> 'site.warehouse'
- Update fixture strings in work-orders + spare-parts tests

NOTE: miningos-wrk-inventory needs the matching change (constants +
_validateLocation set) or the inventory worker will reject these
values with ERR_INVALID_LOCATION. Existing stored rows ('Site
Warehouse', etc.) need a one-time migration to the new keys.

* feat(alerts): filter & search alerts by device tag; fix site deviceId filter (#97)

- expose `message` (device/equipment tag) on historical alerts and whitelist it
  for filter+search on both site and history endpoints
- expose `deviceId` on site alerts so the existing deviceId filter works (alert
  objects previously only carried `id`); aligns site with history
- add a Postman collection for the alerts endpoints

* Feat: batch create endpoints for work orders and spare parts (#98)

* Feat: batch create endpoints for work orders and spare parts

* refactor: use WORK_ORDER_TYPES constants in work order schemas

Replace magic enum values [3, 4] and [1, 2, 3, 4] with the existing
WORK_ORDER_TYPES constant, addressing PR #98 review feedback.

* Feat: move work order relocates the device atomically

A type-2 (move) work order auto-closes on creation, so it must perform the relocation itself — otherwise the closed WO records a move that never happened. createWorkOrder and createWorkOrdersBatch reuse submitWorkOrderAction (now taking an optional rackId) to push an updateThing to the part's own rack before recording the WO.

Claude-Session: https://claude.ai/code/session_01Hk3GYz4TPiRmSs3Qmvv27V

* feat(cooling): redesigned Operations Centre cooling/energy data (#100)

cooling-system: explicit fan-coil valve_tag/temperature_tag map; binary cooling_towers[].vibration_switch (replaces numeric vibration); HVAC C1 control_valves.pressure_bypass.pressure (PIT-7501); miners C1 lines[].differential_pressure[] plus summary inlet/outlet/delta_p averages; mining_room.groups[].rack_statuses (online = live rack power); ambient averaging over real readings (drop only missing).

energy-system: site_pm.current.total used for the SLD aggregate.

metrics: new GET /auth/metrics/cooling historical endpoint (hourly/daily) backed by DCS stat ops; WORKER_TYPES.DCS set to the actual thing type 'dcs-siemens'.

Tests added/updated.

* fix(work-orders): declare partsMoves in PATCH schema (#102)

* feat: record remarks + replacement link on spare-part moves (#104)

PUT /auth/spare-parts/:id now accepts optional `remarks` and
`replacesPartCode`. The server-computed partsMoves entry gains:
- `remarks` (falls back to info.reason) — backs the move REASON column
- `replacesPartCode` + `replacesPartId` + `role: 'replacement'` — links a
  replacement part to the part it replaces (the REPLACES column)

replacesPartCode is validated server-side (must resolve to an existing
part, can't be the part itself), so the partsMoves audit trail stays
non-forgeable. Both fields are optional, move-only metadata and are not
written to the part record; existing payloads are unaffected.

Claude-Session: https://claude.ai/code/session_01UcMhxtedvx5EDRffjJdBRF

* Feat: add work order reopen route (#105)

Claude-Session: https://claude.ai/code/session_018SZfbTWwga6AnWp6HVY35a

* Fix: execute device relocation on Move work orders (#107)

The part rack rejects any location change without info.workOrderId
(ERR_PART_MOVE_REQUIRES_WO), so the Move relocation was silently dropped.
Pre-generate the WO id, pass it as the relocation's workOrderId and as the
WO thing id, and surface relocation push errors instead of swallowing them.

Claude-Session: https://claude.ai/code/session_018SZfbTWwga6AnWp6HVY35a

* feat: forecast manual override (#108)

* feat: add PDU layout endpoint (#109)

* feat: server-side alerts filtering with operators and `type` param (#110)

* feat: server-side alerts filtering with operators and type param

- Validate `filter` against allow-listed fields + operators
  ($eq,$ne,$in,$nin,$gt,$gte,$lt,$lte); reject others with ERR_INVALID_FILTER.
- Push thing-level filtering down to the listThings / getHistoricalLogs
  queries instead of filtering only in-memory; re-apply on the merged
  result for per-alert and multi-rack correctness.
- Add `type` query param (all|operational|miner) on the site and history
  endpoints, matching the miner base type and its subtypes (e.g. miner-am-s19xp).

* feat: include alert name in site alerts search

Add `name` to SITE_ALERTS_SEARCH_FIELDS so search matches the alert name
(history search already covers it).

* feat: unify alert device fields (type, position) across endpoints

- History now returns the device type as `type` (was `deviceType`), matching
  the site endpoint; history filter field renamed accordingly.
- Site alerts now carry `position` (info.pos) so the Position column is
  populated; both endpoints now expose type/container/position consistently.

* Fix: persist register-WO notes, writable issue field, fresh WO detail (#111)

* Fix: persist register-WO notes, writable issue field, fresh WO detail

- registerSparePart/Batch store the operator note as info.notes on the work order (single omitted it; batch wrote info.note)
- allow issue inside the work-order update schema's info object
- stop caching GET /work-orders/:id so log/issue writes reflect immediately

Claude-Session: https://claude.ai/code/session_01FxFopChofyQ21y8Q2ZzYrT

* Fix: pass empty opts on spare-part registerThing so miner racks accept it

Miner racks require opts on registerThing; the register-device flow now sends opts:{} on the device/part registration (not the WO), letting a warehouse miner register into inventory without relaxing the template validation or adding FE credential fields.

Claude-Session: https://claude.ai/code/session_01FxFopChofyQ21y8Q2ZzYrT

* feat(cooling): populate per-group return and differential pressures (#112)

Group differential pressure now reads supply/return/differential from a
single transmitter per group (group_pressure_sensors as an ordered list),
preferring the DCS-provided differential. Falls back to the legacy
supply/return id-pair model. Response shape is unchanged.

Co-authored-by: Parag More <parag.more@tether.to>

* Fix: overwriteCache on refetch and map miner model in RMA export (#113)

* Fix: bypass in-flight dedup when overwriteCache is set

Claude-Session: https://claude.ai/code/session_01XV2QxZ65mrwUZCBjjMh1W2

* Fix: map miner model name in RMA export

Claude-Session: https://claude.ai/code/session_01XV2QxZ65mrwUZCBjjMh1W2

* Fix: map miner model name in work-order CSV export (#114)

Claude-Session: https://claude.ai/code/session_01XV2QxZ65mrwUZCBjjMh1W2

* feat: merge worker-level alerts into alerts and history endpoints (#115)

Read worker-level alerts from pool-worker ext data (getWrkExtData 'alerts' /
'alerts-history') and merge them into the site alerts and history-alerts
responses. Worker alerts use the same flat shape as thing alerts, so they need
no special-casing. Workers opt in via ALERT_EXT_DATA_WORKER_TYPES.

* fix(cooling): surface C2 towers, C2 pre/post-HX and C1 pressure averages (#116)

* feat(cooling): populate per-group return and differential pressures

Group differential pressure now reads supply/return/differential from a
single transmitter per group (group_pressure_sensors as an ordered list),
preferring the DCS-provided differential. Falls back to the legacy
supply/return id-pair model. Response shape is unchanged.

* fix(cooling): surface C2 towers, C2 pre/post-HX and C1 pressure averages

- Filter cooling_towers by circuit so each Circuit 2 view shows only its
  own tower (COOLING_TOWER vs HVAC_CONDENSER).
- Read Miners C2 pre/post-HX temps from the loop's configured sensors,
  falling back to the heat-exchanger-derived values.
- Average the Miners C1 header pressures over the 16 per-group PTs, with a
  line-level fallback, instead of the unset per-line pressure sensors.

---------

Co-authored-by: Parag More <parag.more@tether.to>

* fix(cooling): keep sensor id on readings even when the tag is unreadable (#117)

Single-sensor summary fields collapsed to null when the tag was not
readable, dropping the sensor label. Add a sensorReading helper that
returns { value, unit, sensor } whenever a sensor is configured (value
null when unreadable), and null only when no sensor is set. Applied to
miners C2 pre/post-HX and tower level, HVAC C2 tower level, and HVAC C1
system pressure.

* fix(cooling): keep units on readings when tags are unreadable (#118)

getSensorReading and getSensorWithTag now return { value: null, unit }
when a configured sensor is present in the snap but its tag isn't
readable (offline devices are still emitted), instead of collapsing to
null and dropping the unit; they return null only when no sensor is
configured. Also surface makeup pump speed/current and return it by
config id so it is present with null values when offline.

* fix(cooling): include per-group PTs in miners C1 supply/return tables (#119)

The FE builds the SUPPLY/RETURN tables from line.supply.sensors /
line.return.sensors and the delta-P table from differential_pressure.
Append each group's supply/return PT (type 'Pressure - Group N') to the
sensor lists so the per-group pressures render, and number groups
absolutely (line1 1-8, line2 9-16) since the FE prints entry.group.

* feat(cooling): surface make-up water system on miners circuit 1 (#121)

Extract the makeup block builder shared with circuit 2 and include it in
the circuit 1 and layout views. The make-up pump now comes only in the
makeup block instead of the circulation pumps array, the tank
description carries its volume, on/off valves report is_open from the
valve status, and the circuit 1 projection fetches levels and tanks.

* user perms update (#122)

* user perms update

* forecast perms

* remove unused property

* perm fix

* get users with read perms

* fix: site manager perms fix (#123)

* fix: site manager perms fix

* user perms

* user perms

* json syntax

* fix: site manager users listing (#124)

* fix: check read-level perms on GET routes (#125)

GET/HEAD routes going through the shared route helpers were always
checked at write level, so read-only roles got 401 on read APIs
(work-orders, spare-parts, site, history). Derive the check level
from the HTTP method instead.

* remove pool config perms for site operator (#126)

* Fix: average metrics buckets instead of summing samples, add interval (#128)

* Fix: average metrics buckets instead of summing samples, add interval

/auth/metrics/hashrate, /consumption and /efficiency read the pre-aggregated
field over tail-log with shouldCalculateAvg instead of summing every sample in
a range-aggr window, which inflated hashrate ~288x and consumption ~1440x.

Bucket size is now selectable via interval (1h/1d/1w) through the existing
resolveInterval/getIntervalConfig helpers, and interval is part of the cache
key so buckets of different sizes no longer poison each other.

Claude-Session: https://claude.ai/code/session_015M7BptTmTQj1pwNz2LKRNT

* Fix: drop summary.totalHashrateMhs, a sum of rates over time

totalHashrateMhs was the sum of per-bucket hashrate levels, so it equalled
avgHashrateMhs * bucketCount and moved with the requested interval rather than
with fleet output. Summing an instantaneous rate across time is not a physical
quantity, unlike consumption where powerW pairs with an integrated
consumptionMWh.

Dropped from both the ungrouped and grouped summaries, including the per-group
breakdown. The field was unused by the UI.

Claude-Session: https://claude.ai/code/session_015M7BptTmTQj1pwNz2LKRNT

* feat(site): surface miner cooling system status on the live status header (#131)

On central DCS sites the site status live API now returns
cooling.status (Running/Off/Unavailable) read from the status the
miningos-wrk-dcs-siemens worker publishes ready-made at
stats.dcs_specific.cooling_system.status — app-node only passes it
through, falling back to Unavailable while the DCS tag is not
provisioned or the DCS thing is missing.

The handler now fetches the DCS thing once per request and derives both
the site consumption (site_main meter) and the cooling status from it,
instead of a separate fetch inside the consumption resolver.

* add groupBy support for metrics/efficiency endpoints (#133)

* Feat: miner-status error field + groupBy=type (#132)

miner-status now emits an `error` count and excludes errored miners from
`online` (previously they were counted as online). Adds groupBy=type, returning
each status as a per-type keyed object, so the doughnut and the status chart can
drop their tail-log calls.

Efficiency groupBy is no longer part of this branch; it shipped in #133.

Claude-Session: https://claude.ai/code/session_015M7BptTmTQj1pwNz2LKRNT

* Feat: add /auth/metrics/miners/by-container snapshot rollup (#135)

* Feat: add /auth/metrics/miners/by-container snapshot rollup

New GET endpoint returning a latest-snapshot rollup keyed by container:
miner count, online/offline/error/not-mining/sleep counts, power-mode
breakdown, hashrate, power, efficiency and temperature. All values come from
the container-grouped aggregates the rack already computes (STAT_RTD), merged
across orks the same way /auth/site/overview/groups does.

Replaces the per-container tail-log snapshots the frontend currently assembles
by hand on the container widgets, home tab, site overview and the efficiency
mining-unit and energy-report views.

Claude-Session: https://claude.ai/code/session_015M7BptTmTQj1pwNz2LKRNT

* Fix: drop always-null asOf from miners/by-container

stat-rtd entries carry no per-entry ts (only aggrTsRange/aggrCount), so asOf
was always null against real ORK data. Removed rather than ship a dead field.

Claude-Session: https://claude.ai/code/session_015M7BptTmTQj1pwNz2LKRNT

* feat(work-orders): manage miner pos, container and status on move and repair WOs (#136)

Move WOs now clear pos and park the miner in maintenance when it leaves
the miner room, and apply the picked group/socket (plus optional subnet)
when it enters. Device status changes ride along as info.deviceStatus so
the WO workflow status stays untouched. MicroBT repair WOs update the
miner named by info.minerIdentifier since only parts are listed as
devices. partsMoves entries record from/to pos, container and status.

* fix: lastActiveAt field added (#134)

* fix: lastActiveAt field added

* forecast perms and fix tests

* fix tests

* Feat: add /auth/metrics/site/summary and /auth/metrics/inventory/summary (#137)

site/summary: a metrics-namespace alias of the existing getSiteLiveStatus, so
the frontend can pull the site rollup (hashrate, power, efficiency, miner
counts, alerts, pools) from one /auth/metrics/* call instead of assembling it
from tail-log in useHeaderStats. Same handler as /auth/site/status/live.

inventory/summary: a new snapshot rollup keyed by inventory status and location
for miners and for each spare-part type (controller, hashboard, psu), from the
inventory-grouped stat-5m aggregates the racks already compute. Keys pass
through untouched so the UI keeps its own labels. Replaces the two grouped
tail-log calls in the Inventory dashboard.

Claude-Session: https://claude.ai/code/session_015M7BptTmTQj1pwNz2LKRNT

* Feat: add /auth/metrics/revenue/hourly (#138)

New endpoint returning hourly pool revenue estimates: reads the pool worker's
hourlyRevenues (produced by _aggrTransactions on aggrHourly queries) via
getWrkExtData, merges the buckets across orks, and returns { log: [{ ts,
revenueBTC }], summary: { totalRevenueBTC } }. Takes an optional pool param
(minerpool-<pool>), defaulting to the generic minerpool type.

Depends on tetherto/miningos-wrk-ork#38, which adds the aggregation op that
lets hourlyRevenues survive the ORK re-aggregation.

Claude-Session: https://claude.ai/code/session_015M7BptTmTQj1pwNz2LKRNT

* Feat: hashrate container/current params + container-history interval (#139)

/auth/metrics/hashrate: optional `container` reads a single container from the
container-group aggregate the racks already compute, and optional `current`
adds summary.currentHashrateMhs from the latest stat-rtd sample. Both are
opt-in; without them the request, the RPC payload and the response are
unchanged.

/auth/metrics/containers/:id/history: optional `interval` selects the stat key
(20s, 1m, 5m, 30m, 3h, 1d) instead of always reading stat-5m. Container racks
schedule 20s/1m/rtd on top of the thing defaults, so every interval is real
data. Defaults to stat-5m.

Together these close the chart call sites that the timeseries scoping found
uncovered, without a generic tail-log passthrough endpoint.

Claude-Session: https://claude.ai/code/session_01GhvfQJep6PVSLJbuime6GF

* fix: /metrics/consumption Use siemens worker power consumption if centralDCSSetup is enabled (#140)

* feat: split site alerts summary into operational/miner and sort severity by rank (#142)

* validations and permissions fix (#144)

* validations and permissions fix

* fix tests

* node version update

* fix(logs): retry dropped RPC channels and bound unlimited tail-log ranges (#141)

* fix(logs): retry dropped RPC channels and bound unlimited tail-log ranges

A pooled protomux-rpc channel can be torn down between requests, so the
next jRequest that reuses it fails with CHANNEL_CLOSED while an immediate
re-dial succeeds. Because requestDataMap did not catch it, a single dead
channel rejected the whole request and surfaced to the client as a 400.

- retry the transient channel failures in a shared _jRequest wrapper used
  by all three RPC paths: 3 attempts with linear backoff, propagating any
  non-retryable error immediately and the final attempt unchanged.
- make requestDataMap degrade instead of failing: a store that still fails
  after its retries is dropped with a warning, so one flaky ork no longer
  loses the response. When every store fails the first error propagates,
  since a total outage must not be mistaken for an empty result set.
- reject limitless tail-log ranges whose bucket count would exceed
  TAIL_LOG_MAX_ROWS with ERR_RANGE_TOO_LARGE, rather than streaming
  hundreds of thousands of rows over one channel. Requests that carry
  their own limit are already bounded and are left alone, and limit is now
  capped in the route schemas.

TAIL_LOG_MAX_ROWS is sized to admit every current caller (30 days of
stat-1m is 43.2k buckets) while rejecting the unbounded year-long ranges.

* fix(logs): size the tail-log caps off the UI's real callers

TAIL_LOG_MAX_ROWS = 50000 served two unrelated jobs — the range-guard
budget and the schema's limit maximum — and was justified by a caller
that does not exist: 30 days of limitless stat-1m. The UI's export
coarsens to stat-5m past 6 days precisely so it never asks for that.

Split the two and size each off what the UI actually sends:

- TAIL_LOG_MAX_ROWS = 9000 bounds limitless requests. The only such
  caller is the historical miner KPI export, whose own ladder
  (moria-app-ui useMinerStatKey, #2797) caps it at 8640 rows.
- TAIL_LOG_MAX_LIMIT = 10080 caps a client-supplied limit, set by the
  dashboard power-mode timeline's 7 days of stat-1m. Dropping the cap
  below this would have rejected that chart outright.

Worst case served falls from 50k rows to 9k limitless / 10,080 limited,
with no frontend change.

Tests: replace the 30-day stat-1m case with the export ladder's actual
ceiling (30 days of stat-5m and 6 days of stat-1m, both 8640) and assert
the month of stat-1m is now rejected. New routes test pins the limit cap
at or above the dashboard's 10,080 so lowering it fails loudly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(logs): confine RPC retries to reads and close the range-guard gaps

Two review findings on #141, both in code this branch introduced.

_jRequest retried every method, so it covered the writes too: pushAction,
pushActionsBatch, voteAction, cancelActionsBatch, saveWrkSettings,
storeFile, removeFile, saveThingComment, setGlobalConfig and the energy
setters. A channel can drop after the ork applied the request but before
the response arrives — indistinguishable from a drop before it applied —
so a retried miner control action can execute twice, and a single
pushAction carries no idempotency key to dedupe on.

Gate the retry on RPC_RETRYABLE_METHODS, an allowlist of the 18 read
methods that reach the proxy. Writes keep develop's behaviour of failing
on the first CHANNEL_CLOSED, and an allowlist means the dynamic comment
operation in things.handlers, plus any write added later, defaults to not
retrying.

The range guard also failed open on half of LOG_KEYS: TAIL_LOG_BUCKET_MS
carried 3 of the 7 keys, and assertRangeWithinRowLimit returns early when
a key has no width. key is a bare string in both route schemas, so a
limitless year of stat-20s — about 1.58M buckets — walked straight past
it, as did stat-30m at 17.5k.

- give stat-20s, stat-30m and stat-1D their widths, so all six
  fixed-width keys are checked.
- fail closed on an unrecognised stat-* key with a bounded limitless
  range: ERR_KEY_NOT_RANGE_CHECKABLE, so a key added upstream is
  rejected loudly rather than silently unbounded.
- exempt stat-rtd explicitly via TAIL_LOG_UNBUCKETED_KEYS. It holds the
  latest sample per thing, not a series, so the thing count bounds it
  rather than the range. Non-stat keys stay out of scope.

getContainerHistory was never exposed here: it defaults its limit and the
schema caps it at 1000, so it is bounded either way.

Tests: 5 new in data.proxy.test.js pinning that writes attempt exactly
once on both proxy paths, that an unlisted method does not retry, and
that reads still retry through requestDataAllPages. 4 new in
logs.handlers.test.js covering the three added keys, the fail-closed
path, and the limit exemption still applying to a key it cannot size.
Unit suite 1259/1261, the 2 failures the pre-existing admin_external
ones.

* style: satisfy standard in the central-DCS request params

The multiline ternary added in fd48659 fails standard on four counts
(multiline-ternary twice, comma-dangle twice), which is why `npm run lint`
is red on develop and `npm test` short-circuits before the unit suite ever
runs. CI hides this: ci.yml:134 pipes lint through `|| echo "::warning::"`,
so the pipeline stays green either way.

Reindent to the operator-first form standard wants and drop the trailing
commas. No behaviour change — same two branches, same keys.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: /metrics/efficiency Use siemens worker power consumption if centralDCSSetup is enabled (#143)

* fix(metrics): normalize grouped range-string timestamps (#145)

* fix(metrics): normalize grouped range-string timestamps

Report charts showed 1970-01-01 as the first X-axis value on the single-site
Operations Dashboard, Efficiency and Energy pages.

With groupRange set, the worker returns ts as a range string such as
"1770854400000-1771459199999" rather than a number. parseEntryTs exists for
exactly this and is documented as such, but six sites in this handler read
val.ts raw: the hashrate, consumption and efficiency logs plus their grouped
variants. Those are the ones feeding the affected charts.

Any span over two days resolves to interval '1d' -> groupRange '1D', and the
dashboards default to seven days, so the default view was always the grouped
one. The UI did _toNumber() on the range string, got NaN, and plotted the
point at the epoch; once it started rejecting undated readings the same
entries were dropped and the charts read "No data" instead.

power.consumption.handlers.js also reads entry.ts raw but never passes
groupRange -- it pins a 5m interval -- so its entries are always numeric and
it is left alone.

The two failing admin_external tests are unrelated and fail on a clean tree.

* fix(finance): normalize the grouped range-string ts on the energy stats too

Same bug as the previous commit, a second handler, and a quieter symptom.

`getEnergyBalance` and `getRevenueSummary` both request the electricity worker's
stats-history with `groupRange: '1D'`, so `ts` comes back as a range string. Their shared
`processEnergyData` read it raw:

  const ts = getStartOfDay(item.ts || item.timestamp)   // NaN on a range string
  if (!ts) continue                                     // ... so every row is dropped

Unlike the metrics charts this fails silently: nothing lands in the daily map, so
`activeEnergyIn` and `uteEnergy` are 0 and the report reads `curtailmentMWh: null`,
`curtailmentRate: null`, `operationalIssuesRate: null` on days that have the data. A
missing metric looks like a site that was never curtailed rather than a parse failure,
which is why nobody reported it.

`parseEntryTs` where the value is read, exactly as in metrics.handlers.js.

The other raw `ts` reads in this file are left alone deliberately: they consume tail-log
range-aggr, minerpool transactions and mempool prices, none of which pass `groupRange`,
so their entries are numeric.

Tests: one case through `getEnergyBalance` asserting curtailment is derived from a
range-string bucket rather than dropped. It fails on the unfixed tree with
`curtailmentMWh: null`.

`brittle tests/unit/handlers/finance.handlers.test.js` -> 71/71 pass, 293/293 asserts.
Full unit suite 1310/1312, the two failures being the pre-existing `admin_external`
cases that fail identically on a clean tree.

* version update

* test mock fix

---------

Co-authored-by: Parag More <34959548+paragmore@users.noreply.github.com>
Co-authored-by: Caesar Mukama <mcaesar88@gmail.com>
Co-authored-by: andretetherio <andre.mendes@tether.io>
Co-authored-by: borik91 <9007515+boris91@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Robert <30272573+rob-aslanian@users.noreply.github.com>
Co-authored-by: Parag More <parag.more@tether.to>
Co-authored-by: Shrutesh Pachineela <sruteshp@gmail.com>
Co-authored-by: Arif Dewi <11630380+arif-dewi@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
paragmore added a commit that referenced this pull request Sep 4, 2026
* Feature - Site status live api (#4)

* feat: add API v2 endpoints (finance, pools, pool-stats, pool-manager) (#11)

* feat: add GET /auth/finance/energy-balance endpoint

Add energy balance API endpoint that aggregates power consumption,
pool transactions, BTC prices, and production costs into a unified
time-series response with period-based aggregation (daily/monthly/yearly).

Includes shared infrastructure: period.utils, constants for RPC methods,
worker types, aggregation fields, and utility functions (getStartOfDay,
safeDiv, runParallel).

* feat: add GET /auth/pools/:pool/balance-history endpoint

Add pool balance history API endpoint that fetches pool balance
snapshots via tailLog RPC, groups them into time buckets (1D/1W/1M),
and returns time-series data with balance and revenue per bucket.

* tiny improvements

* feat: add GET /auth/finance/ebitda endpoint

Add EBITDA API endpoint that aggregates pool transactions, power/hashrate
data, BTC prices, production costs, and block data to calculate selling
and HODL EBITDA metrics with period-based aggregation.

Includes shared infrastructure: period.utils, constants, and utility
functions required for finance endpoints.

* feat: add GET /auth/finance/cost-summary endpoint

Add cost summary API endpoint that aggregates production costs, BTC
prices, and power consumption to calculate energy and operational cost
breakdowns with period-based aggregation.

* feat: add GET /auth/pools endpoint

Add pools API endpoint that aggregates pool device lists and stats
from ORK clusters, supports mingo-based filtering, sorting, and
field selection, and returns pool summary with totals.

* feat: add GET /auth/pool-stats/aggregate endpoint

Add pool stats aggregate API endpoint that fetches pool stats history
and transactions, processes daily stats and revenue, and aggregates
by period (daily/weekly/monthly) with optional pool filtering.

* Fix costs bugs

* feat: add curtailment, operational issues, and power utilization to energy-balance

- Add ELECTRICITY worker type, ENERGY_AGGR/ACTIVE_ENERGY_IN/UTE_ENERGY aggr fields,
  and GLOBAL_CONFIG RPC method to constants
- Add 3 new parallel RPC calls: electricity stats-history (active_energy_in,
  ute_energy) and getGlobalConfig (nominalPowerAvailability_MW)
- Calculate curtailmentMWh, curtailmentRate, operationalIssuesRate, and
  powerUtilization per log entry
- Add avgCurtailmentRate, avgOperationalIssuesRate, avgPowerUtilization to summary
- Fix getProductionCosts to use ctx.globalDataLib instead of direct Hyperbee access
- Fix processCostsData to return daily costs (energyCostPerDay, operationalCostPerDay)
- Update tests to match new cost format and globalDataLib mock

* feat: remove block data, fix costs processing, add site param to ebitda

- Remove block data RPC call and processBlockData (not in spec)
- Remove blockCount, difficulty, ebitdaMarginSelling, ebitdaMarginHodl from log
- Fix processCostsData to handle flat objects from globalDataLib with daily costs
- Replace direct globalDataLib.getGlobalData range query with getProductionCosts helper
- Add site query param to schema, cache key, and handler for site-specific costs

* feat: fix costs processing and add site param to cost-summary

- Fix processCostsData to handle flat objects from globalDataLib with daily costs
- Replace direct globalDataLib range query with getProductionCosts helper
- Add site query param to schema, cache key, and handler for site-specific costs
- Update tests to match new cost format

* feat: add hashrate and remove snapshotCount from balance-history log entries

Align pool balance-history endpoint with API v2 spec which requires
balance, hashrate, and revenue time-series data.

* feat: remove snapshotCount from pool-stats-aggregate log entries

Align pool stats aggregate endpoint with API v2 spec which does not
include snapshotCount in the response.

* fix: use ext-data transactions instead of tail-log for pool stats aggregate

tail-log returns empty for type=minerpool. Switch to ext-data with
transactions key which has daily revenue and hashrate data. Also fix
changed_balance handling (values are BTC, not satoshis).

* fix: use ext-data transactions instead of tail-log for balance history

tail-log returns empty for type=minerpool. Switch to ext-data with
transactions key which provides daily revenue (changed_balance) and
hashrate (mining_extra.hash_rate). Also treat 'all' pool param as
no filter.

* fix: use ext-data stats array instead of list-things for pools endpoint

list-things for t-minerpool returns empty; pool data lives in ext-data
stats responses. Replaced flattenResults/flattenStatsResults/mergePoolData
with flattenPoolStats that parses the actual stats array format.

* code cleanup

* cleanup

* cleanup

* Ports pool manager from moria

* Removes regions concept to align with site-focused nature of API v2

* chore: remove comments from pool manager files

* Remove unncecessaey sute param and add integration tests

* removes site

* fix: remove site param from ebitda endpoint and add integration test

Data is fetched for a single site, so the site parameter is unnecessary.
Added security integration test for finance/ebitda endpoint.

* fix: remove site param from cost-summary endpoint and add integration test

Data is fetched for a single site, so the site parameter is unnecessary.
Added security integration test for finance/cost-summary endpoint.

* fix: rename filter to query param and use mingo projection for pools

Align with list-things pattern by renaming filter→query. Replace manual
sort and field filtering with mingo cursor methods for consistency.

* fix: move pool filter to RPC payload for balance-history endpoint

Pool name filter should be handled by the RPC worker, not filtered
client-side. The tx username is the account id.

* fix: move pool filter to RPC payload for pool-stats-aggregate endpoint

Pool name filter should be handled by the RPC worker, not filtered
client-side. The tx username is the account id.

* Address PR comments

* fix failing tests

* Removes tags from projections

* Fix integration tests

* requestRpcMapAllPages to fetch all pages from orks

* fix linting

* Fix failing tests

* fix: Improves user and roles ux (#12)

* return the created user

* fix: require users:r permission for GET /auth/roles/permissions

* fix: update getRolesPermissions route to require users:w permission

* fix: enhance getRolesPermissions to filter roles based on user role

* fix: update getRolesPermissions to exclude roleManagement for super admin

* Cleanup

* feat: add GET /auth/finance/subsidy-fees endpoint (#14)

* feat: add GET /auth/finance/subsidy-fees endpoint

Port Bitcoin network block data (subsidy rewards + transaction fees) from
legacy dashboard to API v2. Fetches HISTORICAL_BLOCKSIZES from mempool
worker, aggregates by period (daily/weekly/monthly).

* fix: remove unused site parameter from getProductionCosts

* Adding CI (#23)

* feat: add alerts API v2 endpoints (#24)

* resolve conflicts

* Feature: Alerts

* refactor: use RPC constants and explicit field mapping in alerts handlers

* Fix code smells

* fix failing tests

* Remove metrics

* Address comments

* feat: add metrics API v2 endpoints (#21)

* feat: add finance revenue and revenue-summary endpoints (#22)

* feat: add GET /auth/finance/revenue endpoint

Port pool transaction revenue data from legacy dashboard to API v2.
Separates revenue from pool fees, supporting both F2Pool (changed_balance
+ tx_fee) and Ocean (satoshis_net_earned + fees_colected_satoshis) formats.
Aggregates by period with optional pool filter.

* feat: add GET /auth/finance/revenue-summary endpoint

Most impactful endpoint — replaces 9 API calls + ~1500 LOC of frontend
processing. Merges transaction data, BTC prices, costs, power, hashrate,
block data, and electricity data into comprehensive daily rows with EBITDA,
production cost, hash revenue, curtailment, and power utilization metrics.

* fix: include pool name in worker type for revenue endpoint

* refactor: extract shared finance utils and consolidate duplicated logic

- Create finance.utils.js with shared utilities:
  - validateStartEnd: replaces duplicated validation in every handler
  - normalizeTimestampMs: moved from finance.handlers.js
  - processTransactions: consolidates processTransactionData and
    processEbitdaTransactions (fixes missing normalizeTimestampMs and
    incorrect BTC_SATS division bugs), supports trackFees option
  - extractCurrentPrice: merges flat and nested EBITDA price formats
  - processBlockData: extracted for reuse across revenue endpoints
- Refactor finance.handlers.js to use shared utils
- Add comprehensive unit tests for all utils (24 tests)
- Update handler tests to remove tests for deleted functions

* Remove spec doc

* feat: add device listing API v2 endpoints (#25)

* feat: add device listing API v2 endpoints

* fix: paginate listThings RPC and add field projections for device endpoints

Address PR review: use requestRpcMapAllPages to fetch all items beyond
the default 100-item RPC limit, and pass DEVICE_LIST_FIELDS projection
to reduce response size when status=1.

* fix: use requestRpcMapLimit with limit/offset and remove status

- Switch getMiners/getContainers from requestRpcMapAllPages to
  requestRpcMapLimit, passing limit/offset to RPC instead of
  fetching all objects
- Remove status:1 from all listThings calls as DEVICE_LIST_FIELDS
  don't have snap properties
- Update pagination test to verify RPC payload

* Add maximium to limit

* Remove /auth/miners

* Feat/add get configs route (#26)

* Add get configs route

* Add tests and lint

* Remove old pool manager apis (#29)

* feat: add GET /auth/finance/hash-revenue endpoint (#27)

* non rpc method calls to ork (#28)

* fix: address OWASP security review findings for v2 API endpoints (#30)

* ci: enforce coverage (#31)

* feat: pool config stats (#32)

* feat: pool config stats

* var name typo fix

* feat: add GET /auth/miners with getThingsCount (#33)

* fix: config routes read permissions fix (#34)

* ci: improve coverage summary (NYC table) (#35)

* feat: containers pool stats (#36)

* fix: add schema bounds to prevent negative limit/offset bypass (#37)

* ci: move away from pull_request_target (#38)

* feat: add groups stats endpoint and racks filter for metrics (#47)

* feat: add groups stats endpoint and racks filter for metrics

Add GET /auth/groups/stats endpoint for live rack-group aggregation
(efficiency, hashrate, power, miner counts). Extend hashrate and
consumption metrics with optional racks query parameter for filtering
by rack groups via ORK RPC.

* feat: add groups stats endpoint and racks filter for metrics

Add GET /auth/groups/stats endpoint for live rack-group aggregation
(efficiency, hashrate, power, miner counts). Extend hashrate and
consumption metrics with optional racks query parameter for filtering
by rack groups via ORK RPC.

* fix: rename racks filter to containers for metrics endpoints

* refactor: deduplicate parseContainers, extractKeyEntry and use AGGR_FIELDS constants

* fix: remove non-functional containers filter from hashrate/consumption

tailLogCustomRangeAggr RPC method does not support containers param,
so remove the dead code from getHashrate/getConsumption handlers,
their schemas, route cache keys, and related tests.

* fix: handle noAuth mode in capCheck

capCheck crashes in noAuth mode when routes pass permissions because
ctx.authLib is undefined. Add early return guard matching authCheck.

* Revert "fix: handle noAuth mode in capCheck"

This reverts commit 2d4d9929545e3791a6cfe3f15263bfeb27fb0b8d.

* test: improve branch coverage for metrics handlers

Add tests for uncovered branches: temperature rolling avg,
container history prefix match, non-object guard branches,
and groupRange config paths in getPowerMode/getTemperature.

* chore: remove unnecessary comment

* Add new auth/cooling system api (#51)

* Add new auth/cooling system api

* Fix review comments, make the tag generic for dcs device and update endpoint path

* Fix

* remove device name

* fix

* fix: sanitize queryActions input to prevent injection and DoS (#49)

* fix: sanitize queryActions input to prevent injection and DoS (TDEBT-38)

Add maxLength constraints to suffix (200) and queries (10000) in route
schemas, validate queries array type and cap length at 50 in handler.

* fix: tighten queryActions limits and extract to constants

Reduce queries string maxLength from 10000 to 1000 and cap the array at
10 items (previously 50). Extract both limits to named constants.

* Add energy views endpoint support (#52)

* Add energy views endpoint support

* move outside to dir with other utils

* fix imports

* feat: microsoft oauth (#54)

* Support site overview endpoints for dcs and miner group wise overview (#55)

* Support site overview endpoints for dcs and miner group wise overview

* update move to site handlers

* Support new dcs device tags update and fix cooling system apis (#56)

* Support new dcs device tags update and fix cooling system apis

* update the attributes for rack group

* Tests

* tests

* Add hvac circuit 1 and layout api updates (#58)

* Add hvac circuit 1 and layout api updates

* Update hvac circuit2 api

* Add site effciency api (#57)

* Add site effciency api

* Add site effciency api routes

* tests

* Add explorer racks api (#59)

* (improvement) Explorer handlers, Racks list, Search filtering flow improved (#60)

* improv: workers/lib/server/handlers - explorer - 'filterBySearch' flow improved.

* improv: workers/lib/server/handlers - explorer - 'filterBySearch' flow & usage improved.

* fix: workers/lib/server/handlers - explorer - 'filterBySearch' flow fixed.

* test: tests/unit/handlers - explorer - 'filterBySearch' cases coverage extended.

* test: tests/unit/handlers - coolingSystem - 'createMockConfig' result generation fixed.

* feat: group hashrate by miner and container (#62)

* fix: finance v2 handler RPC regressions (#67)

* fix: processBlockData parses HISTORICAL_BLOCKSIZES flat-array shape

The mempool worker's getWrkExtData returns a flat array of block records
[{ts, blockSize, blockReward, blockTotalFees}, ...], but processBlockData
only handled nested .data/.blocks wrappers - it iterated entry's own field
names, Number('ts') returned NaN, and produced an empty daily map. The
SubsidyFee page then rendered "No data available".

Detect the flat shape (entry has .ts/.timestamp/.time) and process it
directly. Also surface blockSize on getSubsidyFees and getRevenueSummary
log entries (and totalBlockSize/avgBlockSize on the summary) - needed for
SubsidyFee's "Avg Fees in Sats/vByte" chart to render after this fix.

* fix: cost-summary propagates per-day btcPrice

Two compounding bugs in getCostSummary caused btcPrice=0 for every log
entry, leaving ProductionCostPriceChart's overlay line at 0:

1. Wrong RPC key. The handler queried mempool with key: 'prices', which
   the worker doesn't recognize and so returns the live snapshot
   ({currentPrice, blockHeight, ...}). Switched to 'HISTORICAL_PRICES',
   which returns [{ts, priceUSD}, ...].

2. processEbitdaPrices couldn't parse the production shape. Same class
   as the processBlockData fix (Bug C): the mempool worker returns a flat
   per-ORK array of records, not a wrapper. Detect when entry has
   .ts/.timestamp/.time and process it as the item directly; also accept
   priceUSD (the actual upstream field name) alongside price.

The processEbitdaPrices fix incidentally makes getRevenueSummary and
getHashRevenue use real per-day prices instead of falling back to
currentBtcPrice. getEbitda still uses the wrong key: 'prices' (line 344)
and is left for a follow-up.

* fix: processTailLogData drills into .val for nested TAIL_LOG_RANGE_AGGR

The TAIL_LOG_RANGE_AGGR RPC returns per-day items as
{ts, val: {site_power_w, hashrate_mhs_5m_sum_aggr}}, but
processTailLogData read item[AGGR_FIELDS.SITE_POWER] directly without
drilling into .val, so powerW and hashrateMhs were 0 for every entry.
RevenueSummary then showed 0 for Avg Power, Avg Hashrate, and Hashrate
Capacity Factor; EBITDA's per-day power/hashrate were similarly zeroed.

Mirror the .val/.flat fallback pattern that processConsumptionData
already uses (workers/lib/server/handlers/finance.handlers.js:172).

* fix: hash-revenue hashrate and network-hashrate populate from real RPC

Two bugs in getHashRevenue caused hashrateMhs and networkHashrateMhs
to be 0 for every log entry, leaving HashBalance entirely zeroed:

1. processHashrateData read item[AGGR_FIELDS.HASHRATE_SUM] directly,
   but TAIL_LOG_RANGE_AGGR wraps measurements as
   {ts, val: {hashrate_mhs_5m_sum_aggr}}. Same .val drilling fix as
   processTailLogData.

2. processNetworkHashrateData expected entries wrapped under .data,
   but the HISTORICAL_HASHRATE RPC returns a flat per-ORK array of
   {ts, avgHashrateMHs} records. Same flat-shape detection as
   processBlockData (Bug C).

* Fix memory leak in authCheck by implementing LRU caching and removing in-memory cache (#61)

* Support summary for grouped by in hashrate metrics api (#68)

* Support summary for grouped by in hashrate metrics api

* update groupKey

* fix: groups stats accepts rack ids and returns rack objects (#69)

* Add groupBy for metrics consumption api (#70)

* Update the cooling system handlers to fix flow readings (#71)

* fix(finance): correct energy-balance period aggregation (#72)

The shared aggregateByPeriod summed every numeric field, which is
correct for totals but produced nonsense for rates/means (curtailment
rate, btcPrice, sitePowerMW, etc.) on weekly/monthly buckets.

- Add an optional `meanKeys` arg to aggregateByPeriod that averages
  listed fields instead of summing. Default behavior is unchanged for
  the 6 existing callers.
- getEnergyBalance now passes meanKeys for sitePowerMW, btcPrice and
  the rate fields, then post-processes to recompute energyRevenue*_MW
  against the (correctly meaned) sitePowerMW denominator.
- Add sitePowerMW + energyRevenueBTC_MW/energyRevenueUSD_MW to log
  entries; add avgPowerConsumption + avgEnergyCostPerMWh +
  avgOperationalCostPerMWh to summary.
- Schema: add 'weekly' to the period enum.

* chore: facs version update (#73)

* Update the dcs apis to support the new tags (#74)

* develop sync (#76)

* feat: adopt JWT-based svc-facs-auth (#53)

* feat: forecast endpoints (#80)

* feat: add rack grouping to metrics/hashrate endpoint (#82)

Extend GET /auth/metrics/hashrate with `groupBy=rack` and an optional
`racks` CSV filter so charts can render a per-rack hashrate time series
for a specific set of racks.

- groupBy=rack reads the hashrate_mhs_5m_pdu_rack_group_sum aggregation
  from stat-1D, the same obj_concat aggregation family already used for
  type/container group grouping
- racks=group-1_rack-1,group-2_rack-1 narrows each log entry's hashrate
  object and the summary breakdown to the requested racks
- racks is cache-keyed and only applied when groupBy=rack

* feat: add rack grouping to metrics/consumption endpoint (#83)

Extend GET /auth/metrics/consumption with `groupBy=rack` and an optional
`racks` CSV filter, mirroring the rack grouping added to metrics/hashrate
in #82.

- groupBy=rack reads the power_w_pdu_rack_group_sum aggregation from
  stat-1D, the same obj_concat aggregation family already used for
  type/container group grouping
- racks=group-1_rack-1,group-2_rack-1 narrows each log entry's powerW
  object (and the derived consumptionMWh) and the summary to those racks
- racks is cache-keyed and only applied when groupBy=rack

* feat: work order + spare parts HTTP API (#79)

* feat: expose Work Order HTTP API + RBAC

Adds the Work Order HTTP surface as thin wrappers over the existing
things RPC plumbing:
  POST   /auth/work-orders                create  (registerThing)
  GET    /auth/work-orders                list    (listThings + type filter)
  GET    /auth/work-orders/:id            get     (listThings + id+type filter)
  PATCH  /auth/work-orders/:id            update  (updateThing)
  POST   /auth/work-orders/:id/close      close   (updateThing status=closed)
  POST   /auth/work-orders/:id/cancel     cancel  (updateThing status=cancelled)
  POST   /auth/work-orders/:id/assign     assign  (updateThing assignedTo)
  GET    /auth/work-orders/:id/audit      history (getHistoricalLogs)

Introduces a new RBAC resource work_order (r/rw) granted to
operator/admin/repair_technician roles for write and to viewer
roles for read. Strips the duplicated [HRPC_ERR]= prefix and
bubbles structured rack errors verbatim. Skips capCheck when the
gateway runs with --noauth so local curl-testing works through
permissioned routes.

* feat: searchable Work Order list with pagination envelope

Adds filter shortcuts (q, assignee, creator, partId, status, type,
from, to) to GET /auth/work-orders alongside the existing mingo
?query passthrough; q is a case-insensitive regex $or against code
and info.issue, the rest map to info.* paths and run on the rack
the same way every other list does. Pages now return the
{data, totalCount, offset, limit, hasMore} envelope by calling
listThings and getThingsCount in parallel, and every shortcut is
wired into the cache key so cached pages do not bleed across
filter combinations.

* feat: spare-part PUT gated by an open Work Order + repair history

Adds PUT /auth/spare-parts/:id which validates the supplied
workOrderId resolves to an open or in_progress WO, pushes the
underlying updateThing on the part rack with workOrderId injected
into info, and appends a denormalised entry to the WO's
partsMoves[] so the audit trail stays on the Work Order itself.
GET /auth/spare-parts/:id/repair-history mingo-queries every WO
that ever touched the part and returns each move row hydrated with
workOrderCode in the standard {data, totalCount, offset, limit,
hasMore} envelope.

* feat: Work Order log entries + file upload/download/delete routes

Adds POST /auth/work-orders/:id/log which forwards to the existing
saveThingComment RPC so the WO comments[] doubles as the work log.
Adds POST/GET/DELETE /auth/work-orders/:id/files routes backed by
@fastify/multipart on the gateway and the new
storeWorkOrderFile/loadWorkOrderFile/removeWorkOrderFile pass-
throughs on the ork; uploads validate mime + size, append metadata
to WO.info.files via pushAction, and reject mutations on closed or
cancelled WOs.

* feat: spare-part register flow + Type-1/Type-2 Work Order dispatch

POST /auth/spare-parts now creates the part and a paired Type-1 WO in
one call, returning {partId, workOrderId, workOrderCode}. POST
/auth/work-orders resolves the target part for both flows from
deviceIdentifier (id, code, serialNum, or macAddress) and rejects
unknown deviceTypes with ERR_INVALID_DEVICE_TYPE / unresolved parts
with ERR_PART_NOT_FOUND.

* feat: accept warranty payload on Work Order create/update

Adds an open `warranty: { vendor, fields }` shape to the create and
update body schemas; vendor-specific field validation lives in the
inventory worker so app-node stays agnostic of vendor differences.

* feat: GET /auth/work-orders/:id/export with PDF + CSV output

Export accepts either the WO uuid or its IVI-* code in the path and a
required ?format=pdf|csv|docx querystring. CSV flattens one row per
parts-movement entry (WO header columns repeated). PDF is server-rendered
via PDFKit and includes header, triage, work log, parts movements, file
references, and a vendor-specific warranty section. format=docx returns
501 with ERR_EXPORT_FORMAT_NOT_IMPLEMENTED until phase 2.

* refactor: ship CSV only; pdf + docx return 501 (deferred to FE/phase 2)

Drops the pdfkit dependency and the PDF renderer. The export endpoint
now serves CSV directly; pdf and docx both 501 with
ERR_EXPORT_FORMAT_NOT_IMPLEMENTED, keeping the route shape stable for
when the frontend renders PDF client-side.

* feat: align SPARE_PART_INITIAL_LOCATION to canonical 'Site Warehouse'

Mirrors the new MINER_LOCATIONS enum from the inventory worker.

* refactor: camelCase MINER_LOCATIONS values + SPARE_PART_INITIAL_LOCATION

Mirrors the inventory worker constant.

* feat: GET /auth/spare-parts with mingo-side location / status / q filters

Querystring shortcuts (location, status, q) translate to a mingo query
forwarded to the rack via listThings + getThingsCount — no app-node
post-filtering. q is a case-insensitive regex against code, info.serialNum,
and info.macAddress. WO things are excluded by default via type:{\$ne:...},
overridable by an explicit query.type. Cache key includes every filter
shortcut so combinations don't collide.

* refactor: extract escapeRegex, listThingsWithCount, stableJsonString to utils

Both list handlers (work-orders + spare-parts) hand-rolled the same
regex-escape + listThings/getThingsCount/pagination skeleton; collapses
to a single utils helper used by both. Cache keys for the two GET list
routes now canonicalize their JSON inputs via stableJsonString so
semantically-equal queries share a cache slot instead of missing each
other on key-order differences.

* refactor: drop waitForThing — registerSparePart returns action ids, no polling

App-node was polling listThings after each pushAction to surface the
rack-assigned thing code in the response — exactly the
'submit-and-poll-in-app-node' anti-pattern the codebase had already
rejected. registerSparePart now pre-generates partId/woId, fires both
registerThing pushActions in parallel, and returns
{partId, workOrderId, partActionId, workOrderActionId, errors}. Clients
read the eventually-assigned codes from GET /auth/actions/done/:id like
every other write in the app.

Removes waitForThing + sleep from utils and the two timeout constants
that only existed to tune it.

* refactor: address peer-review #3 #6 #8 #10 #12 #20

#3 (atomicity) — updateSparePart now checks the part pushAction's
   errors[] before firing the WO partsMoves append. If the part write
   was rejected at push time, throws ERR_PART_UPDATE_PUSH_FAILED:<msg>
   with a 502 instead of silently enqueueing the WO append. Response
   shape also gains partActionId / workOrderActionId / workOrderAppendErrors
   so the FE can detect a post-push WO failure and trigger a manual
   reconcile.
#6 (status casing) — WO status enum normalised to 'inProgress' so
   every info.* enum value follows the camelCase convention adopted
   for MINER_LOCATIONS.
#8 (registerSparePart field sprawl) — drops the
   info.model→info.deviceModel→'unknown' fallback chain and the
   serialNum→macAddress→partId fallback. deviceModel and serialNum
   are now both required up front with explicit ERR_*_REQUIRED errors.
#10 (action latency) — register/update responses include
   expectedActionLatencyMs sourced from ctx.conf.expectedActionLatencyMs
   (default 1000) so the FE can pace its /auth/actions/done/:id polling
   instead of guessing.
#12 (export 501) — error message now carries the requested format
   (ERR_EXPORT_FORMAT_NOT_IMPLEMENTED:pdf / :docx) so the FE can tell
   which format was rejected without re-reading the request.
#20 (pagination) — listThingsWithCount now slices the dedupe'd union
   down to the requested limit so multi-ork fan-out can't return more
   rows than the caller asked for. hasMore is now derived from
   offset + limit rather than offset + data.length. Multi-rack
   offset-pagination remains best-effort because each rack applies
   the offset locally; documented inline.

* revert: restore 'in_progress' in WO status schema enum

Mirrors the inventory rack revert — keeps existing 'in_progress' WO
records valid for transitions.

* revert: restore human-readable MINER_LOCATIONS values

Mirrors the inventory rack revert.

* refactor: call generic storeFile / loadFile / removeFile with file type

Updates the work-order file handlers to use the renamed generic file
RPCs and tags each call with FILE_TYPES.WORK_ORDER so the rack can
dispatch on it.

* fix: pass workOrderId + fileId to scoped file RPCs, surface blobCleared

loadFile / removeFile are now called with { workOrderId, fileId } so the
rack resolves the blob descriptor from the work order it belongs to —
app-node no longer forwards a raw blobRef. deleteWorkOrderFile surfaces
the rack's { cleared } result as blobCleared so the HTTP caller can tell
whether the underlying blob was actually removed.

* refactor: address PR #79 review feedback

- resolve the work_order rack from the ork rack registry via listRacks
  (getWorkOrderRackId, cached on ctx) and drop the workOrderRackId config
  key — the rack id is discovered, not deployment config
- relocate submitWorkOrderAction out of generic utils into the new
  server/lib/workOrders module alongside getWorkOrderRackId
- add WORK_ORDER_TERMINAL_STATUSES constant, replacing the duplicated
  ['closed', 'cancelled'] literals across the WO handlers
- workOrderExport: derive the CSV header and rows from one [name,
  extractor] schema instead of two hand-synced lists
- rename pushOne -> pushSingleAction in registerSparePart

* refactor: derive WO export CSV columns from json keys

renderWorkOrderCsv builds the header from the work order's own properties
— top-level code plus every info field, then each parts-movement entry's
keys — so the CSV tracks the worker response schema with no hand-kept
column list. Drops the CSV_HEADERS / WO_COLUMNS / MOVE_COLUMNS definitions.

* refactor: remove non-functional comments from WO export and rack helpers

* refactor: promote _csvEscape to a generic csvEscape util

* refactor: rename WO and spare-parts modules to dot-separated file names

Align the work order and spare parts file names with the repo convention
(cooling.system.routes.js, energy.system.routes.js, etc.): camelCase
basenames become dot-separated — spareParts.handlers.js ->
spare.parts.handlers.js, workOrders.js -> work.orders.js,
workOrderExport.js -> work.order.export.js. Updates every require() path,
route registration in index.js, and test references.

* (fix) NPM audit, Dependencies invulnerability fixed (#84)

* chore: root - package-lock - dependency tree updated.

* fix: root - package-lock - dependency tree packages invulnerability fixed.

* feat: list firmwares endpoint (#86)

* feat: miner log download via Hypercore P2P streaming (#85)

* feat: add miner log download REST endpoints and unit tests

* add LogDownloader for P2P miner log streaming

Introduces a LogDownloader class that fetches miner log files directly
from wrk-miner via Hypercore/Hyperswarm, bypassing the HRPC pipeline.
Wired up in the http worker after facilities are ready.

* trim verbose comments in log download handlers

* Fix LOGS_PEER_NOT_FOUND

* feat: forecast history endpoint (#87)

* feat: forecast settings (#89)

* feat: forecast settings

* get forecast settings endpoint

* fix: add info params + fix WO auth (#88)

* feat: add info params (notes, remarks, site, location) to work order schema

Accept optional info object with notes, remarks, site, and location
on POST /auth/work-orders and PATCH /auth/work-orders/:id. The handler
merges these into the thing info alongside the top-level fields.

* fix: auto-qualify bare permission names in tokenHasPerms

tokenHasPerms received bare resource names (e.g. 'work_order') from
route perms, but _permsMatch expects 'resource:level' format. Splitting
a bare name by ':' left `required` as undefined, crashing on spread.

* Chore/main sync (#91)

* Ci pr trigger change main (#39)

* chore: package json updates for publishing (#41)

* chore: package json updates for publishing

* update deps

* ci: package publishing (#40)

* ci: adding release pr process and version bump (#42)

* Release: v1.0.0 (#44)

* ci: release pr local jobs (#46)

* ci: persistents credentials to allow tag creation (#48)

* Release: v1.0.0 (#50)

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* chore: facs version update (#77)

* refactor: logs cleanup (#78)

* ci: CI Checks — dependency review, audits, and workflow improvements (#81)

- Introduce security dependency review on pull requests
- Improve audit reporting (dependency review, npm/pnpm audit, audit-ci on core)
- Improve overall CI checks: single install + node_modules cache, lockfile gate, parallel jobs, summary

Fleet: core. Suggested PR title matches commit subject.

* reset ci updates

---------

Co-authored-by: andretetherio <andre.mendes@tether.io>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* fix: harden miner download-logs API and P2P log streaming (#94)

- LogDownloader: replicate cores onto already-open Hyperswarm connections
  (reused peer sockets get no 'connection' event), fixing 503
  ERR_LOG_PEER_NOT_FOUND on every download after the first
- accept empty JSON bodies on POST (body-less download-logs start request)
- status endpoint: include a human-readable message with worker error codes

* feat: align site/status/live with header UI logic (#93)

- source consumption by featureConfig (DCS site_main meter, transformer
  meters, or site power meter) and expose power alert/error
- fix minerpool stats parsing and convert pool hashrate to MH/s
- sum alerts across miner, powermeter and container entries
- limit tail-log query to a 10 minute freshness window
- drop derived miners.sleep, add unit metadata to response fields
- extract aggregation helpers to site.utils.js

* feat: GET /site/power-consumption history API (#96)

* Feat: Inventory v3 additions (#95)

* Feat: add ACME Container inventory location

Insert 'ACME Container' between Vendor and Scrapped in MINER_LOCATIONS
per Inventory v3 spec (Site overview locations table).

Asana: T1

* Feat: expand work order types to 1/2/3/4

Redefine WORK_ORDER_TYPES = REGISTER(1), MOVE(2), MICROBT_MINER(3),
MICROBT_NON_MINER(4) per Inventory v3 numbering. Widen the create/list
type enum to [1,2,3,4] and require 'issue' for the MicroBT repair types
(3,4) instead of the old type 2. The IVI-<type>-<seq> numbering already
supports any numeric type. Update REGULAR references (issue rule,
diagnosis parts-seed) to the MicroBT types.

Asana: T2

* Feat: seed a move parts-move for Move Devices work orders

Add the Move(2) branch to createWorkOrder: resolve the part and seed a
role:'move' parts-move capturing fromLocation (part's current location)
and toLocation (requested info.location). Register(1) and MicroBT(3,4)
seeds unchanged; the issue-required remap landed with the type change.

Asana: T4

* Feat: stamp closedAt when a work order is closed

Set info.closedAt on closeWorkOrder so the WO explorer can render a
'date closed' column from listThings (status/assignedTo/deviceType
already present). Auto-closed Register/Move WOs stamp closedAt in the
rack on creation.

Asana: T5

* Feat: add Export RMA (multi-select CSV, MicroBT Miner only)

New GET /auth/work-orders/export/rma?ids=<comma-separated> endpoint that
loads the selected WOs, keeps only MicroBT Miner (type 3) ones, and emits
a CSV with the fixed RMA columns (Ticket, Repaired type, Repaired Miner
Sn, Repaired/Replaced part SN, Repaired Analyze/Treatment, Remark, Miner
Model, Repair Date, Engineer). renderRmaCsv reuses csvEscape and reads
repaired/replacement parts from partsMoves roles. The per-WO PDF
('Export Selected') is deferred until its format is finalized.

Asana: T7

* Fix: skip capCheck under --noauth so dev/local mode can serve /auth routes

authLib is only constructed when not in noAuth mode, but createAuthOnRequest
still ran capCheck (which calls ctx.authLib.tokenHasPerms) for any route with
perms, so every /auth route 500'd under --noauth. Guard capCheck with
!ctx.noAuth. Production 'start' never sets noAuth, so permission enforcement
is unchanged there. Enables local curl verification of the work-order routes.

Not part of the inventory tickets — separate so it can be dropped/owned independently.

* Refactor: move RMA_COLUMNS to constants

Move the fixed RMA_COLUMNS list to workers/lib/constants.js and import it
in work.order.export.js. Repair-date formatting is inlined in renderRmaCsv
(single use, no shared helper). No behaviour change.

* Style: remove explanatory comments

* Refactor: align MINER_LOCATIONS with FE dotted-lowercase format

Per FE/BE alignment on PR #95: switch all location values from
Title Case display strings (e.g. 'Site Warehouse') to dotted-lowercase
keys (e.g. 'site.warehouse'). The set is the union of the spec's
locations and the FE's existing constant — 11 values total.

- MINER_LOCATIONS: workshop.warehouse, workshop.lab, site.warehouse,
  site.lab, site.container, miner.room, vendor, acme.container,
  scrapped, disposed, unknown
- SPARE_PART_INITIAL_LOCATION: 'Site Warehouse' -> 'site.warehouse'
- Update fixture strings in work-orders + spare-parts tests

NOTE: miningos-wrk-inventory needs the matching change (constants +
_validateLocation set) or the inventory worker will reject these
values with ERR_INVALID_LOCATION. Existing stored rows ('Site
Warehouse', etc.) need a one-time migration to the new keys.

* feat(alerts): filter & search alerts by device tag; fix site deviceId filter (#97)

- expose `message` (device/equipment tag) on historical alerts and whitelist it
  for filter+search on both site and history endpoints
- expose `deviceId` on site alerts so the existing deviceId filter works (alert
  objects previously only carried `id`); aligns site with history
- add a Postman collection for the alerts endpoints

* Feat: batch create endpoints for work orders and spare parts (#98)

* Feat: batch create endpoints for work orders and spare parts

* refactor: use WORK_ORDER_TYPES constants in work order schemas

Replace magic enum values [3, 4] and [1, 2, 3, 4] with the existing
WORK_ORDER_TYPES constant, addressing PR #98 review feedback.

* Feat: move work order relocates the device atomically

A type-2 (move) work order auto-closes on creation, so it must perform the relocation itself — otherwise the closed WO records a move that never happened. createWorkOrder and createWorkOrdersBatch reuse submitWorkOrderAction (now taking an optional rackId) to push an updateThing to the part's own rack before recording the WO.

Claude-Session: https://claude.ai/code/session_01Hk3GYz4TPiRmSs3Qmvv27V

* feat(cooling): redesigned Operations Centre cooling/energy data (#100)

cooling-system: explicit fan-coil valve_tag/temperature_tag map; binary cooling_towers[].vibration_switch (replaces numeric vibration); HVAC C1 control_valves.pressure_bypass.pressure (PIT-7501); miners C1 lines[].differential_pressure[] plus summary inlet/outlet/delta_p averages; mining_room.groups[].rack_statuses (online = live rack power); ambient averaging over real readings (drop only missing).

energy-system: site_pm.current.total used for the SLD aggregate.

metrics: new GET /auth/metrics/cooling historical endpoint (hourly/daily) backed by DCS stat ops; WORKER_TYPES.DCS set to the actual thing type 'dcs-siemens'.

Tests added/updated.

* fix(work-orders): declare partsMoves in PATCH schema (#102)

* feat: record remarks + replacement link on spare-part moves (#104)

PUT /auth/spare-parts/:id now accepts optional `remarks` and
`replacesPartCode`. The server-computed partsMoves entry gains:
- `remarks` (falls back to info.reason) — backs the move REASON column
- `replacesPartCode` + `replacesPartId` + `role: 'replacement'` — links a
  replacement part to the part it replaces (the REPLACES column)

replacesPartCode is validated server-side (must resolve to an existing
part, can't be the part itself), so the partsMoves audit trail stays
non-forgeable. Both fields are optional, move-only metadata and are not
written to the part record; existing payloads are unaffected.

Claude-Session: https://claude.ai/code/session_01UcMhxtedvx5EDRffjJdBRF

* Feat: add work order reopen route (#105)

Claude-Session: https://claude.ai/code/session_018SZfbTWwga6AnWp6HVY35a

* Fix: execute device relocation on Move work orders (#107)

The part rack rejects any location change without info.workOrderId
(ERR_PART_MOVE_REQUIRES_WO), so the Move relocation was silently dropped.
Pre-generate the WO id, pass it as the relocation's workOrderId and as the
WO thing id, and surface relocation push errors instead of swallowing them.

Claude-Session: https://claude.ai/code/session_018SZfbTWwga6AnWp6HVY35a

* feat: forecast manual override (#108)

* feat: add PDU layout endpoint (#109)

* feat: server-side alerts filtering with operators and `type` param (#110)

* feat: server-side alerts filtering with operators and type param

- Validate `filter` against allow-listed fields + operators
  ($eq,$ne,$in,$nin,$gt,$gte,$lt,$lte); reject others with ERR_INVALID_FILTER.
- Push thing-level filtering down to the listThings / getHistoricalLogs
  queries instead of filtering only in-memory; re-apply on the merged
  result for per-alert and multi-rack correctness.
- Add `type` query param (all|operational|miner) on the site and history
  endpoints, matching the miner base type and its subtypes (e.g. miner-am-s19xp).

* feat: include alert name in site alerts search

Add `name` to SITE_ALERTS_SEARCH_FIELDS so search matches the alert name
(history search already covers it).

* feat: unify alert device fields (type, position) across endpoints

- History now returns the device type as `type` (was `deviceType`), matching
  the site endpoint; history filter field renamed accordingly.
- Site alerts now carry `position` (info.pos) so the Position column is
  populated; both endpoints now expose type/container/position consistently.

* Fix: persist register-WO notes, writable issue field, fresh WO detail (#111)

* Fix: persist register-WO notes, writable issue field, fresh WO detail

- registerSparePart/Batch store the operator note as info.notes on the work order (single omitted it; batch wrote info.note)
- allow issue inside the work-order update schema's info object
- stop caching GET /work-orders/:id so log/issue writes reflect immediately

Claude-Session: https://claude.ai/code/session_01FxFopChofyQ21y8Q2ZzYrT

* Fix: pass empty opts on spare-part registerThing so miner racks accept it

Miner racks require opts on registerThing; the register-device flow now sends opts:{} on the device/part registration (not the WO), letting a warehouse miner register into inventory without relaxing the template validation or adding FE credential fields.

Claude-Session: https://claude.ai/code/session_01FxFopChofyQ21y8Q2ZzYrT

* feat(cooling): populate per-group return and differential pressures (#112)

Group differential pressure now reads supply/return/differential from a
single transmitter per group (group_pressure_sensors as an ordered list),
preferring the DCS-provided differential. Falls back to the legacy
supply/return id-pair model. Response shape is unchanged.

Co-authored-by: Parag More <parag.more@tether.to>

* Fix: overwriteCache on refetch and map miner model in RMA export (#113)

* Fix: bypass in-flight dedup when overwriteCache is set

Claude-Session: https://claude.ai/code/session_01XV2QxZ65mrwUZCBjjMh1W2

* Fix: map miner model name in RMA export

Claude-Session: https://claude.ai/code/session_01XV2QxZ65mrwUZCBjjMh1W2

* Fix: map miner model name in work-order CSV export (#114)

Claude-Session: https://claude.ai/code/session_01XV2QxZ65mrwUZCBjjMh1W2

* feat: merge worker-level alerts into alerts and history endpoints (#115)

Read worker-level alerts from pool-worker ext data (getWrkExtData 'alerts' /
'alerts-history') and merge them into the site alerts and history-alerts
responses. Worker alerts use the same flat shape as thing alerts, so they need
no special-casing. Workers opt in via ALERT_EXT_DATA_WORKER_TYPES.

* fix(cooling): surface C2 towers, C2 pre/post-HX and C1 pressure averages (#116)

* feat(cooling): populate per-group return and differential pressures

Group differential pressure now reads supply/return/differential from a
single transmitter per group (group_pressure_sensors as an ordered list),
preferring the DCS-provided differential. Falls back to the legacy
supply/return id-pair model. Response shape is unchanged.

* fix(cooling): surface C2 towers, C2 pre/post-HX and C1 pressure averages

- Filter cooling_towers by circuit so each Circuit 2 view shows only its
  own tower (COOLING_TOWER vs HVAC_CONDENSER).
- Read Miners C2 pre/post-HX temps from the loop's configured sensors,
  falling back to the heat-exchanger-derived values.
- Average the Miners C1 header pressures over the 16 per-group PTs, with a
  line-level fallback, instead of the unset per-line pressure sensors.

---------

Co-authored-by: Parag More <parag.more@tether.to>

* fix(cooling): keep sensor id on readings even when the tag is unreadable (#117)

Single-sensor summary fields collapsed to null when the tag was not
readable, dropping the sensor label. Add a sensorReading helper that
returns { value, unit, sensor } whenever a sensor is configured (value
null when unreadable), and null only when no sensor is set. Applied to
miners C2 pre/post-HX and tower level, HVAC C2 tower level, and HVAC C1
system pressure.

* fix(cooling): keep units on readings when tags are unreadable (#118)

getSensorReading and getSensorWithTag now return { value: null, unit }
when a configured sensor is present in the snap but its tag isn't
readable (offline devices are still emitted), instead of collapsing to
null and dropping the unit; they return null only when no sensor is
configured. Also surface makeup pump speed/current and return it by
config id so it is present with null values when offline.

* fix(cooling): include per-group PTs in miners C1 supply/return tables (#119)

The FE builds the SUPPLY/RETURN tables from line.supply.sensors /
line.return.sensors and the delta-P table from differential_pressure.
Append each group's supply/return PT (type 'Pressure - Group N') to the
sensor lists so the per-group pressures render, and number groups
absolutely (line1 1-8, line2 9-16) since the FE prints entry.group.

* feat(cooling): surface make-up water system on miners circuit 1 (#121)

Extract the makeup block builder shared with circuit 2 and include it in
the circuit 1 and layout views. The make-up pump now comes only in the
makeup block instead of the circulation pumps array, the tank
description carries its volume, on/off valves report is_open from the
valve status, and the circuit 1 projection fetches levels and tanks.

* user perms update (#122)

* user perms update

* forecast perms

* remove unused property

* perm fix

* get users with read perms

* fix: site manager perms fix (#123)

* fix: site manager perms fix

* user perms

* user perms

* json syntax

* fix: site manager users listing (#124)

* fix: check read-level perms on GET routes (#125)

GET/HEAD routes going through the shared route helpers were always
checked at write level, so read-only roles got 401 on read APIs
(work-orders, spare-parts, site, history). Derive the check level
from the HTTP method instead.

* remove pool config perms for site operator (#126)

* Fix: average metrics buckets instead of summing samples, add interval (#128)

* Fix: average metrics buckets instead of summing samples, add interval

/auth/metrics/hashrate, /consumption and /efficiency read the pre-aggregated
field over tail-log with shouldCalculateAvg instead of summing every sample in
a range-aggr window, which inflated hashrate ~288x and consumption ~1440x.

Bucket size is now selectable via interval (1h/1d/1w) through the existing
resolveInterval/getIntervalConfig helpers, and interval is part of the cache
key so buckets of different sizes no longer poison each other.

Claude-Session: https://claude.ai/code/session_015M7BptTmTQj1pwNz2LKRNT

* Fix: drop summary.totalHashrateMhs, a sum of rates over time

totalHashrateMhs was the sum of per-bucket hashrate levels, so it equalled
avgHashrateMhs * bucketCount and moved with the requested interval rather than
with fleet output. Summing an instantaneous rate across time is not a physical
quantity, unlike consumption where powerW pairs with an integrated
consumptionMWh.

Dropped from both the ungrouped and grouped summaries, including the per-group
breakdown. The field was unused by the UI.

Claude-Session: https://claude.ai/code/session_015M7BptTmTQj1pwNz2LKRNT

* feat(site): surface miner cooling system status on the live status header (#131)

On central DCS sites the site status live API now returns
cooling.status (Running/Off/Unavailable) read from the status the
miningos-wrk-dcs-siemens worker publishes ready-made at
stats.dcs_specific.cooling_system.status — app-node only passes it
through, falling back to Unavailable while the DCS tag is not
provisioned or the DCS thing is missing.

The handler now fetches the DCS thing once per request and derives both
the site consumption (site_main meter) and the cooling status from it,
instead of a separate fetch inside the consumption resolver.

* add groupBy support for metrics/efficiency endpoints (#133)

* Feat: miner-status error field + groupBy=type (#132)

miner-status now emits an `error` count and excludes errored miners from
`online` (previously they were counted as online). Adds groupBy=type, returning
each status as a per-type keyed object, so the doughnut and the status chart can
drop their tail-log calls.

Efficiency groupBy is no longer part of this branch; it shipped in #133.

Claude-Session: https://claude.ai/code/session_015M7BptTmTQj1pwNz2LKRNT

* Feat: add /auth/metrics/miners/by-container snapshot rollup (#135)

* Feat: add /auth/metrics/miners/by-container snapshot rollup

New GET endpoint returning a latest-snapshot rollup keyed by container:
miner count, online/offline/error/not-mining/sleep counts, power-mode
breakdown, hashrate, power, efficiency and temperature. All values come from
the container-grouped aggregates the rack already computes (STAT_RTD), merged
across orks the same way /auth/site/overview/groups does.

Replaces the per-container tail-log snapshots the frontend currently assembles
by hand on the container widgets, home tab, site overview and the efficiency
mining-unit and energy-report views.

Claude-Session: https://claude.ai/code/session_015M7BptTmTQj1pwNz2LKRNT

* Fix: drop always-null asOf from miners/by-container

stat-rtd entries carry no per-entry ts (only aggrTsRange/aggrCount), so asOf
was always null against real ORK data. Removed rather than ship a dead field.

Claude-Session: https://claude.ai/code/session_015M7BptTmTQj1pwNz2LKRNT

* feat(work-orders): manage miner pos, container and status on move and repair WOs (#136)

Move WOs now clear pos and park the miner in maintenance when it leaves
the miner room, and apply the picked group/socket (plus optional subnet)
when it enters. Device status changes ride along as info.deviceStatus so
the WO workflow status stays untouched. MicroBT repair WOs update the
miner named by info.minerIdentifier since only parts are listed as
devices. partsMoves entries record from/to pos, container and status.

* fix: lastActiveAt field added (#134)

* fix: lastActiveAt field added

* forecast perms and fix tests

* fix tests

* Feat: add /auth/metrics/site/summary and /auth/metrics/inventory/summary (#137)

site/summary: a metrics-namespace alias of the existing getSiteLiveStatus, so
the frontend can pull the site rollup (hashrate, power, efficiency, miner
counts, alerts, pools) from one /auth/metrics/* call instead of assembling it
from tail-log in useHeaderStats. Same handler as /auth/site/status/live.

inventory/summary: a new snapshot rollup keyed by inventory status and location
for miners and for each spare-part type (controller, hashboard, psu), from the
inventory-grouped stat-5m aggregates the racks already compute. Keys pass
through untouched so the UI keeps its own labels. Replaces the two grouped
tail-log calls in the Inventory dashboard.

Claude-Session: https://claude.ai/code/session_015M7BptTmTQj1pwNz2LKRNT

* Feat: add /auth/metrics/revenue/hourly (#138)

New endpoint returning hourly pool revenue estimates: reads the pool worker's
hourlyRevenues (produced by _aggrTransactions on aggrHourly queries) via
getWrkExtData, merges the buckets across orks, and returns { log: [{ ts,
revenueBTC }], summary: { totalRevenueBTC } }. Takes an optional pool param
(minerpool-<pool>), defaulting to the generic minerpool type.

Depends on tetherto/miningos-wrk-ork#38, which adds the aggregation op that
lets hourlyRevenues survive the ORK re-aggregation.

Claude-Session: https://claude.ai/code/session_015M7BptTmTQj1pwNz2LKRNT

* Feat: hashrate container/current params + container-history interval (#139)

/auth/metrics/hashrate: optional `container` reads a single container from the
container-group aggregate the racks already compute, and optional `current`
adds summary.currentHashrateMhs from the latest stat-rtd sample. Both are
opt-in; without them the request, the RPC payload and the response are
unchanged.

/auth/metrics/containers/:id/history: optional `interval` selects the stat key
(20s, 1m, 5m, 30m, 3h, 1d) instead of always reading stat-5m. Container racks
schedule 20s/1m/rtd on top of the thing defaults, so every interval is real
data. Defaults to stat-5m.

Together these close the chart call sites that the timeseries scoping found
uncovered, without a generic tail-log passthrough endpoint.

Claude-Session: https://claude.ai/code/session_01GhvfQJep6PVSLJbuime6GF

* fix: /metrics/consumption Use siemens worker power consumption if centralDCSSetup is enabled (#140)

* feat: split site alerts summary into operational/miner and sort severity by rank (#142)

* validations and permissions fix (#144)

* validations and permissions fix

* fix tests

* node version update

* fix(logs): retry dropped RPC channels and bound unlimited tail-log ranges (#141)

* fix(logs): retry dropped RPC channels and bound unlimited tail-log ranges

A pooled protomux-rpc channel can be torn down between requests, so the
next jRequest that reuses it fails with CHANNEL_CLOSED while an immediate
re-dial succeeds. Because requestDataMap did not catch it, a single dead
channel rejected the whole request and surfaced to the client as a 400.

- retry the transient channel failures in a shared _jRequest wrapper used
  by all three RPC paths: 3 attempts with linear backoff, propagating any
  non-retryable error immediately and the final attempt unchanged.
- make requestDataMap degrade instead of failing: a store that still fails
  after its retries is dropped with a warning, so one flaky ork no longer
  loses the response. When every store fails the first error propagates,
  since a total outage must not be mistaken for an empty result set.
- reject limitless tail-log ranges whose bucket count would exceed
  TAIL_LOG_MAX_ROWS with ERR_RANGE_TOO_LARGE, rather than streaming
  hundreds of thousands of rows over one channel. Requests that carry
  their own limit are already bounded and are left alone, and limit is now
  capped in the route schemas.

TAIL_LOG_MAX_ROWS is sized to admit every current caller (30 days of
stat-1m is 43.2k buckets) while rejecting the unbounded year-long ranges.

* fix(logs): size the tail-log caps off the UI's real callers

TAIL_LOG_MAX_ROWS = 50000 served two unrelated jobs — the range-guard
budget and the schema's limit maximum — and was justified by a caller
that does not exist: 30 days of limitless stat-1m. The UI's export
coarsens to stat-5m past 6 days precisely so it never asks for that.

Split the two and size each off what the UI actually sends:

- TAIL_LOG_MAX_ROWS = 9000 bounds limitless requests. The only such
  caller is the historical miner KPI export, whose own ladder
  (moria-app-ui useMinerStatKey, #2797) caps it at 8640 rows.
- TAIL_LOG_MAX_LIMIT = 10080 caps a client-supplied limit, set by the
  dashboard power-mode timeline's 7 days of stat-1m. Dropping the cap
  below this would have rejected that chart outright.

Worst case served falls from 50k rows to 9k limitless / 10,080 limited,
with no frontend change.

Tests: replace the 30-day stat-1m case with the export ladder's actual
ceiling (30 days of stat-5m and 6 days of stat-1m, both 8640) and assert
the month of stat-1m is now rejected. New routes test pins the limit cap
at or above the dashboard's 10,080 so lowering it fails loudly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(logs): confine RPC retries to reads and close the range-guard gaps

Two review findings on #141, both in code this branch introduced.

_jRequest retried every method, so it covered the writes too: pushAction,
pushActionsBatch, voteAction, cancelActionsBatch, saveWrkSettings,
storeFile, removeFile, saveThingComment, setGlobalConfig and the energy
setters. A channel can drop after the ork applied the request but before
the response arrives — indistinguishable from a drop before it applied —
so a retried miner control action can execute twice, and a single
pushAction carries no idempotency key to dedupe on.

Gate the retry on RPC_RETRYABLE_METHODS, an allowlist of the 18 read
methods that reach the proxy. Writes keep develop's behaviour of failing
on the first CHANNEL_CLOSED, and an allowlist means the dynamic comment
operation in things.handlers, plus any write added later, defaults to not
retrying.

The range guard also failed open on half of LOG_KEYS: TAIL_LOG_BUCKET_MS
carried 3 of the 7 keys, and assertRangeWithinRowLimit returns early when
a key has no width. key is a bare string in both route schemas, so a
limitless year of stat-20s — about 1.58M buckets — walked straight past
it, as did stat-30m at 17.5k.

- give stat-20s, stat-30m and stat-1D their widths, so all six
  fixed-width keys are checked.
- fail closed on an unrecognised stat-* key with a bounded limitless
  range: ERR_KEY_NOT_RANGE_CHECKABLE, so a key added upstream is
  rejected loudly rather than silently unbounded.
- exempt stat-rtd explicitly via TAIL_LOG_UNBUCKETED_KEYS. It holds the
  latest sample per thing, not a series, so the thing count bounds it
  rather than the range. Non-stat keys stay out of scope.

getContainerHistory was never exposed here: it defaults its limit and the
schema caps it at 1000, so it is bounded either way.

Tests: 5 new in data.proxy.test.js pinning that writes attempt exactly
once on both proxy paths, that an unlisted method does not retry, and
that reads still retry through requestDataAllPages. 4 new in
logs.handlers.test.js covering the three added keys, the fail-closed
path, and the limit exemption still applying to a key it cannot size.
Unit suite 1259/1261, the 2 failures the pre-existing admin_external
ones.

* style: satisfy standard in the central-DCS request params

The multiline ternary added in fd48659 fails standard on four counts
(multiline-ternary twice, comma-dangle twice), which is why `npm run lint`
is red on develop and `npm test` short-circuits before the unit suite ever
runs. CI hides this: ci.yml:134 pipes lint through `|| echo "::warning::"`,
so the pipeline stays green either way.

Reindent to the operator-first form standard wants and drop the trailing
commas. No behaviour change — same two branches, same keys.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: /metrics/efficiency Use siemens worker power consumption if centralDCSSetup is enabled (#143)

* fix(metrics): normalize grouped range-string timestamps (#145)

* fix(metrics): normalize grouped range-string timestamps

Report charts showed 1970-01-01 as the first X-axis value on the single-site
Operations Dashboard, Efficiency and Energy pages.

With groupRange set, the worker returns ts as a range string such as
"1770854400000-1771459199999" rather than a number. parseEntryTs exists for
exactly this and is documented as such, but six sites in this handler read
val.ts raw: the hashrate, consumption and efficiency logs plus their grouped
variants. Those are the ones feeding the affected charts.

Any span over two days resolves to interval '1d' -> groupRange '1D', and the
dashboards default to seven days, so the default view was always the grouped
one. The UI did _toNumber() on the range string, got NaN, and plotted the
point at the epoch; once it started rejecting undated readings the same
entries were dropped and the charts read "No data" instead.

power.consumption.handlers.js also reads entry.ts raw but never passes
groupRange -- it pins a 5m interval -- so its entries are always numeric and
it is left alone.

The two failing admin_external tests are unrelated and fail on a clean tree.

* fix(finance): normalize the grouped range-string ts on the energy stats too

Same bug as the previous commit, a second handler, and a quieter symptom.

`getEnergyBalance` and `getRevenueSummary` both request the electricity worker's
stats-history with `groupRange: '1D'`, so `ts` comes back as a range string. Their shared
`processEnergyData` read it raw:

  const ts = getStartOfDay(item.ts || item.timestamp)   // NaN on a range string
  if (!ts) continue                                     // ... so every row is dropped

Unlike the metrics charts this fails silently: nothing lands in the daily map, so
`activeEnergyIn` and `uteEnergy` are 0 and the report reads `curtailmentMWh: null`,
`curtailmentRate: null`, `operationalIssuesRate: null` on days that have the data. A
missing metric looks like a site that was never curtailed rather than a parse failure,
which is why nobody reported it.

`parseEntryTs` where the value is read, exactly as in metrics.handlers.js.

The other raw `ts` reads in this file are left alone deliberately: they consume tail-log
range-aggr, minerpool transactions and mempool prices, none of which pass `groupRange`,
so their entries are numeric.

Tests: one case through `getEnergyBalance` asserting curtailment is derived from a
range-string bucket rather than dropped. It fails on the unfixed tree with
`curtailmentMWh: null`.

`brittle tests/unit/handlers/finance.handlers.test.js` -> 71/71 pass, 293/293 asserts.
Full unit suite 1310/1312, the two failures being the pre-existing `admin_external`
cases that fail identically on a clean tree.

* version update (#149)

* version update

* test mock fix

* Feat: miner replacement in move work orders (#148)

* feat(export): streaming data-export endpoint for UI report downloads (#147)

* feat(export): add streaming data-export endpoint for UI report downloads

Add GET /auth/export, a single type-discriminated endpoint that streams
CSV/JSON report files as browser downloads, replacing the client-side
Blob generation in moria-app-ui. Export types are registered as modules
(miner-stats, container-miner-stats, forecast-overview,
historical-forecast, historical-miner-kpi), each reusing the dataProxy
calls behind the existing data routes and serialized incrementally so
memory stays constant regardless of export size.

* feat(export): require reporting and forecast read perms per export type

* refactor(export): detect system power containers by stats instead of name

---------

Co-authored-by: Parag More <parag.more@tether.to>

* fix(export): correct RMA replaced-part and treatment columns (#151)

Co-authored-by: Parag More <parag.more@tether.to>

* feat(metrics): add timeRange to range-aggregated log entries (#152)

Co-authored-by: Parag More <parag.more@tether.to>

* Feat: move a miner's attached parts with it (#155)

* add by meter cosumption data to metrics/consumption endpoint (#160)

* basic consumption by meter

* add by meter cosumption data to metrics/consumption endpoint

* cleanup

* Feat: reporting tool cost parameters, nominal hashrate and weekly ebitda (#161)

- costParameters global-data type (amortization, margin, pinned LCOE), served
  by the existing GET/POST /auth/global/data — no new endpoint
- opt-in nominal=true on /auth/metrics/hashrate: per-bucket nominal hashrate
  and pctOfNominal from the aggregate the same RPC already returns
- weekly period on /auth/finance/ebitda
- energy cost derived from consumption x LCOE for months saved without one,
  so Cost Input dropping its energy column cannot silently zero EBITDA

All additive: absent the new query param and the new type, every response is
unchanged.

* Work Orders Scheme, Provide device Status on Info (#162)

* fix: Select first device as miner in the repair (#164)

* feat(metrics): serve the dashboard power-mode timeline server-side (#165)

The dashboard currently pulls 7 days of 1m tail-log rows (limit 10080) with
per-miner group objects into the browser and run-length-encodes them on the
main thread. The power-mode/timeline endpoint existed but was stuck at stat-3h
and dropped miners that report a status without a power mode, so the UI could
not switch over.

The endpoint now resolves resolution from the range (<=7d 1m, <=30d 30m, else
3h) with an optional interval override, fetches the tail log in bounded
720-sample windows, and folds segments in a streaming aggregator so memory
scales with mode changes rather than samples x fleet size. Power mode falls
back to status the same way the UI computes it.

* feat(devices): serve explorer containers and cabinets lists fully server-side (#166)

The explorer cabinets tab still pulls the whole sensor fleet via
list-things and groups/sorts/paginates in the bro…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants