diff --git a/README.md b/README.md index 989ea1e..fc4d226 100644 --- a/README.md +++ b/README.md @@ -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. -![Version](https://img.shields.io/badge/version-0.10.6_Beta-blue) +![Version](https://img.shields.io/badge/version-0.10.7_Beta-blue) ![License](https://img.shields.io/badge/license-AGPLv3-blue) ![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) @@ -150,6 +150,7 @@ Each service supports multi-device management with conflict detection: - Filter by device, type, active status, or search by MAC/IP/hostname - Client detail page with connection history and vendor identification - Historical client count metrics +- Configurable list refresh interval (Not Updating / 30 s / 1 min / 3 min) with preference saved per browser ### Backups - Trigger RouterOS backups on demand via SSH diff --git a/backend/package.json b/backend/package.json index bfb918f..1bbdcc8 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "mikrotik-manager-backend", - "version": "0.10.6-beta", + "version": "0.10.7-beta", "description": "Mikrotik Network Management Platform - Backend API", "main": "dist/index.js", "scripts": { diff --git a/frontend/package.json b/frontend/package.json index 8f585be..a85866a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "mikrotik-manager-frontend", - "version": "0.10.6-beta", + "version": "0.10.7-beta", "private": true, "type": "module", "scripts": { diff --git a/frontend/src/pages/ClientsPage.tsx b/frontend/src/pages/ClientsPage.tsx index c49ffe3..3bff241 100644 --- a/frontend/src/pages/ClientsPage.tsx +++ b/frontend/src/pages/ClientsPage.tsx @@ -1,7 +1,7 @@ -import { useState, useMemo } from 'react'; +import { useState, useMemo, useRef } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; -import { Search, Wifi, Network, Users, X, Pencil, Trash2, ChevronUp, ChevronDown, ChevronsUpDown } from 'lucide-react'; +import { Search, Wifi, Network, Users, X, Pencil, Trash2, ChevronUp, ChevronDown, ChevronsUpDown, RefreshCw } from 'lucide-react'; import { clientsApi } from '../services/api'; import type { Client } from '../types'; import { useCanWrite } from '../hooks/useCanWrite'; @@ -10,6 +10,22 @@ import { formatDistanceToNow } from 'date-fns'; import clsx from 'clsx'; +const REFRESH_OPTIONS = [ + { label: 'Not Updating', value: null }, + { label: '30 seconds', value: 30_000 }, + { label: '1 minute', value: 60_000 }, + { label: '3 minutes', value: 180_000 }, +] as const; + +const STORAGE_KEY = 'clients-refresh-interval'; + +function readStoredInterval(): number | null { + const raw = localStorage.getItem(STORAGE_KEY); + if (raw === 'null') return null; + const n = Number(raw); + return REFRESH_OPTIONS.some((o) => o.value === n) ? n : 30_000; +} + function ClientModal({ client, onClose, @@ -113,8 +129,17 @@ export default function ClientsPage() { const [editingClient, setEditingClient] = useState(null); const [sortCol, setSortCol] = useState('last_seen'); const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc'); + const [refreshInterval, setRefreshInterval] = useState(readStoredInterval); + const refreshIntervalRef = useRef(refreshInterval); + refreshIntervalRef.current = refreshInterval; const PAGE_SIZE = 50; + const handleIntervalChange = (val: number | null) => { + setRefreshInterval(val); + refreshIntervalRef.current = val; + localStorage.setItem(STORAGE_KEY, String(val)); + }; + const toggleSort = (col: string) => { if (sortCol === col) setSortDir((d) => d === 'asc' ? 'desc' : 'asc'); else { setSortCol(col); setSortDir('asc'); } @@ -126,7 +151,7 @@ export default function ClientsPage() { clientsApi .list({ search: search || undefined, active: showAll ? undefined : true, limit: PAGE_SIZE, offset: page * PAGE_SIZE }) .then((r) => r.data), - refetchInterval: 30_000, + refetchInterval: refreshInterval ?? false, }); const [purgeResult, setPurgeResult] = useState(''); @@ -140,7 +165,11 @@ export default function ClientsPage() { }); useSocket({ - 'clients:updated': () => queryClient.invalidateQueries({ queryKey: ['clients'] }), + 'clients:updated': () => { + if (refreshIntervalRef.current !== null) { + queryClient.invalidateQueries({ queryKey: ['clients'] }); + } + }, }); const rawClients = data?.clients ?? []; @@ -227,6 +256,23 @@ export default function ClientsPage() { {purgeResult && ( {purgeResult} )} +
+ + +
{isLoading ? ( diff --git a/frontend/src/version.ts b/frontend/src/version.ts index 4a06f52..639680d 100644 --- a/frontend/src/version.ts +++ b/frontend/src/version.ts @@ -1 +1 @@ -export const APP_VERSION = 'v0.10.6 Beta'; +export const APP_VERSION = 'v0.10.7 Beta';