diff --git a/README.md b/README.md
index 932eed8..b86f0e8 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
-
+
diff --git a/backend/package.json b/backend/package.json
index be0c8f4..e349ba8 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -1,6 +1,6 @@
{
"name": "mikrotik-manager-backend",
- "version": "0.23.25-beta",
+ "version": "0.23.26-beta",
"description": "Mikrotik Network Management Platform - Backend API",
"main": "dist/index.js",
"scripts": {
diff --git a/backend/src/services/mikrotik/DeviceCollector.ts b/backend/src/services/mikrotik/DeviceCollector.ts
index 0192bdf..eed8ab4 100644
--- a/backend/src/services/mikrotik/DeviceCollector.ts
+++ b/backend/src/services/mikrotik/DeviceCollector.ts
@@ -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> {
- const rows = await this.client
- .execute('/interface/print', { stats: '', '?name': name })
- .catch(() => [] as Record[]);
- const row = rows.find((r) => r['name'] === name) || rows[0];
- if (!row) return null;
+ let rows: Record[] | null = await this.client
+ .execute('/interface/print', { stats: '' }, [`?name=${name}`])
+ .catch((err) => {
+ console.error(`[${this.device.name}] counter read for ${name} failed:`, (err as Error).message);
+ 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 {
rxBytes: parseInt(row['rx-byte'] || '0', 10),
txBytes: parseInt(row['tx-byte'] || '0', 10),
@@ -3034,7 +3057,12 @@ export class DeviceCollector {
if (!rule) return;
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 key = periodKey(now, rule.reset_hour, rule.reset_minute, rule.timezone);
diff --git a/frontend/package.json b/frontend/package.json
index 387bb92..b84ede0 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "mikrotik-manager-frontend",
- "version": "0.23.25-beta",
+ "version": "0.23.26-beta",
"private": true,
"type": "module",
"scripts": {
diff --git a/frontend/src/version.ts b/frontend/src/version.ts
index 28ccace..4b75adb 100644
--- a/frontend/src/version.ts
+++ b/frontend/src/version.ts
@@ -1 +1 @@
-export const APP_VERSION = 'v0.23.25 Beta';
+export const APP_VERSION = 'v0.23.26 Beta';