fix: syslog coverage count exceeding device total + remove decorative live indicator (v0.11.1 Beta)

- aggregateRules/aggregateActions: deduplicate coverage per device using a per-device seenKeys set,
  preventing a device with duplicate topic|action rules from inflating the coverage count above total
- executePush (add path): skip devices that already have an identical rule/action before pushing to
  all devices, preventing duplicate RouterOS logging entries from being created in the first place
- TopBar: remove hardcoded decorative 'live' indicator that had no backend connection logic

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2GT-Rich
2026-05-15 16:39:07 -07:00
co-authored by Claude Sonnet 4.6
parent 4abc96f015
commit e692a1d7cd
6 changed files with 27 additions and 23 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
A self-hosted, full-stack network management platform for MikroTik devices. Monitor, configure, and manage your entire MikroTik infrastructure — routers, switches, and wireless access points — from a single web interface. A self-hosted, full-stack network management platform for MikroTik devices. Monitor, configure, and manage your entire MikroTik infrastructure — routers, switches, and wireless access points — from a single web interface.
![Version](https://img.shields.io/badge/version-0.11.0_Beta-blue) ![Version](https://img.shields.io/badge/version-0.11.1_Beta-blue)
![License](https://img.shields.io/badge/license-AGPLv3-blue) ![License](https://img.shields.io/badge/license-AGPLv3-blue)
![Docker](https://img.shields.io/badge/docker-compose-2496ED?logo=docker&logoColor=white) ![Docker](https://img.shields.io/badge/docker-compose-2496ED?logo=docker&logoColor=white)
![TypeScript](https://img.shields.io/badge/TypeScript-5.3-3178C6?logo=typescript&logoColor=white) ![TypeScript](https://img.shields.io/badge/TypeScript-5.3-3178C6?logo=typescript&logoColor=white)
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "mikrotik-manager-backend", "name": "mikrotik-manager-backend",
"version": "0.11.0-beta", "version": "0.11.1-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": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "mikrotik-manager-frontend", "name": "mikrotik-manager-frontend",
"version": "0.11.0-beta", "version": "0.11.1-beta",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
-15
View File
@@ -41,21 +41,6 @@ export default function TopBar({ onMenuClick }: TopBarProps) {
</div> </div>
<div className="flex items-center gap-2 flex-shrink-0"> <div className="flex items-center gap-2 flex-shrink-0">
{/* Live indicator */}
<span
className="hidden sm:flex items-center gap-[6px] px-[10px] py-[4px] rounded-full text-[11px] mono"
style={{ color: 'var(--ink-3)', border: '1px solid var(--line)' }}
>
<span
className="w-[6px] h-[6px] rounded-full flex-shrink-0"
style={{
background: 'var(--good)',
boxShadow: '0 0 0 2px rgba(141,224,138,0.2), 0 0 6px rgba(141,224,138,0.5)',
}}
/>
live
</span>
{/* Theme toggle */} {/* Theme toggle */}
<button <button
onClick={toggleTheme} onClick={toggleTheme}
@@ -44,10 +44,14 @@ function aggregateActions(
): AggregatedRow[] { ): AggregatedRow[] {
const map = new Map<string, AggregatedRow>(); const map = new Map<string, AggregatedRow>();
for (const d of deviceData) { for (const d of deviceData) {
const seenKeys = new Set<string>();
for (const a of d.actions) { for (const a of d.actions) {
const key = a['name'] ?? ''; const key = a['name'] ?? '';
if (!map.has(key)) map.set(key, { key, sample: a, coverage: [] }); if (!map.has(key)) map.set(key, { key, sample: a, coverage: [] });
map.get(key)!.coverage.push({ deviceId: d.deviceId, deviceName: d.deviceName, rowId: a['.id'] ?? '' }); if (!seenKeys.has(key)) {
map.get(key)!.coverage.push({ deviceId: d.deviceId, deviceName: d.deviceName, rowId: a['.id'] ?? '' });
seenKeys.add(key);
}
} }
} }
return Array.from(map.values()); return Array.from(map.values());
@@ -58,10 +62,14 @@ function aggregateRules(
): AggregatedRow[] { ): AggregatedRow[] {
const map = new Map<string, AggregatedRow>(); const map = new Map<string, AggregatedRow>();
for (const d of deviceData) { for (const d of deviceData) {
const seenKeys = new Set<string>();
for (const r of d.rules) { for (const r of d.rules) {
const key = `${r['topics'] ?? ''}|${r['action'] ?? ''}`; const key = `${r['topics'] ?? ''}|${r['action'] ?? ''}`;
if (!map.has(key)) map.set(key, { key, sample: r, coverage: [] }); if (!map.has(key)) map.set(key, { key, sample: r, coverage: [] });
map.get(key)!.coverage.push({ deviceId: d.deviceId, deviceName: d.deviceName, rowId: r['.id'] ?? '' }); if (!seenKeys.has(key)) {
map.get(key)!.coverage.push({ deviceId: d.deviceId, deviceName: d.deviceName, rowId: r['.id'] ?? '' });
seenKeys.add(key);
}
} }
} }
return Array.from(map.values()); return Array.from(map.values());
@@ -469,14 +477,25 @@ export default function NetworkServicesSyslogPage() {
); );
targetNames = coverage.map(c => c.deviceName); targetNames = coverage.map(c => c.deviceName);
} else { } else {
// Skip devices that already have an identical action/rule to avoid duplicates
const devicesToAdd = onlineDevices.filter(d => {
const deviceSyslog = allDevicesSyslog.find(ds => ds.deviceId === d.id);
if (!deviceSyslog) return true;
if (type === 'action') {
const newName = data['name'] ?? '';
return !deviceSyslog.actions.some(a => (a['name'] ?? '') === newName);
}
const newKey = `${data['topics'] ?? ''}|${data['action'] ?? ''}`;
return !deviceSyslog.rules.some(r => `${r['topics'] ?? ''}|${r['action'] ?? ''}` === newKey);
});
settled = await Promise.allSettled( settled = await Promise.allSettled(
onlineDevices.map(d => devicesToAdd.map(d =>
type === 'action' type === 'action'
? networkServicesApi.addSyslogAction(d.id, data) ? networkServicesApi.addSyslogAction(d.id, data)
: networkServicesApi.addSyslogRule(d.id, data) : networkServicesApi.addSyslogRule(d.id, data)
) )
); );
targetNames = onlineDevices.map(d => d.name); targetNames = devicesToAdd.map(d => d.name);
} }
setIsPushing(false); setPendingPush(null); setIsPushing(false); setPendingPush(null);
+1 -1
View File
@@ -1 +1 @@
export const APP_VERSION = 'v0.11.0 Beta'; export const APP_VERSION = 'v0.11.1 Beta';