LAN-Scanner: Sparkline breiter/schnelleres Fenster, Traffic in MB/s
TrafficSparkline-Breite 50pt→200pt, Zeitfenster 10s→30s, Poll-Intervall 3s→0,1s. Traffic-Anzeige von auto-skaliertem kbps/Mbps/Gbps auf festes MB/s umgestellt. Alles per Nutzeranfrage iterativ angepasst, noch nicht live gegen echten Router getestet (siehe found.md Fund 4). Manual.md + Manual.pdf mit aktualisiert (build-manual.py). Fund 5 (App-Settings/ Preferences-Bereich) als offene Feature-Idee in found.md notiert, noch nicht umgesetzt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -326,9 +326,10 @@ selbst verwaltet.
|
||||
Zeigt alle Geräte im Netzwerk (aus DHCP-Leases und ARP-Tabelle),
|
||||
gruppiert nach physischem Port. „Neu scannen“ fragt Leases/ARP/
|
||||
Bridge-Host-Tabelle erneut ab. Jede Port-Überschrift zeigt zusätzlich den
|
||||
aktuellen Live-Durchsatz (↓/↑, grünes Symbol bei aktivem Datenverkehr,
|
||||
graues im Leerlauf) mit kleinem Liniendiagramm der letzten 10 Sekunden.
|
||||
Spalten: Name, IP-Adresse, MAC-Adresse, Status.
|
||||
aktuellen Live-Durchsatz in MB/s (↓/↑, grünes Symbol bei aktivem
|
||||
Datenverkehr, graues im Leerlauf, Abfrage alle 0,1s) mit kleinem
|
||||
Liniendiagramm der letzten 30 Sekunden. Spalten: Name, IP-Adresse,
|
||||
MAC-Adresse, Status.
|
||||
|
||||
Alle Aktionen zu einem Gerät liegen im „Aktionen“-Button (•••) hinter der
|
||||
jeweiligen Zeile:
|
||||
|
||||
BIN
Binary file not shown.
@@ -14,8 +14,9 @@ struct InterfaceTraffic: Equatable {
|
||||
/// One timestamped throughput reading, kept in a short rolling per-port history so the
|
||||
/// LAN-Scanner's port headers can show a small sparkline of the last few seconds, not just the
|
||||
/// current instantaneous value (Nutzerwunsch: "ein kleines Liniendiagramm der letzten 10 Sekunden
|
||||
/// ... pro port"). Timestamped (not just appended in order) so trimming to "last 10 seconds" stays
|
||||
/// correct even if a poll tick is ever delayed or skipped, rather than assuming a fixed cadence.
|
||||
/// ... pro port", window later widened to 30s). Timestamped (not just appended in order) so
|
||||
/// trimming to a fixed window stays correct even if a poll tick is ever delayed or skipped,
|
||||
/// rather than assuming a fixed cadence.
|
||||
struct TrafficSample: Identifiable {
|
||||
let id = UUID()
|
||||
let timestamp: Date
|
||||
|
||||
@@ -156,21 +156,14 @@ struct DevicesView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Same unit-suffix style RouterOS itself reports live traffic in (`SSHTransport.
|
||||
/// parseBitsPerSecond`'s doc comment: "50.7kbps", "34.0kbps") — kept consistent instead of
|
||||
/// inventing a different display format for the same underlying number.
|
||||
/// Fixed MB/s (megabytes/second, not auto-scaling kbps/Mbps/Gbps) per explicit request —
|
||||
/// bits-per-second from RouterOS divided by 8 (bytes) then by 1,000,000 (MB).
|
||||
private static func formatTraffic(_ traffic: InterfaceTraffic) -> String {
|
||||
"↓\(formatBitsPerSecond(traffic.rxBitsPerSecond)) ↑\(formatBitsPerSecond(traffic.txBitsPerSecond))"
|
||||
"↓\(formatMegabytesPerSecond(traffic.rxBitsPerSecond)) ↑\(formatMegabytesPerSecond(traffic.txBitsPerSecond))"
|
||||
}
|
||||
|
||||
private static func formatBitsPerSecond(_ bitsPerSecond: Int) -> String {
|
||||
let units: [(suffix: String, divisor: Double)] = [
|
||||
("Gbps", 1_000_000_000), ("Mbps", 1_000_000), ("kbps", 1_000)
|
||||
]
|
||||
for unit in units where Double(bitsPerSecond) >= unit.divisor {
|
||||
return String(format: "%.1f%@", Double(bitsPerSecond) / unit.divisor, unit.suffix)
|
||||
}
|
||||
return "\(bitsPerSecond)bps"
|
||||
private static func formatMegabytesPerSecond(_ bitsPerSecond: Int) -> String {
|
||||
String(format: "%.2fMB/s", Double(bitsPerSecond) / 8 / 1_000_000)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
@@ -342,9 +335,9 @@ private extension View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Small sparkline next to each port header — last-10-seconds rolling window from
|
||||
/// `DevicesViewModel.portTrafficHistory` (Nutzerwunsch: "ein kleines Liniendiagramm der letzten
|
||||
/// 10 Sekunden ... pro Port"). No axes/labels by design — a trend glance, not a readable chart;
|
||||
/// Small sparkline next to each port header — last-30-seconds rolling window from
|
||||
/// `DevicesViewModel.portTrafficHistory` (window widened from the original 10s per explicit
|
||||
/// follow-up request). No axes/labels by design — a trend glance, not a readable chart;
|
||||
/// the exact current numbers are already shown right next to it via the ↓/↑ text. Stays an empty
|
||||
/// fixed-size placeholder (not collapsing/disappearing) with fewer than two points, so the row
|
||||
/// layout doesn't jump around during the first couple of poll ticks after opening the tab.
|
||||
@@ -369,7 +362,7 @@ private struct TrafficSparkline: View {
|
||||
Color.clear
|
||||
}
|
||||
}
|
||||
.frame(width: 50, height: 16)
|
||||
.frame(width: 200, height: 16)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,10 +39,10 @@ final class DevicesViewModel: ObservableObject {
|
||||
/// interface name), not by device). "unbekannt" (devices RouterOS couldn't resolve onto a
|
||||
/// real port) is never polled — it isn't a real interface.
|
||||
@Published private(set) var portTraffic: [String: InterfaceTraffic] = [:]
|
||||
/// Rolling last-10-seconds history per port, for the small sparkline next to each port
|
||||
/// header (Nutzerwunsch: "ein kleines Liniendiagramm der letzten 10 Sekunden ... pro Port").
|
||||
/// Trimmed by actual elapsed time each tick, not by a fixed sample count, so it stays a true
|
||||
/// "last 10 seconds" window even if a poll tick is ever late.
|
||||
/// Rolling last-30-seconds history per port, for the small sparkline next to each port
|
||||
/// header (window widened from 10s to 30s per explicit follow-up request). Trimmed by
|
||||
/// actual elapsed time each tick, not by a fixed sample count, so it stays a true
|
||||
/// "last 30 seconds" window even if a poll tick is ever late.
|
||||
@Published private(set) var portTrafficHistory: [String: [TrafficSample]] = [:]
|
||||
private let trafficMonitor = InterfaceTrafficMonitor()
|
||||
private var trafficPollingTask: Task<Void, Never>?
|
||||
@@ -61,9 +61,10 @@ final class DevicesViewModel: ObservableObject {
|
||||
self.networkToolsService = networkToolsService
|
||||
}
|
||||
|
||||
/// Same 3s cadence/reasoning as `ConnectViewModel.startTrafficPolling` — re-reads
|
||||
/// `portGroups` every tick so a newly-appearing port (e.g. a VLAN interface added via the
|
||||
/// Setup wizard while this tab is open) gets picked up without restarting the poll.
|
||||
/// 0.1s cadence (faster than `ConnectViewModel.startTrafficPolling`'s 3s, per explicit
|
||||
/// request — this tab's sparkline needs a finer-grained trend, not just the link dot) —
|
||||
/// re-reads `portGroups` every tick so a newly-appearing port (e.g. a VLAN interface added
|
||||
/// via the Setup wizard while this tab is open) gets picked up without restarting the poll.
|
||||
func startTrafficPolling(credentials: RouterOSCredentials) {
|
||||
stopTrafficPolling()
|
||||
trafficPollingTask = Task {
|
||||
@@ -76,13 +77,13 @@ final class DevicesViewModel: ObservableObject {
|
||||
appendTrafficHistory(traffic, at: Date())
|
||||
}
|
||||
}
|
||||
try? await Task.sleep(for: .seconds(3))
|
||||
try? await Task.sleep(for: .milliseconds(100))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func appendTrafficHistory(_ traffic: [String: InterfaceTraffic], at timestamp: Date) {
|
||||
let cutoff = timestamp.addingTimeInterval(-10)
|
||||
let cutoff = timestamp.addingTimeInterval(-30)
|
||||
for (name, sample) in traffic {
|
||||
var history = portTrafficHistory[name] ?? []
|
||||
history.append(TrafficSample(timestamp: timestamp, totalBitsPerSecond: sample.rxBitsPerSecond + sample.txBitsPerSecond))
|
||||
|
||||
@@ -70,3 +70,29 @@ Nachbesserung 6 (User-Wunsch: den Close-Button-Stil aus dem Fokus-Popup bei alle
|
||||
Nachbesserung 7 (User-Feedback: alle Close-Buttons müssen in der Header-Leiste des Popups liegen, nicht doppelt): das untere "Abbrechen" in `ExpertItemEditView` war seit Nachbesserung 6 doppelt zum neuen Kopfzeilen-X — entfernt, einziger Close-Button ist jetzt der im Header. "Anlegen"/"Speichern" bleibt unten als reine Save-Aktion (kein Close). Geprüft: alle übrigen "Abbrechen"-Vorkommen in der App gehören zu nativen `.confirmationDialog`/`.alert`-Dialogen (keine Header-Leiste, nicht betroffen).
|
||||
|
||||
Nachbesserung 8 (User-Wunsch: Verhalten+Layout des Close-Buttons bei ALLEN Popups exakt wie beim Fokus-Popup der Übersicht) — Rückfrage gestellt, ob der Header beim Experte-Bearbeiten-Sheet dafür aus der scrollenden Form raus und fest oben stehen soll (mehr Umbau) oder nur optisch angeglichen wird; User: fest oben, wie Fokus-Popup. Umgesetzt in allen vier Popups (`ExpertItemEditView`, `RawFieldsSheet`, `NetworkToolResultSheet`, `PortScanResultSheet`): identische Struktur wie `OverviewView.focusPanel` — `HStack{Titel; Spacer; xmark.circle.fill-Button}.padding(10)` gefolgt direkt von `Divider()`, außerhalb jeder Form/ScrollView/Section, bleibt beim Scrollen des restlichen Inhalts immer sichtbar. Beim Experte-Sheet wanderte der Header dafür vor die `Form` (Mindesthöhe leicht erhöht: 480→520/idealHeight 620→660, damit durch den zusätzlichen festen Header nichts gestaucht wirkt). Bei den drei Devices-Sheets wurde die bisher gemeinsame Erklärungs-Caption+Divider+Content-Gruppe vom Header getrennt und bekam eigenes Padding. Build+Tests grün, App startet ohne Absturz. Noch nicht live geprüft — bisher nur der Fokus-Popup-Teil (Fund #3) wurde live bestätigt "sieht gut", der Rest noch nicht.
|
||||
|
||||
### 4. LAN-Scanner: Sparkline-Breite + Abfrage-Intervall
|
||||
**Status:** offen (implementiert, bitte live testen)
|
||||
|
||||
Wunsch: Sparkline-Breite verdoppeln, Traffic-Abfrage auf 0,5s setzen (statt 3s).
|
||||
|
||||
Umsetzung: `TrafficSparkline`-Frame in `DevicesView.swift` von 50pt auf 100pt Breite; `DevicesViewModel.startTrafficPolling` von `.seconds(3)` auf `.milliseconds(500)`. Bewusst nur den LAN-Scanner-Poll geändert, nicht `ConnectViewModel`s separate 3s-Schleife (die nur den Link-Punkt treibt, nicht die Sparkline) — User-Scope war explizit "im lan-scanner".
|
||||
|
||||
Nachbesserung 1 (User-Wunsch: nochmal doppelt so breit): 100pt → 200pt.
|
||||
|
||||
Nachbesserung 2 (User-Wunsch: Trafficanzeige in MB, Sparkline-Zeitfenster auf 30s): `formatBitsPerSecond` (kbps/Mbps/Gbps auto-skaliert) ersetzt durch festes `formatMegabytesPerSecond` (Bits/8/1.000.000, 2 Nachkommastellen, "MB/s"). Sparkline-Fenster in `DevicesViewModel.appendTrafficHistory` von 10s auf 30s (`cutoff`). Manual.md-Textstelle + `build-manual.py`-Lauf (PDF neu) mit aktualisiert.
|
||||
|
||||
Nachbesserung 3 (User-Wunsch: Aktualisierungsrate auf 0,1s): `startTrafficPolling` von `.milliseconds(500)` auf `.milliseconds(100)` — bei 30s-Fenster damit bis zu 300 Punkte pro Sparkline/Port (bisher ~60). Nicht weiter geprüft, ob das bei vielen Ports spürbar CPU kostet — bei Bedarf zurückmelden. Manual.md "Abfrage alle 0,1s" + PDF mit aktualisiert. Build grün, App startet ohne Absturz. Noch nicht live geprüft.
|
||||
|
||||
### 5. App-Einstellungen / Settings — eigener Menüpunkt
|
||||
**Status:** offen (nur Feature-Idee notiert, noch nicht umgesetzt)
|
||||
|
||||
Wunsch: eigener Einstellungen-Bereich für Personalisierung, Layout, Farbverwaltung, Refreshraten, Updates, Schriftgrößen, Responsiveness, etc.
|
||||
|
||||
Ist-Zustand: es gibt aktuell **eine einzige** persistierte App-Einstellung (`@AppStorage("appLanguage")`, DE/EN-Umschalter in der Toolbar). Alles andere, was unter "Settings" fallen würde, ist im Code hart verdrahtet und über die Session verstreut — u.a. gerade erst in diesem Fund-Zettel mehrfach angefasste Werte:
|
||||
- Refreshraten: LAN-Scanner-Traffic-Poll `DevicesViewModel.startTrafficPolling` (aktuell 0,1s), Verbinden-Tab-Traffic-Poll `ConnectViewModel.startTrafficPolling` (3s, separat)
|
||||
- Sparkline-Zeitfenster (`DevicesViewModel.appendTrafficHistory`, aktuell 30s) und -Breite (`TrafficSparkline`, aktuell 200pt)
|
||||
- Farben: `OverviewStyle.color(for:)` (Node-Kategorien, Kanten-Arten), Firewall-Icons, Status-Farben im LAN-Scanner (Port-Scan rot/grün/grau)
|
||||
- Übersicht-Zoom-Standardwert, Fokus-Popup-Größe/Verhalten
|
||||
|
||||
Keine echte Settings-Infrastruktur (kein Settings-Tab/-Fenster, kein zentrales Preferences-Model) vorhanden — müsste komplett neu aufgebaut werden. Größerer, noch nicht spezifizierter Umbau; sollte vor Umsetzung geklärt werden: eigener Tab vs. natives macOS-Settings-Fenster (⌘,), welche der o.g. Werte tatsächlich zuerst einstellbar sein sollen, ob pro-Gerät oder global.
|
||||
|
||||
Reference in New Issue
Block a user