fix(lte): data-cap usage was never counted (v0.23.26-beta)
Reported from the field: manual SMS send works, but no traffic is accumulated. Correct on both counts, and the cause is a single wrong call shape made invisible by a swallowed error. The interface filter was passed as a parameter rather than a RouterOS query word, so the request went out as `=?name=lte1` and the device answered "unknown parameter ?name". That threw on every poll, a `.catch(() => [])` turned it into an empty result, and the empty result made the tracker return early — every time, in silence. The rule looked configured, the usage bar sat at zero, and nothing anywhere said why. Proved against hardware rather than reasoned about: as-shipped -> THREW: unknown parameter ?name query-word -> rows: 1 ether1 rx=4997765728 The filter now goes through the client's query-word argument, falls back to an unfiltered read if a filtered one comes back empty, and — the part that matters more — logs instead of discarding. A counter read that fails, or an interface that is not in the result, now says so. The tracker also warns when it has no counters, because a rule that exists and never counts is precisely the failure the user experiences. Accumulation verified end to end on real hardware afterwards: first poll establishes a baseline and counts nothing, second and third accumulate 118 KB then 144 KB, with the period keyed to the configured local date. Worth recording why this shipped. The pure accounting logic has eighteen tests and all of them pass — reboot handling, midnight rollover across zones, margin, cooldown. None of it ran, because the one integration point those tests cannot reach was wrong. Verifying the API paths and the configuration flow, as was done before release, exercised everything except the thing the feature is for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
864ca40427
commit
19e2d914f5
@@ -6,7 +6,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://img.shields.io/badge/version-0.23.25_Beta-blue" alt="Version" />
|
<img src="https://img.shields.io/badge/version-0.23.26_Beta-blue" alt="Version" />
|
||||||
<img src="https://img.shields.io/badge/license-AGPLv3-blue" alt="License" />
|
<img src="https://img.shields.io/badge/license-AGPLv3-blue" alt="License" />
|
||||||
<img src="https://img.shields.io/badge/docker-compose-2496ED?logo=docker&logoColor=white" alt="Docker" />
|
<img src="https://img.shields.io/badge/docker-compose-2496ED?logo=docker&logoColor=white" alt="Docker" />
|
||||||
<img src="https://img.shields.io/badge/TypeScript-5.3-3178C6?logo=typescript&logoColor=white" alt="TypeScript" />
|
<img src="https://img.shields.io/badge/TypeScript-5.3-3178C6?logo=typescript&logoColor=white" alt="TypeScript" />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mikrotik-manager-backend",
|
"name": "mikrotik-manager-backend",
|
||||||
"version": "0.23.25-beta",
|
"version": "0.23.26-beta",
|
||||||
"description": "Mikrotik Network Management Platform - Backend API",
|
"description": "Mikrotik Network Management Platform - Backend API",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -2865,13 +2865,36 @@ export class DeviceCollector {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Current byte counters for one interface, for data-cap accounting. */
|
/**
|
||||||
|
* Current byte counters for one interface, for data-cap accounting.
|
||||||
|
*
|
||||||
|
* The filter is a RouterOS *query word* and has to be passed as one. Smuggled
|
||||||
|
* in as a parameter it becomes `=?name=lte1`, which the device rejects with
|
||||||
|
* "unknown parameter ?name" — and because that error was being swallowed, the
|
||||||
|
* data-cap counter silently read nothing on every poll while the rest of the
|
||||||
|
* feature looked healthy. Errors here are logged rather than discarded for
|
||||||
|
* exactly that reason.
|
||||||
|
*/
|
||||||
async getInterfaceCounters(name: string): Promise<{ rxBytes: number; txBytes: number } | null> {
|
async getInterfaceCounters(name: string): Promise<{ rxBytes: number; txBytes: number } | null> {
|
||||||
const rows = await this.client
|
let rows: Record<string, string>[] | null = await this.client
|
||||||
.execute('/interface/print', { stats: '', '?name': name })
|
.execute('/interface/print', { stats: '' }, [`?name=${name}`])
|
||||||
.catch(() => [] as Record<string, string>[]);
|
.catch((err) => {
|
||||||
const row = rows.find((r) => r['name'] === name) || rows[0];
|
console.error(`[${this.device.name}] counter read for ${name} failed:`, (err as Error).message);
|
||||||
if (!row) return null;
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Some versions answer a filtered print with nothing at all; falling back to
|
||||||
|
// the full list costs one call and removes a whole class of silent zero.
|
||||||
|
if (rows && rows.length === 0) {
|
||||||
|
rows = await this.client.execute('/interface/print', { stats: '' }).catch(() => null);
|
||||||
|
}
|
||||||
|
if (!rows) return null;
|
||||||
|
|
||||||
|
const row = rows.find((r) => r['name'] === name);
|
||||||
|
if (!row) {
|
||||||
|
console.warn(`[${this.device.name}] interface ${name} not present in counter read`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
rxBytes: parseInt(row['rx-byte'] || '0', 10),
|
rxBytes: parseInt(row['rx-byte'] || '0', 10),
|
||||||
txBytes: parseInt(row['tx-byte'] || '0', 10),
|
txBytes: parseInt(row['tx-byte'] || '0', 10),
|
||||||
@@ -3034,7 +3057,12 @@ export class DeviceCollector {
|
|||||||
if (!rule) return;
|
if (!rule) return;
|
||||||
|
|
||||||
const counters = await this.getInterfaceCounters(interfaceName);
|
const counters = await this.getInterfaceCounters(interfaceName);
|
||||||
if (!counters) return;
|
if (!counters) {
|
||||||
|
// A rule that exists but never counts is the failure the user actually
|
||||||
|
// sees, so it must not be silent.
|
||||||
|
console.warn(`[DataCap] ${this.device.name}/${interfaceName}: no counters available; usage not updated`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const key = periodKey(now, rule.reset_hour, rule.reset_minute, rule.timezone);
|
const key = periodKey(now, rule.reset_hour, rule.reset_minute, rule.timezone);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mikrotik-manager-frontend",
|
"name": "mikrotik-manager-frontend",
|
||||||
"version": "0.23.25-beta",
|
"version": "0.23.26-beta",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export const APP_VERSION = 'v0.23.25 Beta';
|
export const APP_VERSION = 'v0.23.26 Beta';
|
||||||
|
|||||||
Reference in New Issue
Block a user