Repository navigation
Add hvac circuit 1 and layout api updates - #58
Merged
Merged
Conversation
boris91
approved these changes
Apr 21, 2026
tekwani
approved these changes
Apr 21, 2026
mukama
approved these changes
Apr 21, 2026
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…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.