Files
RouterOS/RouterOSAssistant/Core/Models/RouterOSSchemaCatalog.swift
T
KayandClaude Sonnet 5 c9ecd3a0a9 M9/M10: Einfach/Experte-Modus + Experte-Tab (generischer RouterOS-Zugriff)
M9: Einrichten-Wizard bekommt einen Einfach/Experte-Modusschalter
(ModeStepView). Einfach überspringt VLAN, erlaubt nur ein LAN-Netzwerk
ohne Isolation, Firewall-Grundschutz fest an.

M10: neuer "Experte"-Tab mit generischem Motor (RouterOSMenuItem,
RouterOSCommand.remove, ConnectionService.fetchMenuItems, freies
"eigener Menüpfad"-Feld) plus kuratierten Formularen mit Tooltips
(RouterOSSchemaCatalog) für Firewall/NAT/Mangle/Raw/Adress-Listen,
Interfaces, IP, VPN, WLAN, Queues, System, Werkzeuge.

Live gegen einen hEX-Testrouter verifiziert (erst per SSH, dann vom
Nutzer selbst in der App), dabei 7 reale Bugs gefunden und gefixt —
der wichtigste: RouterOS' SSH-CLI gibt bei fehlgeschlagenen Befehlen
Exit-Code 0 zurück, wodurch apply() app-weit Fehler verschluckte statt
sie zu melden. Danach ergänzt: Bestätigungsdialog vor Anlegen/Ändern
+ Auto-Backup vor dem ersten Experte-Tab-Schreibvorgang je Sitzung
(Angleichung an den Wizard), sowie ein Dauer-Editor (Tage/Std/Min/Sek)
für Lease-/Ablaufzeit-Felder statt Freitext.

Details zu allen Bugs/Fixes: HANDOFF.md, CHATLOG.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EW3r6rW1xCf6UT5jNvt6rn
2026-09-14 00:03:50 +02:00

722 lines
53 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
/// Curated catalog of RouterOS menus for the Expert tool. Depth is intentionally uneven: the
/// Firewall/NAT/Mangle/Address-List family (what was explicitly asked for) has full field
/// schemas with tooltips and warnings; everything else has enough to browse/add/remove items
/// generically (empty `fields`, so the Expert tool falls back to a raw key=value form for those)
/// while still being properly labeled and explained. RouterOS has 300+ menus in total — this
/// catalog is a curated shortlist of the ones that matter for a home/small-office router, not
/// an exhaustive mirror of the OS. Anything not listed here is still reachable in the Expert
/// tool via its "eigener Menüpfad" free-entry field, generically.
enum RouterOSSchemaCatalog {
static let all: [RouterOSMenuSchema] = firewallFamily + interfaceFamily + ipFamily
+ routingFamily + vpnFamily + wirelessFamily + queueFamily + systemFamily + toolFamily
static func schemas(in category: RouterOSMenuCategory) -> [RouterOSMenuSchema] {
all.filter { $0.category == category }
}
// MARK: - Firewall (fully curated)
private static let firewallFamily: [RouterOSMenuSchema] = [
RouterOSMenuSchema(
menuPath: "/ip firewall filter",
restPath: "ip/firewall/filter",
category: .firewallFilter,
displayName: "Filter-Regeln",
summary: "Entscheidet, ob Pakete durchgelassen (accept), verworfen (drop/reject) oder weiter geprüft werden.",
explanation: "input = Zugriffe auf den Router selbst, forward = Verkehr, der durch den Router hindurchgeht (z.B. LAN↔Internet oder zwischen zwei Netzen), output = vom Router ausgehender Verkehr. Regeln werden von oben nach unten geprüft; die erste passende Regel entscheidet.",
warning: "Reihenfolge zählt. Neue Regeln landen am Ende der Liste — eine bereits vorhandene, weiter oben stehende Regel kann eine neue Regel unerreichbar machen. Position danach mit \"/ip firewall filter print\" kontrollieren, notfalls mit \"move\" verschieben.",
fields: [
RouterOSFieldSchema(key: "chain", label: "Chain", kind: .text,
help: "input = an den Router, forward = durch den Router, output = vom Router aus. Eigene Chains (Sprungziele über \"jump\") sind ebenfalls möglich.",
defaultValue: "forward", required: true),
RouterOSFieldSchema(key: "action", label: "Aktion", kind: .enumPick(options: [
"accept", "drop", "reject", "log", "jump", "return",
"add-src-to-address-list", "add-dst-to-address-list", "fasttrack-connection", "passthrough"
]), help: "Was mit passenden Paketen geschieht. reject schickt zusätzlich eine Fehlermeldung zurück, drop verwirft stillschweigend.",
defaultValue: "accept", required: true),
RouterOSFieldSchema(key: "src-address", label: "Quell-Adresse", kind: .text,
help: "Einzel-IP oder Netz (z.B. 192.168.88.0/24). Leer = beliebig."),
RouterOSFieldSchema(key: "dst-address", label: "Ziel-Adresse", kind: .text,
help: "Einzel-IP oder Netz. Leer = beliebig."),
RouterOSFieldSchema(key: "src-address-list", label: "Quelle in Adress-Liste",
kind: .menuItemPick(menuPath: "/ip firewall address-list", restPath: "ip/firewall/address-list", valueField: "list"),
help: "Nur Pakete, deren Absender in dieser zuvor angelegten Adress-Liste steht."),
RouterOSFieldSchema(key: "dst-address-list", label: "Ziel in Adress-Liste",
kind: .menuItemPick(menuPath: "/ip firewall address-list", restPath: "ip/firewall/address-list", valueField: "list"),
help: "Nur Pakete, deren Ziel in dieser zuvor angelegten Adress-Liste steht."),
RouterOSFieldSchema(key: "in-interface", label: "Eingangs-Interface", kind: .interfacePick,
help: "Nur Pakete, die über dieses Interface hereinkommen."),
RouterOSFieldSchema(key: "out-interface", label: "Ausgangs-Interface", kind: .interfacePick,
help: "Nur Pakete, die über dieses Interface hinausgehen."),
RouterOSFieldSchema(key: "protocol", label: "Protokoll", kind: .enumPick(options: [
"tcp", "udp", "icmp", "gre", "ipsec-esp", "ipsec-ah"
]), help: "IP-Protokoll. Leer = alle."),
RouterOSFieldSchema(key: "dst-port", label: "Ziel-Port(s)", kind: .text,
help: "Nur bei tcp/udp sinnvoll. Einzelner Port (80) oder Bereich (8000-8100)."),
RouterOSFieldSchema(key: "connection-state", label: "Verbindungsstatus", kind: .enumPick(options: [
"new", "established", "related", "invalid", "untracked"
]), help: "Status laut Connection-Tracking. \"established,related\" ist die übliche \"schon erlaubte Verbindung\"-Regel."),
RouterOSFieldSchema(key: "layer7-protocol", label: "Layer7-Protokoll", kind: .text,
help: "Name eines vorher unter \"/ip firewall layer7-protocol\" angelegten Musters (z.B. Erkennung bestimmter Apps). Kostet spürbar CPU, mit Bedacht einsetzen."),
RouterOSFieldSchema(key: "comment", label: "Kommentar", kind: .text,
help: "Nur zur eigenen Wiedererkennung, ohne technische Wirkung."),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool,
help: "Regel inaktiv schalten, ohne sie zu löschen.", defaultValue: "no")
],
listColumns: ["chain", "action", "comment"]
),
RouterOSMenuSchema(
menuPath: "/ip firewall nat",
restPath: "ip/firewall/nat",
category: .firewallNat,
displayName: "NAT-Regeln",
summary: "Übersetzt Adressen — Internetfreigabe (srcnat/masquerade) und Portweiterleitung (dstnat).",
explanation: "srcnat ändert die Absenderadresse ausgehender Pakete (z.B. private LAN-IP → öffentliche WAN-IP). dstnat ändert die Zieladresse eingehender Pakete (z.B. Anfrage an die WAN-IP auf Port 80 → internen Server auf 192.168.88.10:80 umleiten).",
warning: "Eine dstnat-Regel (Portweiterleitung) macht ein internes Gerät direkt aus dem Internet erreichbar — nur für Dienste einrichten, die das wirklich sein sollen, und danach den entsprechenden Port in der Filter-Tabelle nicht blockieren.",
fields: [
RouterOSFieldSchema(key: "chain", label: "Chain", kind: .enumPick(options: ["srcnat", "dstnat"]),
help: "srcnat = Absenderadresse ändern (Internetfreigabe). dstnat = Zieladresse ändern (Portweiterleitung).",
defaultValue: "srcnat", required: true),
RouterOSFieldSchema(key: "action", label: "Aktion", kind: .enumPick(options: [
"masquerade", "src-nat", "dst-nat", "netmap", "redirect", "same"
]), help: "masquerade = automatisches NAT über die aktuelle WAN-IP (empfohlen bei wechselnder IP). src-nat = feste NAT-Adresse. dst-nat = Ziel umschreiben (Portweiterleitung). redirect = auf den Router selbst umlenken.",
defaultValue: "masquerade", required: true),
RouterOSFieldSchema(key: "src-address", label: "Quell-Adresse", kind: .text, help: "Leer = beliebig."),
RouterOSFieldSchema(key: "dst-address", label: "Ziel-Adresse", kind: .text, help: "Leer = beliebig."),
RouterOSFieldSchema(key: "in-interface", label: "Eingangs-Interface", kind: .interfacePick,
help: "Bei dstnat meist der WAN-Port (woher die Anfrage aus dem Internet kommt)."),
RouterOSFieldSchema(key: "out-interface", label: "Ausgangs-Interface", kind: .interfacePick,
help: "Bei srcnat/masquerade meist der WAN-Port."),
RouterOSFieldSchema(key: "protocol", label: "Protokoll", kind: .enumPick(options: ["tcp", "udp", "icmp"]),
help: "Nötig, damit Ports geprüft werden können."),
RouterOSFieldSchema(key: "dst-port", label: "Ziel-Port der Anfrage", kind: .text,
help: "Der Port, auf den die Anfrage von außen ankommt (z.B. 8080)."),
RouterOSFieldSchema(key: "to-addresses", label: "Weiterleiten an (interne IP)", kind: .text,
help: "Nur bei dst-nat: die interne IP-Adresse, an die weitergeleitet wird."),
RouterOSFieldSchema(key: "to-ports", label: "Weiterleiten an (interner Port)", kind: .text,
help: "Nur bei dst-nat: interner Port, falls abweichend vom Ziel-Port (z.B. extern 8080 → intern 80)."),
RouterOSFieldSchema(key: "comment", label: "Kommentar", kind: .text, help: "Nur zur Wiedererkennung."),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool,
help: "Regel inaktiv schalten, ohne sie zu löschen.", defaultValue: "no")
],
listColumns: ["chain", "action", "comment"]
),
RouterOSMenuSchema(
menuPath: "/ip firewall mangle",
restPath: "ip/firewall/mangle",
category: .firewallMangle,
displayName: "Mangle-Regeln",
summary: "Markiert Verbindungen/Pakete zur späteren Weiterverarbeitung (z.B. durch Queues).",
explanation: "Mangle selbst verändert nicht, wie ein Paket behandelt wird — es klebt nur eine Markierung darauf. Erst eine andere Regel (typischerweise eine Queue oder eine Routing-Regel), die genau diese Markierung als Bedingung abfragt, wird dadurch wirksam.",
warning: "Eine Markierung ohne etwas, das sie auswertet (z.B. keine passende Queue), hat keinerlei sichtbaren Effekt — das ist der häufigste Verwirrungspunkt bei Mangle.",
fields: [
RouterOSFieldSchema(key: "chain", label: "Chain", kind: .enumPick(options: [
"prerouting", "input", "forward", "output", "postrouting"
]), help: "Verarbeitungspunkt im Router-internen Paketfluss.", defaultValue: "forward", required: true),
RouterOSFieldSchema(key: "action", label: "Aktion", kind: .enumPick(options: [
"mark-connection", "mark-packet", "mark-routing", "change-mss", "change-ttl", "set-priority", "accept", "passthrough"
]), help: "mark-connection markiert die ganze Verbindung (wirkt auf alle ihre Pakete), mark-packet nur einzelne Pakete.",
defaultValue: "mark-connection", required: true),
RouterOSFieldSchema(key: "new-connection-mark", label: "Name der Verbindungsmarkierung", kind: .text,
help: "Frei wählbarer Name, den z.B. eine Queue später wiedererkennt."),
RouterOSFieldSchema(key: "new-packet-mark", label: "Name der Paketmarkierung", kind: .text,
help: "Frei wählbarer Name für die Paketmarkierung."),
RouterOSFieldSchema(key: "passthrough", label: "Weitere Mangle-Regeln noch prüfen", kind: .bool,
help: "Ja (Standard) lässt nachfolgende Mangle-Regeln diese Verbindung noch zusätzlich prüfen.", defaultValue: "yes"),
RouterOSFieldSchema(key: "src-address", label: "Quell-Adresse", kind: .text, help: "Leer = beliebig."),
RouterOSFieldSchema(key: "dst-address", label: "Ziel-Adresse", kind: .text, help: "Leer = beliebig."),
RouterOSFieldSchema(key: "in-interface", label: "Eingangs-Interface", kind: .interfacePick, help: ""),
RouterOSFieldSchema(key: "out-interface", label: "Ausgangs-Interface", kind: .interfacePick, help: ""),
RouterOSFieldSchema(key: "protocol", label: "Protokoll", kind: .enumPick(options: ["tcp", "udp", "icmp"]), help: ""),
RouterOSFieldSchema(key: "dst-port", label: "Ziel-Port(s)", kind: .text, help: "Nur bei tcp/udp."),
RouterOSFieldSchema(key: "comment", label: "Kommentar", kind: .text, help: "Nur zur Wiedererkennung."),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool, help: "", defaultValue: "no")
],
listColumns: ["chain", "action", "comment"]
),
RouterOSMenuSchema(
menuPath: "/ip firewall raw",
restPath: "ip/firewall/raw",
category: .firewallRaw,
displayName: "Raw-Regeln",
summary: "Läuft vor jeder Connection-Tracking-Verarbeitung — meist zur Entlastung oder für DDoS-Grobfilter.",
explanation: "Regeln hier greifen, bevor RouterOS eine Verbindung überhaupt \"kennt\" (Connection-Tracking). Damit lässt sich z.B. bekannt uninteressanter Verkehr sehr günstig verwerfen, oder gezielt vom Tracking ausnehmen (notrack).",
warning: "notrack nimmt den betroffenen Verkehr aus dem Connection-Tracking heraus — danach greifen dafür keine Filter-/NAT-Regeln mehr, die sich auf connection-state oder NAT verlassen. Nur gezielt einsetzen.",
fields: [
RouterOSFieldSchema(key: "chain", label: "Chain", kind: .enumPick(options: ["prerouting", "output"]),
help: "", defaultValue: "prerouting", required: true),
RouterOSFieldSchema(key: "action", label: "Aktion", kind: .enumPick(options: ["accept", "drop", "notrack"]),
help: "notrack = von der Connection-Tracking-Verfolgung ausnehmen (siehe Warnhinweis oben).",
defaultValue: "accept", required: true),
RouterOSFieldSchema(key: "src-address", label: "Quell-Adresse", kind: .text, help: ""),
RouterOSFieldSchema(key: "dst-address", label: "Ziel-Adresse", kind: .text, help: ""),
RouterOSFieldSchema(key: "in-interface", label: "Eingangs-Interface", kind: .interfacePick, help: ""),
RouterOSFieldSchema(key: "protocol", label: "Protokoll", kind: .enumPick(options: ["tcp", "udp", "icmp"]), help: ""),
RouterOSFieldSchema(key: "dst-port", label: "Ziel-Port(s)", kind: .text, help: ""),
RouterOSFieldSchema(key: "comment", label: "Kommentar", kind: .text, help: ""),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool, help: "", defaultValue: "no")
],
listColumns: ["chain", "action", "comment"]
),
RouterOSMenuSchema(
menuPath: "/ip firewall address-list",
restPath: "ip/firewall/address-list",
category: .firewallAddressLists,
displayName: "Adress-Listen",
summary: "Benannte Gruppen von IP-Adressen/Netzen, die Filter/NAT/Mangle-Regeln als Bedingung referenzieren können.",
explanation: "Statt in jeder Regel einzelne IPs aufzuzählen, legst du hier eine benannte Liste an (z.B. \"gesperrt\" oder \"vertraut\") und verweist in Filter-/NAT-/Mangle-Regeln per \"src-address-list\"/\"dst-address-list\" darauf. Einträge können dauerhaft oder mit Ablaufzeit (timeout) sein.",
fields: [
RouterOSFieldSchema(key: "list", label: "Listen-Name", kind: .text,
help: "Frei wählbarer Name, wird in anderen Regeln referenziert.", required: true),
RouterOSFieldSchema(key: "address", label: "Adresse", kind: .text,
help: "Einzel-IP oder Netz, z.B. 192.168.88.5 oder 10.0.0.0/24.", required: true),
RouterOSFieldSchema(key: "timeout", label: "Ablaufzeit", kind: .duration,
help: "Optional. Nach dieser Zeit wird der Eintrag automatisch entfernt. Alles auf 0 lassen = bleibt dauerhaft, bis manuell entfernt."),
RouterOSFieldSchema(key: "comment", label: "Kommentar", kind: .text, help: "")
],
listColumns: ["list", "address"]
)
]
// MARK: - Interfaces (generic, browsing + free-form fields)
private static let interfaceFamily: [RouterOSMenuSchema] = [
RouterOSMenuSchema(
menuPath: "/interface bridge", restPath: "interface/bridge", category: .interfaces,
displayName: "Bridge",
summary: "Fasst mehrere physische Ports zu einem gemeinsamen Layer-2-Netzwerk zusammen.",
explanation: "Geräte an gebrückten Ports verhalten sich, als hingen sie am selben Netzwerk-Kabel. Eine IP-Adresse wird meist auf die Bridge selbst gelegt, nicht auf die einzelnen Ports.",
fields: [
RouterOSFieldSchema(key: "name", label: "Name", kind: .text, help: "Frei wählbar, z.B. bridge-lan.", required: true),
RouterOSFieldSchema(key: "vlan-filtering", label: "VLAN-Filterung (802.1Q)", kind: .bool,
help: "Aktiviert echte VLAN-Trennung über diese Bridge — nötig, wenn mehrere VLANs über dieselben Bridge-Ports laufen sollen.", defaultValue: "no"),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool, help: "", defaultValue: "no")
],
listColumns: ["name"]
),
RouterOSMenuSchema(
menuPath: "/interface bridge port", restPath: "interface/bridge/port", category: .interfaces,
displayName: "Bridge-Ports",
summary: "Ordnet einen physischen Port einer Bridge zu.",
explanation: "Erst danach ist der Port Teil des Bridge-Netzwerks.",
fields: [
RouterOSFieldSchema(key: "bridge", label: "Bridge", kind: .text, help: "Name der Bridge, z.B. bridge-lan.", required: true),
RouterOSFieldSchema(key: "interface", label: "Physischer Port", kind: .interfacePick,
help: "Der Port, der der Bridge hinzugefügt wird.", required: true),
RouterOSFieldSchema(key: "pvid", label: "Port-VLAN-ID (PVID)", kind: .int,
help: "Nur mit aktivierter VLAN-Filterung relevant: VLAN, dem untagged ankommender Verkehr an diesem Port zugeordnet wird.")
],
listColumns: ["bridge", "interface"]
),
RouterOSMenuSchema(
menuPath: "/interface vlan", restPath: "interface/vlan", category: .interfaces,
displayName: "VLAN-Interfaces",
summary: "Virtuelle, getaggte Sub-Interfaces auf einem physischen Port oder einer Bridge.",
explanation: "Ein eigenes logisches Netzwerk auf demselben Kabel, unterschieden durch eine VLAN-Kennung im Ethernet-Rahmen.",
fields: [
RouterOSFieldSchema(key: "name", label: "Name", kind: .text, help: "Frei wählbar, z.B. vlan20-gaeste.", required: true),
RouterOSFieldSchema(key: "vlan-id", label: "VLAN-ID", kind: .int, help: "Eindeutige Kennung, 24094.", required: true),
RouterOSFieldSchema(key: "interface", label: "Basis-Interface", kind: .interfacePick,
help: "Physischer Port oder Bridge, auf dem dieses VLAN aufsetzt.", required: true),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool, help: "", defaultValue: "no")
],
listColumns: ["name", "vlan-id", "interface"]
),
RouterOSMenuSchema(
menuPath: "/interface wireguard", restPath: "interface/wireguard", category: .interfaces,
displayName: "WireGuard-Interfaces",
summary: "Moderner, schlanker VPN-Tunnel-Typ.",
explanation: "Ein WireGuard-Interface allein stellt noch keine Verbindung her — dazu gehören Gegenstellen (siehe \"WireGuard-Peers\") mit öffentlichem Schlüssel und erlaubten Adressen.",
warning: "Erst hier das Interface anlegen (mit Listen-Port), danach unter WireGuard-Peers die Gegenstellen eintragen. Der private Schlüssel wird beim Anlegen automatisch erzeugt, falls nicht angegeben.",
fields: [
RouterOSFieldSchema(key: "name", label: "Name", kind: .text, help: "Frei wählbar, z.B. wg-heim.", required: true),
RouterOSFieldSchema(key: "listen-port", label: "Listen-Port (UDP)", kind: .int,
help: "Port, auf dem dieser Tunnel auf eingehende Verbindungen lauscht, z.B. 51820.", defaultValue: "51820"),
RouterOSFieldSchema(key: "private-key", label: "Privater Schlüssel", kind: .text,
help: "Geheim halten. Leer lassen, damit RouterOS automatisch einen erzeugt."),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool, help: "", defaultValue: "no")
],
listColumns: ["name", "listen-port"]
),
RouterOSMenuSchema(
menuPath: "/interface wireguard peers", restPath: "interface/wireguard/peers", category: .interfaces,
displayName: "WireGuard-Peers",
summary: "Gegenstellen (Clients/andere Router) eines WireGuard-Tunnels.",
explanation: "Jede Gegenstelle braucht ihren eigenen öffentlichen Schlüssel und eine Angabe, welche Adressen über sie geroutet werden.",
fields: [
RouterOSFieldSchema(key: "interface", label: "WireGuard-Interface",
kind: .menuItemPick(menuPath: "/interface wireguard", restPath: "interface/wireguard", valueField: "name"),
help: "Name des zuvor angelegten WireGuard-Interfaces.", required: true),
RouterOSFieldSchema(key: "public-key", label: "Öffentlicher Schlüssel der Gegenstelle", kind: .text, help: "", required: true),
RouterOSFieldSchema(key: "allowed-address", label: "Erlaubte Adressen", kind: .text,
help: "Welche IP-Adressen/Netze über diesen Peer laufen dürfen, z.B. 10.10.10.2/32."),
RouterOSFieldSchema(key: "endpoint-address", label: "Feste Adresse der Gegenstelle", kind: .text,
help: "Nur nötig, wenn diese Gegenstelle selbst erreichbar sein muss (Site-to-Site). Bei Roadwarrior-Clients, die sich selbst melden, leer lassen."),
RouterOSFieldSchema(key: "endpoint-port", label: "Port der Gegenstelle", kind: .text, help: ""),
RouterOSFieldSchema(key: "persistent-keepalive", label: "Keepalive (Sekunden)", kind: .text,
help: "Z.B. 25 — hält die Verbindung durch NAT/Firewalls am Leben. Wichtig bei Clients hinter NAT.")
],
listColumns: ["interface", "allowed-address"]
),
RouterOSMenuSchema(
menuPath: "/interface pppoe-client", restPath: "interface/pppoe-client", category: .interfaces,
displayName: "PPPoE-Client",
summary: "DSL-Einwahl-Client, meist auf dem WAN-Port.",
explanation: "Ersetzt eine feste/DHCP-WAN-Adresse durch eine PPPoE-Einwahl beim Provider.",
fields: [
RouterOSFieldSchema(key: "name", label: "Name", kind: .text, help: "Frei wählbar, z.B. pppoe-wan.", required: true),
RouterOSFieldSchema(key: "interface", label: "Physischer Port", kind: .interfacePick,
help: "Der Port, über den die Einwahl läuft, meist der WAN-Port.", required: true),
RouterOSFieldSchema(key: "user", label: "Benutzername", kind: .text, help: "Zugangsdaten des Providers.", required: true),
RouterOSFieldSchema(key: "password", label: "Passwort", kind: .text, help: "", required: true),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool, help: "", defaultValue: "no")
],
listColumns: ["name", "interface"]
),
RouterOSMenuSchema(
menuPath: "/interface bonding", restPath: "interface/bonding", category: .interfaces,
displayName: "Bonding",
summary: "Bündelt mehrere physische Ports zu einer logischen, ausfalltoleranten/schnelleren Verbindung.",
explanation: "",
fields: [
RouterOSFieldSchema(key: "name", label: "Name", kind: .text, help: "Frei wählbar.", required: true),
RouterOSFieldSchema(key: "slaves", label: "Gebündelte Ports", kind: .text,
help: "Kommagetrennte Liste physischer Ports, z.B. ether2,ether3."),
RouterOSFieldSchema(key: "mode", label: "Modus", kind: .enumPick(options: [
"802.3ad", "active-backup", "balance-rr", "balance-xor", "broadcast"
]), help: "802.3ad (LACP) braucht einen kompatiblen, entsprechend konfigurierten Switch.")
],
listColumns: ["name", "mode"]
)
]
// MARK: - IP addressing & services
private static let ipFamily: [RouterOSMenuSchema] = [
RouterOSMenuSchema(
menuPath: "/ip address", restPath: "ip/address", category: .ipAddressing,
displayName: "IP-Adressen",
summary: "Weist Interfaces IP-Adressen zu.",
explanation: "Jede IP-Adresse hängt an genau einem Interface (physischer Port, Bridge oder VLAN).",
fields: [
RouterOSFieldSchema(key: "address", label: "Adresse", kind: .text,
help: "Mit Präfix, z.B. 192.168.88.1/24.", required: true),
RouterOSFieldSchema(key: "interface", label: "Interface", kind: .interfacePick, help: "", required: true),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool, help: "", defaultValue: "no")
],
listColumns: ["address", "interface"]
),
RouterOSMenuSchema(
menuPath: "/ip pool", restPath: "ip/pool", category: .ipAddressing,
displayName: "Adress-Pools",
summary: "Adressbereiche, aus denen DHCP-Server oder PPP-Profile Adressen vergeben.",
explanation: "",
fields: [
RouterOSFieldSchema(key: "name", label: "Name", kind: .text, help: "Frei wählbar.", required: true),
RouterOSFieldSchema(key: "ranges", label: "Bereich(e)", kind: .text,
help: "Z.B. 192.168.88.10-192.168.88.254. Mehrere Bereiche kommagetrennt.", required: true)
],
listColumns: ["name", "ranges"]
),
RouterOSMenuSchema(
menuPath: "/ip dhcp-server", restPath: "ip/dhcp-server", category: .ipAddressing,
displayName: "DHCP-Server",
summary: "Vergibt automatisch IP-Adressen an Geräte in einem Netzwerk.",
explanation: "Für die üblichen Fälle deckt das bereits der Einrichten-Assistent (LAN-/VLAN-Schritt) ab — hier direkter Zugriff für Sonderfälle.",
fields: [
RouterOSFieldSchema(key: "name", label: "Name", kind: .text, help: "Frei wählbar.", required: true),
RouterOSFieldSchema(key: "interface", label: "Interface", kind: .interfacePick,
help: "Netzwerk, in dem dieser Server Adressen vergibt.", required: true),
RouterOSFieldSchema(key: "address-pool", label: "Adress-Pool",
kind: .menuItemPick(menuPath: "/ip pool", restPath: "ip/pool", valueField: "name"),
help: "Name eines zuvor angelegten Adress-Pools.", required: true),
RouterOSFieldSchema(key: "lease-time", label: "Lease-Zeit", kind: .duration,
help: "Wie lange ein Gerät seine Adresse behält, bevor sie erneuert werden muss."),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool, help: "", defaultValue: "no")
],
listColumns: ["name", "interface"]
),
RouterOSMenuSchema(
menuPath: "/ip dhcp-server network", restPath: "ip/dhcp-server/network", category: .ipAddressing,
displayName: "DHCP-Netzwerke",
summary: "Gateway/DNS/Optionen, die ein DHCP-Server an seine Klienten verteilt.",
explanation: "Getrennt vom DHCP-Server selbst, weil dieselben Netzwerk-Optionen für mehrere DHCP-Server gelten können.",
fields: [
RouterOSFieldSchema(key: "address", label: "Netz", kind: .text,
help: "Für dieses Netz gelten die folgenden Optionen, z.B. 192.168.88.0/24.", required: true),
RouterOSFieldSchema(key: "gateway", label: "Gateway", kind: .text,
help: "In der Regel die Router-Adresse in diesem Netz.", required: true),
RouterOSFieldSchema(key: "dns-server", label: "DNS-Server", kind: .text, help: "Meist der Router selbst.")
],
listColumns: ["address", "gateway"]
),
RouterOSMenuSchema(
menuPath: "/ip dhcp-client", restPath: "ip/dhcp-client", category: .ipAddressing,
displayName: "DHCP-Client (WAN)",
summary: "Bezieht automatisch eine IP-Adresse vom Internetanbieter.",
explanation: "",
fields: [
RouterOSFieldSchema(key: "interface", label: "Interface", kind: .interfacePick,
help: "Meist der WAN-Port.", required: true),
RouterOSFieldSchema(key: "add-default-route", label: "Standardroute übernehmen", kind: .bool,
help: "Übernimmt die vom Provider mitgeteilte Standardroute ins Internet.", defaultValue: "yes"),
RouterOSFieldSchema(key: "use-peer-dns", label: "DNS-Server übernehmen", kind: .bool,
help: "Übernimmt die vom Provider mitgeteilten DNS-Server.", defaultValue: "yes"),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool, help: "", defaultValue: "no")
],
listColumns: ["interface"]
),
RouterOSMenuSchema(
menuPath: "/ip dns", restPath: "ip/dns", category: .ipAddressing,
displayName: "DNS-Einstellungen",
summary: "Namensauflösung des Routers selbst (und optional als DNS-Server fürs LAN).",
explanation: "",
fields: [
RouterOSFieldSchema(key: "servers", label: "DNS-Server", kind: .text,
help: "Ein oder mehrere Server, kommagetrennt, z.B. 1.1.1.1,8.8.8.8."),
RouterOSFieldSchema(key: "allow-remote-requests", label: "Als DNS-Server fürs LAN erlauben", kind: .bool,
help: "Lässt Geräte im LAN den Router selbst als DNS-Server nutzen.", defaultValue: "no")
],
listColumns: ["servers"],
isSingleton: true
),
RouterOSMenuSchema(
menuPath: "/ip service", restPath: "ip/service", category: .ipAddressing,
displayName: "Verwaltungsdienste",
summary: "Schaltet Zugriffswege auf den Router (Winbox, API, SSH, WWW/REST, Telnet, FTP) an/aus und ändert deren Port.",
explanation: "Jeder aktive Dienst ist ein potenzieller Angriffspunkt aus dem jeweils erreichbaren Netz — nicht benötigte Dienste deaktivieren.",
warning: "Den gerade genutzten Zugriffsweg hier abzuschalten oder umzuportieren kann die eigene Verbindung sofort kappen — Vorsicht bei www-ssl (REST-API dieser App) und ssh.",
fields: [
RouterOSFieldSchema(key: "name", label: "Dienst", kind: .enumPick(options: [
"www", "www-ssl", "ssh", "api", "api-ssl", "winbox", "ftp", "telnet"
]), help: "Welcher Verwaltungsdienst geändert wird.", required: true),
RouterOSFieldSchema(key: "port", label: "Port", kind: .int, help: "Auf welchem Port der Dienst lauscht."),
RouterOSFieldSchema(key: "available-from", label: "Erlaubt von", kind: .text,
help: "Optional: nur von dieser Adresse/diesem Netz aus erreichbar, z.B. 192.168.88.0/24."),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool, help: "", defaultValue: "no")
],
listColumns: ["name", "port"]
),
generic("/ip hotspot", "ip/hotspot", .ipAddressing, "Hotspot",
"Login-Portal für Gäste-WLAN/-LAN mit Umleitung auf eine Anmeldeseite.",
"Besteht aus mehreren zusammenhängenden Teilen (Server, Server-Profil, Benutzer-Profil, Benutzer) — hier generischer Zugriff auf \"/ip hotspot\" selbst, verwandte Teile über \"Eigener Menüpfad\" (z.B. \"/ip hotspot user\")."),
RouterOSMenuSchema(
menuPath: "/ip route", restPath: "ip/route", category: .routing,
displayName: "Statische Routen",
summary: "Feste, manuell eingetragene Wegewahl zu Netzen, die nicht direkt angeschlossen sind.",
explanation: "Für alles außer \"Standard-Internet über eine WAN-Schnittstelle\" (das übernimmt bereits die DHCP-Client-/PPPoE-Route automatisch).",
fields: [
RouterOSFieldSchema(key: "dst-address", label: "Zielnetz", kind: .text,
help: "Z.B. 10.0.0.0/24, oder 0.0.0.0/0 für eine Standardroute.", required: true),
RouterOSFieldSchema(key: "gateway", label: "Gateway", kind: .text,
help: "Nächster Hop — IP-Adresse oder Name eines Interfaces.", required: true),
RouterOSFieldSchema(key: "distance", label: "Distanz", kind: .int,
help: "Priorität bei mehreren passenden Routen zum selben Ziel — kleinere Zahl wird bevorzugt."),
RouterOSFieldSchema(key: "comment", label: "Kommentar", kind: .text, help: ""),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool, help: "", defaultValue: "no")
],
listColumns: ["dst-address", "gateway"]
)
]
// MARK: - Routing (dynamic protocols)
private static let routingFamily: [RouterOSMenuSchema] = [
generic("/routing ospf instance", "routing/ospf/instance", .routing, "OSPF-Instanzen",
"Dynamisches Innennetz-Routing-Protokoll — tauscht Routen automatisch mit anderen OSPF-Routern aus.",
"Nur relevant, wenn mehrere Router im selben Netz eigenständig Routen lernen sollen.",
warning: "Fehlkonfiguriertes OSPF kann Routen zu bestehenden Netzen überschreiben. Nur mit Netzwerkplan einsetzen."),
generic("/routing bgp connection", "routing/bgp/connection", .routing, "BGP-Verbindungen",
"Routing-Protokoll für Verbindungen zwischen unabhängigen Netzen/Providern.",
"Für Privat-/Kleinnetz i.d.R. nicht nötig — relevant bei eigenem Provider-unabhängigem Adressraum (Multihoming).")
]
// MARK: - VPN
private static let vpnFamily: [RouterOSMenuSchema] = [
RouterOSMenuSchema(
menuPath: "/ppp secret", restPath: "ppp/secret", category: .vpn,
displayName: "PPP-Benutzer",
summary: "Zugangsdaten für PPPoE-/L2TP-/PPTP-/OpenVPN-Einwahl in den Router.",
explanation: "Jeder Benutzer kann optional einem Profil zugeordnet werden, das IP-Pool/DNS/Verschlüsselung vorgibt.",
fields: [
RouterOSFieldSchema(key: "name", label: "Benutzername", kind: .text, help: "", required: true),
RouterOSFieldSchema(key: "password", label: "Passwort", kind: .text, help: "", required: true),
RouterOSFieldSchema(key: "service", label: "Dienst", kind: .enumPick(options: [
"any", "pppoe", "l2tp", "pptp", "ovpn", "sstp"
]), help: "Für welche Einwahl-Art dieser Benutzer gilt.", defaultValue: "any"),
RouterOSFieldSchema(key: "profile", label: "Profil",
kind: .menuItemPick(menuPath: "/ppp profile", restPath: "ppp/profile", valueField: "name"),
help: "Name eines zuvor angelegten PPP-Profils."),
RouterOSFieldSchema(key: "local-address", label: "Lokale Adresse", kind: .text,
help: "IP-Adresse des Routers innerhalb dieser Verbindung."),
RouterOSFieldSchema(key: "remote-address", label: "Adresse für den Client", kind: .text,
help: "Feste IP für diesen Benutzer, oder Name eines Adress-Pools."),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool, help: "", defaultValue: "no")
],
listColumns: ["name", "service"]
),
RouterOSMenuSchema(
menuPath: "/ppp profile", restPath: "ppp/profile", category: .vpn,
displayName: "PPP-Profile",
summary: "Vorlagen (IP-Pool, DNS, Verschlüsselung) für PPP-Benutzer.",
explanation: "",
fields: [
RouterOSFieldSchema(key: "name", label: "Name", kind: .text, help: "", required: true),
RouterOSFieldSchema(key: "local-address", label: "Lokale Adresse", kind: .text, help: "IP-Adresse des Routers."),
RouterOSFieldSchema(key: "remote-address", label: "Adress-Pool für Clients", kind: .text,
help: "Name eines zuvor angelegten Adress-Pools."),
RouterOSFieldSchema(key: "dns-server", label: "DNS-Server für Clients", kind: .text, help: "")
],
listColumns: ["name"]
),
generic("/interface l2tp-server server", "interface/l2tp-server/server", .vpn, "L2TP-VPN-Server",
"Nimmt eingehende L2TP-VPN-Einwahlen entgegen.",
"Ein einzelnes Server-weites An/Aus mit gemeinsamen Verschlüsselungs-Einstellungen — Benutzer selbst kommen von \"PPP-Benutzer\"."),
generic("/interface ovpn-server server", "interface/ovpn-server/server", .vpn, "OpenVPN-Server",
"Nimmt eingehende OpenVPN-Einwahlen entgegen.",
"Braucht zusätzlich ein Zertifikat (\"/certificate\") — Benutzer selbst kommen von \"PPP-Benutzer\".")
]
// MARK: - Wireless
private static let wirelessFamily: [RouterOSMenuSchema] = [
RouterOSMenuSchema(
menuPath: "/interface wireless", restPath: "interface/wireless", category: .wireless,
displayName: "WLAN (Legacy-Treiber)",
summary: "WLAN-Interfaces auf älteren/Standard-Wireless-Chips.",
explanation: "Diese Interfaces existieren bereits ab Werk (ein WLAN-Chip = ein Interface) — hier werden sie nur konfiguriert, nicht neu angelegt. Ein Sicherheitsprofil (siehe \"WLAN-Sicherheitsprofile\") muss vorher angelegt sein.",
fields: [
RouterOSFieldSchema(key: "ssid", label: "Netzwerkname (SSID)", kind: .text, help: "", required: true),
RouterOSFieldSchema(key: "security-profile", label: "Sicherheitsprofil", kind: .text,
help: "Name eines zuvor unter \"WLAN-Sicherheitsprofile\" angelegten Profils."),
RouterOSFieldSchema(key: "mode", label: "Modus", kind: .enumPick(options: ["ap-bridge", "station", "bridge"]),
help: "ap-bridge = Access Point (Normalfall), station = als Client mit einem anderen AP verbinden.", defaultValue: "ap-bridge"),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool, help: "", defaultValue: "no")
],
listColumns: ["ssid", "mode"]
),
RouterOSMenuSchema(
menuPath: "/interface wireless security-profiles", restPath: "interface/wireless/security-profiles", category: .wireless,
displayName: "WLAN-Sicherheitsprofile",
summary: "Verschlüsselung/Passwort-Vorlagen für WLAN-Interfaces (Legacy-Treiber).",
explanation: "Ein Profil wird angelegt und dann bei einem WLAN-Interface als \"security-profile\" eingetragen.",
fields: [
RouterOSFieldSchema(key: "name", label: "Name", kind: .text, help: "", required: true),
RouterOSFieldSchema(key: "mode", label: "Modus", kind: .enumPick(options: ["none", "static-keys-required", "dynamic-keys"]),
help: "dynamic-keys ist der übliche WPA/WPA2-Modus.", defaultValue: "dynamic-keys"),
RouterOSFieldSchema(key: "authentication-types", label: "Authentifizierung", kind: .enumPick(options: [
"wpa-psk", "wpa2-psk", "wpa-psk,wpa2-psk", "wpa-eap", "wpa2-eap"
]), help: "wpa2-psk = WPA2 mit gemeinsamem Passwort (Heimnetz-Standard)."),
RouterOSFieldSchema(key: "wpa2-pre-shared-key", label: "WPA2-Passwort", kind: .text, help: "Mindestens 8 Zeichen.")
],
listColumns: ["name", "authentication-types"]
),
RouterOSMenuSchema(
menuPath: "/interface wifi", restPath: "interface/wifi", category: .wireless,
displayName: "WLAN (neuer wifiwave2/802.11ax-Treiber)",
summary: "WLAN-Interfaces auf neueren Wireless-Chips.",
explanation: "Anderes, verschachteltes Konfigurationsschema als der Legacy-Treiber (Punkt-Notation wie \"security.passphrase\") — nicht mit \"/interface wireless\" mischen. Nur auf Geräten mit entsprechend neuem WLAN-Chip vorhanden.",
warning: "Diese App hat den .set-Pfad für den neuen Treiber noch nie gegen echte Hardware getestet (siehe HANDOFF.md) — nach dem Anwenden unbedingt prüfen.",
fields: [
RouterOSFieldSchema(key: "ssid", label: "Netzwerkname (SSID)", kind: .text, help: "", required: true),
RouterOSFieldSchema(key: "security.authentication-types", label: "Authentifizierung", kind: .enumPick(options: [
"wpa2-psk", "wpa3-psk", "wpa2-psk,wpa3-psk"
]), help: "wpa2-psk,wpa3-psk deckt sowohl ältere als auch neuere Geräte ab."),
RouterOSFieldSchema(key: "security.passphrase", label: "Passwort", kind: .text, help: "Mindestens 8 Zeichen."),
RouterOSFieldSchema(key: "datapath.bridge", label: "Ziel-Bridge", kind: .text,
help: "Name der Bridge, der dieses WLAN-Netz zugeordnet wird (üblicherweise dieselbe wie das kabelgebundene LAN)."),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool, help: "", defaultValue: "no")
],
listColumns: ["ssid"]
),
generic("/caps-man manager", "caps-man/manager", .wireless, "CAPsMAN-Zentrale",
"Zentrale Verwaltung mehrerer WLAN-Access-Points von einem Router aus.",
"Nur relevant mit mehreren WLAN-Access-Points, die zentral verwaltet werden sollen — eigenes, umfangreiches Konfigurationsschema (Konfigurationen/Kanäle/Datapaths).")
]
// MARK: - Queues / QoS
private static let queueFamily: [RouterOSMenuSchema] = [
RouterOSMenuSchema(
menuPath: "/queue simple", restPath: "queue/simple", category: .queues,
displayName: "Einfache Bandbreiten-Begrenzung",
summary: "Begrenzt Up-/Download einer IP-Adresse oder eines Netzes.",
explanation: "Reicht für die meisten Heim-/Kleinnetz-Fälle ohne Mangle-Markierungen aus.",
fields: [
RouterOSFieldSchema(key: "name", label: "Name", kind: .text, help: "Frei wählbar.", required: true),
RouterOSFieldSchema(key: "target", label: "Ziel", kind: .text,
help: "IP-Adresse oder Netz, das begrenzt wird, z.B. 192.168.88.50/32.", required: true),
RouterOSFieldSchema(key: "max-limit", label: "Max. Bandbreite (Upload/Download)", kind: .text,
help: "Zwei Werte getrennt durch \"/\", z.B. 10M/50M (Upload/Download).", required: true),
RouterOSFieldSchema(key: "burst-limit", label: "Burst-Bandbreite", kind: .text,
help: "Optional: kurzzeitig erlaubte höhere Bandbreite, z.B. 15M/60M."),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool, help: "", defaultValue: "no")
],
listColumns: ["name", "target", "max-limit"]
),
RouterOSMenuSchema(
menuPath: "/queue tree", restPath: "queue/tree", category: .queues,
displayName: "Queue-Baum",
summary: "Erweiterte, hierarchische Bandbreiten-Steuerung basierend auf Mangle-Markierungen.",
explanation: "Statt einer festen Adresse wirkt ein Queue-Baum-Eintrag auf Verkehr mit einer bestimmten Mangle-Markierung.",
warning: "Setzt eine passende Mangle-Regel voraus, die genau diese Markierung setzt (siehe Firewall: Mangle) — ohne die greift ein Queue-Baum-Eintrag ins Leere.",
fields: [
RouterOSFieldSchema(key: "name", label: "Name", kind: .text, help: "Frei wählbar.", required: true),
RouterOSFieldSchema(key: "parent", label: "Übergeordnet", kind: .text,
help: "Ein Interface (z.B. der WAN-Port) oder der Name eines anderen Queue-Baum-Eintrags.", required: true),
RouterOSFieldSchema(key: "packet-mark", label: "Paket-/Verbindungsmarkierung", kind: .text,
help: "Name der Mangle-Markierung, auf die dieser Eintrag wirkt."),
RouterOSFieldSchema(key: "max-limit", label: "Max. Bandbreite", kind: .text, help: "Einzelwert, z.B. 20M."),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool, help: "", defaultValue: "no")
],
listColumns: ["name", "parent", "max-limit"]
)
]
// MARK: - System
private static let systemFamily: [RouterOSMenuSchema] = [
RouterOSMenuSchema(
menuPath: "/system identity", restPath: "system/identity", category: .system,
displayName: "Router-Name",
summary: "Der Name, unter dem sich der Router meldet (z.B. in Winbox/Terminal-Prompt).",
explanation: "",
fields: [RouterOSFieldSchema(key: "name", label: "Name", kind: .text, help: "", required: true)],
listColumns: ["name"],
isSingleton: true
),
RouterOSMenuSchema(
menuPath: "/system clock", restPath: "system/clock", category: .system,
displayName: "Uhrzeit/Zeitzone", summary: "", explanation: "",
fields: [
RouterOSFieldSchema(key: "time-zone-name", label: "Zeitzone", kind: .text,
help: "Z.B. Europe/Berlin.")
],
listColumns: ["time-zone-name"],
isSingleton: true
),
RouterOSMenuSchema(
menuPath: "/system ntp client", restPath: "system/ntp/client", category: .system,
displayName: "Zeitserver (NTP)",
summary: "Hält die Router-Uhr über einen Zeitserver synchron.",
explanation: "",
warning: "Falsche Systemzeit kann Zertifikatsprüfungen (HTTPS/REST) und Log-Zeitstempel durcheinanderbringen.",
fields: [
RouterOSFieldSchema(key: "enabled", label: "Aktiviert", kind: .bool, help: "", defaultValue: "yes"),
RouterOSFieldSchema(key: "servers", label: "Zeitserver", kind: .text,
help: "Ein oder mehrere Server, kommagetrennt, z.B. pool.ntp.org.")
],
listColumns: ["servers"],
isSingleton: true
),
generic("/system logging", "system/logging", .system, "Protokollierung",
"Was der Router mitschreibt und wohin (Speicher, Datei, Remote-Syslog, E-Mail).",
"Besteht aus \"rules\" (was protokolliert wird) und \"actions\" (wohin) — hier generischer Zugriff, verwandte Teile über \"Eigener Menüpfad\" (z.B. \"/system logging action\")."),
RouterOSMenuSchema(
menuPath: "/system scheduler", restPath: "system/scheduler", category: .system,
displayName: "Zeitplaner",
summary: "Führt ein hinterlegtes Skript zu festen Zeiten/Intervallen aus.",
explanation: "",
warning: "Ein fehlerhaftes geplantes Skript kann unbeaufsichtigt wiederholt Änderungen vornehmen — vor dem Einplanen einmal manuell testen.",
fields: [
RouterOSFieldSchema(key: "name", label: "Name", kind: .text, help: "", required: true),
RouterOSFieldSchema(key: "start-time", label: "Startzeit", kind: .text, help: "Z.B. 00:00:00 oder \"startup\"."),
RouterOSFieldSchema(key: "interval", label: "Intervall", kind: .text,
help: "Wie oft wiederholt wird, z.B. 1d oder 01:00:00. Leer = nur einmal zur Startzeit."),
RouterOSFieldSchema(key: "on-event", label: "Auszuführendes Skript",
kind: .menuItemPick(menuPath: "/system script", restPath: "system/script", valueField: "name"),
help: "Name eines zuvor unter \"Skripte\" angelegten Skripts."),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool, help: "", defaultValue: "no")
],
listColumns: ["name", "interval"]
),
RouterOSMenuSchema(
menuPath: "/system script", restPath: "system/script", category: .system,
displayName: "Skripte",
summary: "Gespeicherte RouterOS-Befehlsfolgen, die manuell oder per Scheduler ausgeführt werden.",
explanation: "",
warning: "Skripte laufen mit den Rechten des Routers selbst — kein Unterschied zu manuell eingegebenen Befehlen.",
fields: [
RouterOSFieldSchema(key: "name", label: "Name", kind: .text, help: "", required: true),
RouterOSFieldSchema(key: "source", label: "Skript-Inhalt", kind: .text,
help: "RouterOS-Befehle, z.B. \":log info \\\"Test\\\"\".")
],
listColumns: ["name"]
),
RouterOSMenuSchema(
menuPath: "/user", restPath: "user", category: .system,
displayName: "Benutzerkonten",
summary: "Zugangskonten für den Router (Winbox/SSH/REST/Terminal).",
explanation: "",
warning: "Das zum Verbinden genutzte eigene Konto hier nicht versehentlich löschen oder herabstufen.",
fields: [
RouterOSFieldSchema(key: "name", label: "Benutzername", kind: .text, help: "", required: true),
RouterOSFieldSchema(key: "password", label: "Passwort", kind: .text, help: ""),
RouterOSFieldSchema(key: "group", label: "Rechte-Gruppe", kind: .enumPick(options: ["full", "write", "read"]),
help: "full = Vollzugriff, write = ohne Benutzerverwaltung, read = nur lesen.", defaultValue: "full"),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool, help: "", defaultValue: "no")
],
listColumns: ["name", "group"]
)
]
// MARK: - Tools
private static let toolFamily: [RouterOSMenuSchema] = [
RouterOSMenuSchema(
menuPath: "/tool netwatch", restPath: "tool/netwatch", category: .tools,
displayName: "Netwatch",
summary: "Überwacht laufend, ob eine Adresse erreichbar ist, und kann bei Statuswechsel ein Skript auslösen.",
explanation: "",
fields: [
RouterOSFieldSchema(key: "host", label: "Zu überwachende Adresse", kind: .text, help: "", required: true),
RouterOSFieldSchema(key: "interval", label: "Prüf-Intervall", kind: .text, help: "Z.B. 00:00:10.", defaultValue: "00:00:10"),
RouterOSFieldSchema(key: "up-script", label: "Skript bei Erreichbarkeit", kind: .text,
help: "Name eines Skripts unter \"Skripte\"."),
RouterOSFieldSchema(key: "down-script", label: "Skript bei Nichterreichbarkeit", kind: .text, help: ""),
RouterOSFieldSchema(key: "disabled", label: "Deaktiviert", kind: .bool, help: "", defaultValue: "no")
],
listColumns: ["host"]
),
RouterOSMenuSchema(
menuPath: "/tool e-mail", restPath: "tool/e-mail", category: .tools,
displayName: "E-Mail-Versand",
summary: "Postausgangs-Server, den Scheduler/Netwatch/Skripte für Benachrichtigungen nutzen.",
explanation: "Ein einzelner, geräteweiter Satz Einstellungen — kein Menü mit mehreren Einträgen.",
fields: [
RouterOSFieldSchema(key: "address", label: "Mailserver-Adresse", kind: .text, help: "", required: true),
RouterOSFieldSchema(key: "port", label: "Port", kind: .int, help: "Meist 587 (STARTTLS) oder 465 (SSL)."),
RouterOSFieldSchema(key: "from", label: "Absenderadresse", kind: .text, help: ""),
RouterOSFieldSchema(key: "user", label: "Benutzername", kind: .text, help: ""),
RouterOSFieldSchema(key: "password", label: "Passwort", kind: .text, help: "")
],
listColumns: ["address"],
isSingleton: true
)
]
private static func generic(
_ menuPath: String,
_ restPath: String,
_ category: RouterOSMenuCategory,
_ displayName: String,
_ summary: String,
_ explanation: String = "",
warning: String? = nil
) -> RouterOSMenuSchema {
RouterOSMenuSchema(
menuPath: menuPath,
restPath: restPath,
category: category,
displayName: displayName,
summary: summary,
explanation: explanation.isEmpty ? summary : explanation,
warning: warning,
fields: []
)
}
}