M31: Handbuch in der App (Textanker, DE+EN)

"?"-Hilfe-Buttons in allen 6 Haupt-Tabs, allen 6 Wizard-Schritten und
allen 45 Experte-Menüs öffnen ein Handbuch-Fenster (WKWebView) und
springen per Textanker direkt zur passenden Manual-Stelle.

build-manual.py generalisiert auf beliebig viele Sprachen (LANGUAGES-
Dict) statt hart DE/EN. Manual.en.md: komplette Handübersetzung aller
Fließtext-Kapitel. Kapitel 5 (Experte-Referenz) wird pro Sprache
automatisch übersetzt, indem L10n.swifts eigenes App-Übersetzungs-
Dictionary wiederverwendet wird (714 Einträge geparst) statt einer
zweiten, separat gepflegten Übersetzung.

Live bestätigt (DE und EN).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Kay
2026-09-17 17:15:29 +02:00
co-authored by Claude Sonnet 5
parent e582728040
commit 3a26f50800
25 changed files with 7247 additions and 128 deletions
+1
View File
@@ -17,3 +17,4 @@ xcuserdata/
# testing the app's "choose backup folder" feature with this directory selected. # testing the app's "choose backup folder" feature with this directory selected.
/Backups/ /Backups/
*.rsc *.rsc
__pycache__/
+1172
View File
File diff suppressed because it is too large Load Diff
+57 -64
View File
@@ -32,7 +32,6 @@ entsprechen, was in der App tatsächlich angezeigt wird.
5. [Experte](#5-experte) 5. [Experte](#5-experte)
6. [Sicherungen](#6-sicherungen) 6. [Sicherungen](#6-sicherungen)
7. [Einstellungen](#7-einstellungen) 7. [Einstellungen](#7-einstellungen)
8. [English summary](#8-english-summary)
--- ---
@@ -62,6 +61,7 @@ bleiben unübersetzt.
--- ---
<a id="tab-connect"></a>
## 1. Verbinden ## 1. Verbinden
### Verbindungsaufbau ### Verbindungsaufbau
@@ -145,6 +145,7 @@ Modus-Schalter zu Beginn wählt zwischen:
![Wizard-Schrittfolge](Manual-assets/wizard_flow.png) ![Wizard-Schrittfolge](Manual-assets/wizard_flow.png)
<a id="step-wan"></a>
### WAN (Internetanschluss) ### WAN (Internetanschluss)
| Feld | Hilfetext | | Feld | Hilfetext |
@@ -157,6 +158,7 @@ Modus-Schalter zu Beginn wählt zwischen:
| PPPoE-Benutzername | „Zugangsdaten von deinem Internetanbieter für die Einwahl (z.B. bei DSL-Anschlüssen).“ | | PPPoE-Benutzername | „Zugangsdaten von deinem Internetanbieter für die Einwahl (z.B. bei DSL-Anschlüssen).“ |
| PPPoE-Passwort | „Das zum Benutzernamen gehörende Passwort von deinem Internetanbieter.“ | | PPPoE-Passwort | „Das zum Benutzernamen gehörende Passwort von deinem Internetanbieter.“ |
<a id="step-lan"></a>
### LAN (ein oder mehrere Netzwerke) ### LAN (ein oder mehrere Netzwerke)
| Feld | Hilfetext | | Feld | Hilfetext |
@@ -184,6 +186,7 @@ entfernt.“ Tatsächlich ausgeführt wird das erst mit „Jetzt anwenden“ am
Ende des Assistenten — bis dahin lässt es sich rückgängig machen, indem Ende des Assistenten — bis dahin lässt es sich rückgängig machen, indem
oben ein anderer Port gewählt wird. oben ein anderer Port gewählt wird.
<a id="step-vlan"></a>
### VLAN (optional) ### VLAN (optional)
„Ein VLAN ist ein zusätzliches Netzwerk mit eigenem Adressbereich — z.B. „Ein VLAN ist ein zusätzliches Netzwerk mit eigenem Adressbereich — z.B.
@@ -201,6 +204,7 @@ einfach.“
| Netzbereich | „Der komplette Adressbereich dieses zusätzlichen Netzwerks.“ | | Netzbereich | „Der komplette Adressbereich dieses zusätzlichen Netzwerks.“ |
| DHCP von/bis | „Ab/Bis zu welcher Adresse Geräte in diesem Netzwerk automatisch eine Adresse bekommen.“ | | DHCP von/bis | „Ab/Bis zu welcher Adresse Geräte in diesem Netzwerk automatisch eine Adresse bekommen.“ |
<a id="step-wifi"></a>
### WLAN (nur falls erkannt) ### WLAN (nur falls erkannt)
„An diesem Gerät wurde kein WLAN erkannt. Dieser Schritt wird „An diesem Gerät wurde kein WLAN erkannt. Dieser Schritt wird
@@ -212,6 +216,7 @@ vergibt Netzwerkname und Passwort.“
| Netzwerkname (SSID) | „Der Name, den Geräte in ihrer WLAN-Liste sehen und mit dem sie sich verbinden.“ | | Netzwerkname (SSID) | „Der Name, den Geräte in ihrer WLAN-Liste sehen und mit dem sie sich verbinden.“ |
| Passwort | „Das WLAN-Passwort (WPA2). Muss mindestens 8 Zeichen lang sein.“ | | Passwort | „Das WLAN-Passwort (WPA2). Muss mindestens 8 Zeichen lang sein.“ |
<a id="step-firewall"></a>
### Firewall-Grundschutz ### Firewall-Grundschutz
„Schützt deinen Router und deine Geräte vor unaufgeforderten Zugriffen „Schützt deinen Router und deine Geräte vor unaufgeforderten Zugriffen
@@ -228,6 +233,7 @@ vorangestellt, bestehende bleiben erhalten — prüfe nach dem Anwenden
trotzdem die Reihenfolge, z.B. über Winbox oder /ip firewall filter trotzdem die Reihenfolge, z.B. über Winbox oder /ip firewall filter
print.“ print.“
<a id="step-review"></a>
### Review / Apply ### Review / Apply
„Vor dem Anwenden wird automatisch eine Sicherung der aktuellen „Vor dem Anwenden wird automatisch eine Sicherung der aktuellen
@@ -247,6 +253,7 @@ Assistenten auf den ersten Schritt zurück.
--- ---
<a id="tab-overview"></a>
## 3. Übersicht ## 3. Übersicht
Grafisches Diagramm des kompletten aktuellen Router-Zustands (IST-Zustand) Grafisches Diagramm des kompletten aktuellen Router-Zustands (IST-Zustand)
@@ -322,6 +329,7 @@ selbst verwaltet.
--- ---
<a id="tab-devices"></a>
## 4. LAN-Scanner ## 4. LAN-Scanner
Zeigt alle Geräte im Netzwerk (aus DHCP-Leases und ARP-Tabelle), Zeigt alle Geräte im Netzwerk (aus DHCP-Leases und ARP-Tabelle),
@@ -363,6 +371,7 @@ darunter der scrollbare Inhalt — derselbe Aufbau wie beim Fokus-Popup der
--- ---
<a id="tab-expert"></a>
## 5. Experte ## 5. Experte
Direkter, kuratierter Zugriff auf die meisten RouterOS-Bereiche. Für Direkter, kuratierter Zugriff auf die meisten RouterOS-Bereiche. Für
@@ -398,6 +407,7 @@ aktuell am Router vorhandenen Interfaces.
### System ### System
<a id="schema-system-identity"></a>
#### Router-Name *(Einstellungsmenü — genau ein Eintrag, kein Anlegen/Löschen)* #### Router-Name *(Einstellungsmenü — genau ein Eintrag, kein Anlegen/Löschen)*
RouterOS-Menü: `/system identity` · REST-Pfad: `system/identity` RouterOS-Menü: `/system identity` · REST-Pfad: `system/identity`
@@ -407,6 +417,7 @@ Der Name, unter dem sich der Router meldet (z.B. in Winbox/Terminal-Prompt).
|---|---|---|---|---|---| |---|---|---|---|---|---|
| Name | `name` | Text | Ja | — | Frei wählbar, z.B. MeinRouter. | | Name | `name` | Text | Ja | — | Frei wählbar, z.B. MeinRouter. |
<a id="schema-system-clock"></a>
#### Uhrzeit/Zeitzone *(Einstellungsmenü — genau ein Eintrag, kein Anlegen/Löschen)* #### Uhrzeit/Zeitzone *(Einstellungsmenü — genau ein Eintrag, kein Anlegen/Löschen)*
RouterOS-Menü: `/system clock` · REST-Pfad: `system/clock` RouterOS-Menü: `/system clock` · REST-Pfad: `system/clock`
@@ -414,6 +425,7 @@ RouterOS-Menü: `/system clock` · REST-Pfad: `system/clock`
|---|---|---|---|---|---| |---|---|---|---|---|---|
| Zeitzone | `time-zone-name` | Text | Nein | — | Z.B. Europe/Berlin. | | Zeitzone | `time-zone-name` | Text | Nein | — | Z.B. Europe/Berlin. |
<a id="schema-system-routerboard-mode-button"></a>
#### Mode-Taste *(Einstellungsmenü — genau ein Eintrag, kein Anlegen/Löschen)* #### Mode-Taste *(Einstellungsmenü — genau ein Eintrag, kein Anlegen/Löschen)*
RouterOS-Menü: `/system routerboard mode-button` · REST-Pfad: `system/routerboard/mode-button` RouterOS-Menü: `/system routerboard mode-button` · REST-Pfad: `system/routerboard/mode-button`
@@ -429,6 +441,7 @@ Manche RouterBOARD-Geräte (z.B. hEX, cAP, hAP ac², LtAP mini, einige CCR/CRS)
| Auszuführendes Skript | `on-event` | Verweis auf bestehenden Eintrag unter `/system script` | Nein | — | Name eines zuvor unter "Skripte" angelegten Skripts. | | Auszuführendes Skript | `on-event` | Verweis auf bestehenden Eintrag unter `/system script` | Nein | — | Name eines zuvor unter "Skripte" angelegten Skripts. |
| Haltedauer (Min..Max) | `hold-time` | Text | Nein | — | Wie lange die Taste gedrückt gehalten werden muss, als Zeitspanne Min..Max, z.B. "3s..5s". Verfügbar ab RouterOS 6.47beta60. | | Haltedauer (Min..Max) | `hold-time` | Text | Nein | — | Wie lange die Taste gedrückt gehalten werden muss, als Zeitspanne Min..Max, z.B. "3s..5s". Verfügbar ab RouterOS 6.47beta60. |
<a id="schema-system-ntp-client"></a>
#### Zeitserver (NTP) *(Einstellungsmenü — genau ein Eintrag, kein Anlegen/Löschen)* #### Zeitserver (NTP) *(Einstellungsmenü — genau ein Eintrag, kein Anlegen/Löschen)*
RouterOS-Menü: `/system ntp client` · REST-Pfad: `system/ntp/client` RouterOS-Menü: `/system ntp client` · REST-Pfad: `system/ntp/client`
@@ -443,6 +456,7 @@ Die Server-Liste selbst liegt in einem eigenen Menü ("NTP-Zeitserver-Liste")
| Aktiviert | `enabled` | Ja/Nein | Nein | `yes` | Zeitsynchronisation ein-/ausschalten. | | Aktiviert | `enabled` | Ja/Nein | Nein | `yes` | Zeitsynchronisation ein-/ausschalten. |
| Modus | `mode` | Auswahl (fest): `unicast`, `broadcast`, `multicast`, `manycast` | Nein | `unicast` | Fast immer "unicast" (direkte Anfrage an feste Server). | | Modus | `mode` | Auswahl (fest): `unicast`, `broadcast`, `multicast`, `manycast` | Nein | `unicast` | Fast immer "unicast" (direkte Anfrage an feste Server). |
<a id="schema-system-ntp-client-servers"></a>
#### NTP-Zeitserver-Liste #### NTP-Zeitserver-Liste
RouterOS-Menü: `/system ntp client servers` · REST-Pfad: `system/ntp/client/servers` RouterOS-Menü: `/system ntp client servers` · REST-Pfad: `system/ntp/client/servers`
@@ -456,6 +470,7 @@ Die Zeitserver, die der Client abfragt.
| Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | Diesen Server deaktivieren, ohne ihn zu löschen. | | Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | Diesen Server deaktivieren, ohne ihn zu löschen. |
| Kommentar | `comment` | Text | Nein | — | Nur zur eigenen Wiedererkennung. | | Kommentar | `comment` | Text | Nein | — | Nur zur eigenen Wiedererkennung. |
<a id="schema-system-scheduler"></a>
#### Zeitplaner #### Zeitplaner
RouterOS-Menü: `/system scheduler` · REST-Pfad: `system/scheduler` RouterOS-Menü: `/system scheduler` · REST-Pfad: `system/scheduler`
@@ -471,6 +486,7 @@ Führt ein hinterlegtes Skript zu festen Zeiten/Intervallen aus.
| Auszuführendes Skript | `on-event` | Verweis auf bestehenden Eintrag unter `/system script` | Nein | — | Name eines zuvor unter "Skripte" angelegten Skripts. | | Auszuführendes Skript | `on-event` | Verweis auf bestehenden Eintrag unter `/system script` | Nein | — | Name eines zuvor unter "Skripte" angelegten Skripts. |
| Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | Zeitplan inaktiv schalten, ohne ihn zu löschen. | | Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | Zeitplan inaktiv schalten, ohne ihn zu löschen. |
<a id="schema-system-script"></a>
#### Skripte #### Skripte
RouterOS-Menü: `/system script` · REST-Pfad: `system/script` RouterOS-Menü: `/system script` · REST-Pfad: `system/script`
@@ -483,6 +499,7 @@ Gespeicherte RouterOS-Befehlsfolgen, die manuell oder per Scheduler ausgeführt
| Name | `name` | Text | Ja | — | Frei wählbar, z.B. backup-taeglich. | | Name | `name` | Text | Ja | — | Frei wählbar, z.B. backup-taeglich. |
| Skript-Inhalt | `source` | Text | Nein | — | RouterOS-Befehle, z.B. ":log info \"Test\"". | | Skript-Inhalt | `source` | Text | Nein | — | RouterOS-Befehle, z.B. ":log info \"Test\"". |
<a id="schema-user"></a>
#### Benutzerkonten #### Benutzerkonten
RouterOS-Menü: `/user` · REST-Pfad: `user` RouterOS-Menü: `/user` · REST-Pfad: `user`
@@ -497,6 +514,7 @@ Zugangskonten für den Router (Winbox/SSH/REST/Terminal).
| Rechte-Gruppe | `group` | Auswahl (fest): `full`, `write`, `read` | Nein | `full` | full = Vollzugriff, write = ohne Benutzerverwaltung, read = nur lesen. | | Rechte-Gruppe | `group` | Auswahl (fest): `full`, `write`, `read` | Nein | `full` | full = Vollzugriff, write = ohne Benutzerverwaltung, read = nur lesen. |
| Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | Konto inaktiv schalten, ohne es zu löschen. | | Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | Konto inaktiv schalten, ohne es zu löschen. |
<a id="schema-system-logging"></a>
#### Protokollierung #### Protokollierung
RouterOS-Menü: `/system logging` · REST-Pfad: `system/logging` RouterOS-Menü: `/system logging` · REST-Pfad: `system/logging`
@@ -509,6 +527,7 @@ Besteht aus "rules" (was protokolliert wird) und "actions" (wohin) — hier gene
### Interfaces (Bridge, VLAN, VPN-Tunnel...) ### Interfaces (Bridge, VLAN, VPN-Tunnel...)
<a id="schema-interface"></a>
#### Alle Interfaces (generisch) #### Alle Interfaces (generisch)
RouterOS-Menü: `/interface` · REST-Pfad: `interface` RouterOS-Menü: `/interface` · REST-Pfad: `interface`
@@ -524,6 +543,7 @@ RouterOS listet hier alle Interfaces zusammen. Typ-spezifische Felder (z.B. die
| Kommentar | `comment` | Text | Nein | — | Nur zur eigenen Wiedererkennung, ohne technische Wirkung. | | Kommentar | `comment` | Text | Nein | — | Nur zur eigenen Wiedererkennung, ohne technische Wirkung. |
| Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | Interface inaktiv schalten, ohne es zu löschen. | | Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | Interface inaktiv schalten, ohne es zu löschen. |
<a id="schema-interface-bridge"></a>
#### Bridge #### Bridge
RouterOS-Menü: `/interface bridge` · REST-Pfad: `interface/bridge` RouterOS-Menü: `/interface bridge` · REST-Pfad: `interface/bridge`
@@ -537,6 +557,7 @@ Geräte an gebrückten Ports verhalten sich, als hingen sie am selben Netzwerk-K
| VLAN-Filterung (802.1Q) | `vlan-filtering` | Ja/Nein | Nein | `no` | Aktiviert echte VLAN-Trennung über diese Bridge — nötig, wenn mehrere VLANs über dieselben Bridge-Ports laufen sollen. | | VLAN-Filterung (802.1Q) | `vlan-filtering` | Ja/Nein | Nein | `no` | Aktiviert echte VLAN-Trennung über diese Bridge — nötig, wenn mehrere VLANs über dieselben Bridge-Ports laufen sollen. |
| Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | Bridge inaktiv schalten, ohne sie zu löschen. | | Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | Bridge inaktiv schalten, ohne sie zu löschen. |
<a id="schema-interface-bridge-port"></a>
#### Bridge-Ports #### Bridge-Ports
RouterOS-Menü: `/interface bridge port` · REST-Pfad: `interface/bridge/port` RouterOS-Menü: `/interface bridge port` · REST-Pfad: `interface/bridge/port`
@@ -550,6 +571,7 @@ Erst danach ist der Port Teil des Bridge-Netzwerks.
| Physischer Port | `interface` | Auswahl aus Live-Interface-Liste des Routers | Ja | — | Der Port, der der Bridge hinzugefügt wird, z.B. ether2. | | Physischer Port | `interface` | Auswahl aus Live-Interface-Liste des Routers | Ja | — | Der Port, der der Bridge hinzugefügt wird, z.B. ether2. |
| Port-VLAN-ID (PVID) | `pvid` | Zahl | Nein | — | Nur mit aktivierter VLAN-Filterung relevant: VLAN, dem untagged ankommender Verkehr an diesem Port zugeordnet wird, z.B. 20. | | Port-VLAN-ID (PVID) | `pvid` | Zahl | Nein | — | Nur mit aktivierter VLAN-Filterung relevant: VLAN, dem untagged ankommender Verkehr an diesem Port zugeordnet wird, z.B. 20. |
<a id="schema-interface-vlan"></a>
#### VLAN-Interfaces #### VLAN-Interfaces
RouterOS-Menü: `/interface vlan` · REST-Pfad: `interface/vlan` RouterOS-Menü: `/interface vlan` · REST-Pfad: `interface/vlan`
@@ -564,6 +586,7 @@ Ein eigenes logisches Netzwerk auf demselben Kabel, unterschieden durch eine VLA
| Basis-Interface | `interface` | Auswahl aus Live-Interface-Liste des Routers | Ja | — | Physischer Port oder Bridge, auf dem dieses VLAN aufsetzt, z.B. bridge. | | Basis-Interface | `interface` | Auswahl aus Live-Interface-Liste des Routers | Ja | — | Physischer Port oder Bridge, auf dem dieses VLAN aufsetzt, z.B. bridge. |
| Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | VLAN-Interface inaktiv schalten, ohne es zu löschen. | | Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | VLAN-Interface inaktiv schalten, ohne es zu löschen. |
<a id="schema-interface-wireguard"></a>
#### WireGuard-Interfaces #### WireGuard-Interfaces
RouterOS-Menü: `/interface wireguard` · REST-Pfad: `interface/wireguard` RouterOS-Menü: `/interface wireguard` · REST-Pfad: `interface/wireguard`
@@ -580,6 +603,7 @@ Ein WireGuard-Interface allein stellt noch keine Verbindung her — dazu gehöre
| Privater Schlüssel | `private-key` | Text | Nein | — | Geheim halten. Leer lassen, damit RouterOS automatisch einen erzeugt. | | Privater Schlüssel | `private-key` | Text | Nein | — | Geheim halten. Leer lassen, damit RouterOS automatisch einen erzeugt. |
| Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | Interface inaktiv schalten, ohne es zu löschen. | | Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | Interface inaktiv schalten, ohne es zu löschen. |
<a id="schema-interface-wireguard-peers"></a>
#### WireGuard-Peers #### WireGuard-Peers
RouterOS-Menü: `/interface wireguard peers` · REST-Pfad: `interface/wireguard/peers` RouterOS-Menü: `/interface wireguard peers` · REST-Pfad: `interface/wireguard/peers`
@@ -596,6 +620,7 @@ Jede Gegenstelle braucht ihren eigenen öffentlichen Schlüssel und eine Angabe,
| Port der Gegenstelle | `endpoint-port` | Text | Nein | — | Meist derselbe Port wie der Listen-Port der Gegenstelle, z.B. 51820. | | Port der Gegenstelle | `endpoint-port` | Text | Nein | — | Meist derselbe Port wie der Listen-Port der Gegenstelle, z.B. 51820. |
| Keepalive | `persistent-keepalive` | Zeitdauer (Tage/Std/Min/Sek, per Stepper) | Nein | — | Hält die Verbindung durch NAT/Firewalls am Leben. Wichtig bei Clients hinter NAT. | | Keepalive | `persistent-keepalive` | Zeitdauer (Tage/Std/Min/Sek, per Stepper) | Nein | — | Hält die Verbindung durch NAT/Firewalls am Leben. Wichtig bei Clients hinter NAT. |
<a id="schema-interface-pppoe-client"></a>
#### PPPoE-Client #### PPPoE-Client
RouterOS-Menü: `/interface pppoe-client` · REST-Pfad: `interface/pppoe-client` RouterOS-Menü: `/interface pppoe-client` · REST-Pfad: `interface/pppoe-client`
@@ -611,6 +636,7 @@ Ersetzt eine feste/DHCP-WAN-Adresse durch eine PPPoE-Einwahl beim Provider.
| Passwort | `password` | Text | Ja | — | Zugangsdaten des Providers. | | Passwort | `password` | Text | Ja | — | Zugangsdaten des Providers. |
| Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | Einwahl inaktiv schalten, ohne sie zu löschen. | | Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | Einwahl inaktiv schalten, ohne sie zu löschen. |
<a id="schema-interface-bonding"></a>
#### Bonding #### Bonding
RouterOS-Menü: `/interface bonding` · REST-Pfad: `interface/bonding` RouterOS-Menü: `/interface bonding` · REST-Pfad: `interface/bonding`
@@ -625,6 +651,7 @@ Bündelt mehrere physische Ports zu einer logischen, ausfalltoleranten/schneller
### IP-Adressierung & Dienste ### IP-Adressierung & Dienste
<a id="schema-ip-address"></a>
#### IP-Adressen #### IP-Adressen
RouterOS-Menü: `/ip address` · REST-Pfad: `ip/address` RouterOS-Menü: `/ip address` · REST-Pfad: `ip/address`
@@ -638,6 +665,7 @@ Jede IP-Adresse hängt an genau einem Interface (physischer Port, Bridge oder VL
| Interface | `interface` | Auswahl aus Live-Interface-Liste des Routers | Ja | — | Interface, dem diese Adresse zugewiesen wird, z.B. bridge oder ether4. | | Interface | `interface` | Auswahl aus Live-Interface-Liste des Routers | Ja | — | Interface, dem diese Adresse zugewiesen wird, z.B. bridge oder ether4. |
| Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | Adresse inaktiv schalten, ohne sie zu löschen. | | Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | Adresse inaktiv schalten, ohne sie zu löschen. |
<a id="schema-ip-pool"></a>
#### Adress-Pools #### Adress-Pools
RouterOS-Menü: `/ip pool` · REST-Pfad: `ip/pool` RouterOS-Menü: `/ip pool` · REST-Pfad: `ip/pool`
@@ -648,6 +676,7 @@ Adressbereiche, aus denen DHCP-Server oder PPP-Profile Adressen vergeben.
| Name | `name` | Text | Ja | — | Frei wählbar, z.B. dhcp_pool_lan. | | Name | `name` | Text | Ja | — | Frei wählbar, z.B. dhcp_pool_lan. |
| Bereich(e) | `ranges` | Text | Ja | — | Von-bis-Adresse ohne Präfix, z.B. 192.168.88.10-192.168.88.254. Mehrere Bereiche kommagetrennt. | | Bereich(e) | `ranges` | Text | Ja | — | Von-bis-Adresse ohne Präfix, z.B. 192.168.88.10-192.168.88.254. Mehrere Bereiche kommagetrennt. |
<a id="schema-ip-dhcp-server"></a>
#### DHCP-Server #### DHCP-Server
RouterOS-Menü: `/ip dhcp-server` · REST-Pfad: `ip/dhcp-server` RouterOS-Menü: `/ip dhcp-server` · REST-Pfad: `ip/dhcp-server`
@@ -663,6 +692,7 @@ Für die üblichen Fälle deckt das bereits der Einrichten-Assistent (LAN-/VLAN-
| Lease-Zeit | `lease-time` | Zeitdauer (Tage/Std/Min/Sek, per Stepper) | Nein | — | Wie lange ein Gerät seine Adresse behält, bevor sie erneuert werden muss, z.B. 1d oder 12h. | | Lease-Zeit | `lease-time` | Zeitdauer (Tage/Std/Min/Sek, per Stepper) | Nein | — | Wie lange ein Gerät seine Adresse behält, bevor sie erneuert werden muss, z.B. 1d oder 12h. |
| Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | DHCP-Server inaktiv schalten, ohne ihn zu löschen. | | Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | DHCP-Server inaktiv schalten, ohne ihn zu löschen. |
<a id="schema-ip-dhcp-server-network"></a>
#### DHCP-Netzwerke #### DHCP-Netzwerke
RouterOS-Menü: `/ip dhcp-server network` · REST-Pfad: `ip/dhcp-server/network` RouterOS-Menü: `/ip dhcp-server network` · REST-Pfad: `ip/dhcp-server/network`
@@ -676,6 +706,7 @@ Getrennt vom DHCP-Server selbst, weil dieselben Netzwerk-Optionen für mehrere D
| Gateway | `gateway` | Text | Ja | — | In der Regel die Router-Adresse in diesem Netz, ohne Präfix, z.B. 192.168.88.1. | | Gateway | `gateway` | Text | Ja | — | In der Regel die Router-Adresse in diesem Netz, ohne Präfix, z.B. 192.168.88.1. |
| DNS-Server | `dns-server` | Text | Nein | — | Meist der Router selbst, z.B. 192.168.88.1. Mehrere Server kommagetrennt möglich. | | DNS-Server | `dns-server` | Text | Nein | — | Meist der Router selbst, z.B. 192.168.88.1. Mehrere Server kommagetrennt möglich. |
<a id="schema-ip-dhcp-client"></a>
#### DHCP-Client (WAN) #### DHCP-Client (WAN)
RouterOS-Menü: `/ip dhcp-client` · REST-Pfad: `ip/dhcp-client` RouterOS-Menü: `/ip dhcp-client` · REST-Pfad: `ip/dhcp-client`
@@ -688,6 +719,7 @@ Bezieht automatisch eine IP-Adresse vom Internetanbieter.
| DNS-Server übernehmen | `use-peer-dns` | Ja/Nein | Nein | `yes` | Übernimmt die vom Provider mitgeteilten DNS-Server. | | DNS-Server übernehmen | `use-peer-dns` | Ja/Nein | Nein | `yes` | Übernimmt die vom Provider mitgeteilten DNS-Server. |
| Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | DHCP-Client inaktiv schalten, ohne ihn zu löschen. | | Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | DHCP-Client inaktiv schalten, ohne ihn zu löschen. |
<a id="schema-ip-dns"></a>
#### DNS-Einstellungen *(Einstellungsmenü — genau ein Eintrag, kein Anlegen/Löschen)* #### DNS-Einstellungen *(Einstellungsmenü — genau ein Eintrag, kein Anlegen/Löschen)*
RouterOS-Menü: `/ip dns` · REST-Pfad: `ip/dns` RouterOS-Menü: `/ip dns` · REST-Pfad: `ip/dns`
@@ -698,6 +730,7 @@ Namensauflösung des Routers selbst (und optional als DNS-Server fürs LAN).
| DNS-Server | `servers` | Text | Nein | — | Ein oder mehrere Server, kommagetrennt, z.B. 1.1.1.1,8.8.8.8. | | DNS-Server | `servers` | Text | Nein | — | Ein oder mehrere Server, kommagetrennt, z.B. 1.1.1.1,8.8.8.8. |
| Als DNS-Server fürs LAN erlauben | `allow-remote-requests` | Ja/Nein | Nein | `no` | Lässt Geräte im LAN den Router selbst als DNS-Server nutzen. Ohne das schlagen DNS-Anfragen von Geräten an den Router fehl, selbst wenn sie ihn als DNS-Server eingetragen haben. | | Als DNS-Server fürs LAN erlauben | `allow-remote-requests` | Ja/Nein | Nein | `no` | Lässt Geräte im LAN den Router selbst als DNS-Server nutzen. Ohne das schlagen DNS-Anfragen von Geräten an den Router fehl, selbst wenn sie ihn als DNS-Server eingetragen haben. |
<a id="schema-ip-service"></a>
#### Verwaltungsdienste #### Verwaltungsdienste
RouterOS-Menü: `/ip service` · REST-Pfad: `ip/service` RouterOS-Menü: `/ip service` · REST-Pfad: `ip/service`
@@ -714,6 +747,7 @@ Jeder aktive Dienst ist ein potenzieller Angriffspunkt aus dem jeweils erreichba
| Erlaubt von | `available-from` | Text | Nein | — | Optional: nur von dieser Adresse/diesem Netz aus erreichbar, mit Präfix, z.B. 192.168.88.0/24. | | Erlaubt von | `available-from` | Text | Nein | — | Optional: nur von dieser Adresse/diesem Netz aus erreichbar, mit Präfix, z.B. 192.168.88.0/24. |
| Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | Dienst inaktiv schalten, ohne den Eintrag zu löschen. | | Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | Dienst inaktiv schalten, ohne den Eintrag zu löschen. |
<a id="schema-ip-hotspot"></a>
#### Hotspot #### Hotspot
RouterOS-Menü: `/ip hotspot` · REST-Pfad: `ip/hotspot` RouterOS-Menü: `/ip hotspot` · REST-Pfad: `ip/hotspot`
@@ -726,6 +760,7 @@ Besteht aus mehreren zusammenhängenden Teilen (Server, Server-Profil, Benutzer-
### Routing ### Routing
<a id="schema-ip-route"></a>
#### Statische Routen #### Statische Routen
RouterOS-Menü: `/ip route` · REST-Pfad: `ip/route` RouterOS-Menü: `/ip route` · REST-Pfad: `ip/route`
@@ -741,6 +776,7 @@ Für alles außer "Standard-Internet über eine WAN-Schnittstelle" (das übernim
| Kommentar | `comment` | Text | Nein | — | Nur zur eigenen Wiedererkennung. | | Kommentar | `comment` | Text | Nein | — | Nur zur eigenen Wiedererkennung. |
| Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | Route inaktiv schalten, ohne sie zu löschen. | | Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | Route inaktiv schalten, ohne sie zu löschen. |
<a id="schema-routing-ospf-instance"></a>
#### OSPF-Instanzen #### OSPF-Instanzen
RouterOS-Menü: `/routing ospf instance` · REST-Pfad: `routing/ospf/instance` RouterOS-Menü: `/routing ospf instance` · REST-Pfad: `routing/ospf/instance`
@@ -752,6 +788,7 @@ Nur relevant, wenn mehrere Router im selben Netz eigenständig Routen lernen sol
*Noch kein kuratiertes Formular — alle Felder erscheinen als freie Schlüssel/Wert-Paare (siehe „Eigener Menüpfad“).* *Noch kein kuratiertes Formular — alle Felder erscheinen als freie Schlüssel/Wert-Paare (siehe „Eigener Menüpfad“).*
<a id="schema-routing-bgp-connection"></a>
#### BGP-Verbindungen #### BGP-Verbindungen
RouterOS-Menü: `/routing bgp connection` · REST-Pfad: `routing/bgp/connection` RouterOS-Menü: `/routing bgp connection` · REST-Pfad: `routing/bgp/connection`
@@ -764,6 +801,7 @@ Für Privat-/Kleinnetz i.d.R. nicht nötig — relevant bei eigenem Provider-una
### VPN-Server/Clients ### VPN-Server/Clients
<a id="schema-ppp-secret"></a>
#### PPP-Benutzer #### PPP-Benutzer
RouterOS-Menü: `/ppp secret` · REST-Pfad: `ppp/secret` RouterOS-Menü: `/ppp secret` · REST-Pfad: `ppp/secret`
@@ -781,6 +819,7 @@ Jeder Benutzer kann optional einem Profil zugeordnet werden, das IP-Pool/DNS/Ver
| Adresse für den Client | `remote-address` | Text | Nein | — | Feste IP für diesen Benutzer (z.B. 10.10.10.2), oder Name eines Adress-Pools. | | Adresse für den Client | `remote-address` | Text | Nein | — | Feste IP für diesen Benutzer (z.B. 10.10.10.2), oder Name eines Adress-Pools. |
| Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | Benutzer inaktiv schalten, ohne ihn zu löschen. | | Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | Benutzer inaktiv schalten, ohne ihn zu löschen. |
<a id="schema-ppp-profile"></a>
#### PPP-Profile #### PPP-Profile
RouterOS-Menü: `/ppp profile` · REST-Pfad: `ppp/profile` RouterOS-Menü: `/ppp profile` · REST-Pfad: `ppp/profile`
@@ -793,6 +832,7 @@ Vorlagen (IP-Pool, DNS, Verschlüsselung) für PPP-Benutzer.
| Adress-Pool für Clients | `remote-address` | Text | Nein | — | Name eines zuvor angelegten Adress-Pools. | | Adress-Pool für Clients | `remote-address` | Text | Nein | — | Name eines zuvor angelegten Adress-Pools. |
| DNS-Server für Clients | `dns-server` | Text | Nein | — | Ein oder mehrere Server, kommagetrennt, z.B. 1.1.1.1,8.8.8.8. | | DNS-Server für Clients | `dns-server` | Text | Nein | — | Ein oder mehrere Server, kommagetrennt, z.B. 1.1.1.1,8.8.8.8. |
<a id="schema-interface-l2tp-server-server"></a>
#### L2TP-VPN-Server #### L2TP-VPN-Server
RouterOS-Menü: `/interface l2tp-server server` · REST-Pfad: `interface/l2tp-server/server` RouterOS-Menü: `/interface l2tp-server server` · REST-Pfad: `interface/l2tp-server/server`
@@ -802,6 +842,7 @@ Ein einzelnes Server-weites An/Aus mit gemeinsamen Verschlüsselungs-Einstellung
*Noch kein kuratiertes Formular — alle Felder erscheinen als freie Schlüssel/Wert-Paare (siehe „Eigener Menüpfad“).* *Noch kein kuratiertes Formular — alle Felder erscheinen als freie Schlüssel/Wert-Paare (siehe „Eigener Menüpfad“).*
<a id="schema-interface-ovpn-server-server"></a>
#### OpenVPN-Server #### OpenVPN-Server
RouterOS-Menü: `/interface ovpn-server server` · REST-Pfad: `interface/ovpn-server/server` RouterOS-Menü: `/interface ovpn-server server` · REST-Pfad: `interface/ovpn-server/server`
@@ -814,6 +855,7 @@ Braucht zusätzlich ein Zertifikat ("/certificate") — Benutzer selbst kommen v
### WLAN / CAPsMAN ### WLAN / CAPsMAN
<a id="schema-interface-wireless"></a>
#### WLAN (Legacy-Treiber) #### WLAN (Legacy-Treiber)
RouterOS-Menü: `/interface wireless` · REST-Pfad: `interface/wireless` RouterOS-Menü: `/interface wireless` · REST-Pfad: `interface/wireless`
@@ -828,6 +870,7 @@ Diese Interfaces existieren bereits ab Werk (ein WLAN-Chip = ein Interface) —
| Modus | `mode` | Auswahl (fest): `ap-bridge`, `station`, `bridge` | Nein | `ap-bridge` | ap-bridge = Access Point (Normalfall), station = als Client mit einem anderen AP verbinden. | | Modus | `mode` | Auswahl (fest): `ap-bridge`, `station`, `bridge` | Nein | `ap-bridge` | ap-bridge = Access Point (Normalfall), station = als Client mit einem anderen AP verbinden. |
| Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | WLAN-Interface inaktiv schalten, ohne es zu löschen. | | Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | WLAN-Interface inaktiv schalten, ohne es zu löschen. |
<a id="schema-interface-wireless-security-profiles"></a>
#### WLAN-Sicherheitsprofile #### WLAN-Sicherheitsprofile
RouterOS-Menü: `/interface wireless security-profiles` · REST-Pfad: `interface/wireless/security-profiles` RouterOS-Menü: `/interface wireless security-profiles` · REST-Pfad: `interface/wireless/security-profiles`
@@ -842,6 +885,7 @@ Ein Profil wird angelegt und dann bei einem WLAN-Interface als "security-profile
| Authentifizierung | `authentication-types` | Auswahl (fest): `wpa-psk`, `wpa2-psk`, `wpa-psk,wpa2-psk`, `wpa-eap`, `wpa2-eap` | Nein | — | wpa2-psk = WPA2 mit gemeinsamem Passwort (Heimnetz-Standard). | | Authentifizierung | `authentication-types` | Auswahl (fest): `wpa-psk`, `wpa2-psk`, `wpa-psk,wpa2-psk`, `wpa-eap`, `wpa2-eap` | Nein | — | wpa2-psk = WPA2 mit gemeinsamem Passwort (Heimnetz-Standard). |
| WPA2-Passwort | `wpa2-pre-shared-key` | Text | Nein | — | Mindestens 8 Zeichen. | | WPA2-Passwort | `wpa2-pre-shared-key` | Text | Nein | — | Mindestens 8 Zeichen. |
<a id="schema-interface-wifi"></a>
#### WLAN (neuer wifiwave2/802.11ax-Treiber) #### WLAN (neuer wifiwave2/802.11ax-Treiber)
RouterOS-Menü: `/interface wifi` · REST-Pfad: `interface/wifi` RouterOS-Menü: `/interface wifi` · REST-Pfad: `interface/wifi`
@@ -859,6 +903,7 @@ Anderes, verschachteltes Konfigurationsschema als der Legacy-Treiber (Punkt-Nota
| Ziel-Bridge | `datapath.bridge` | Text | Nein | — | Name der Bridge, der dieses WLAN-Netz zugeordnet wird (üblicherweise dieselbe wie das kabelgebundene LAN), z.B. bridge. | | Ziel-Bridge | `datapath.bridge` | Text | Nein | — | Name der Bridge, der dieses WLAN-Netz zugeordnet wird (üblicherweise dieselbe wie das kabelgebundene LAN), z.B. bridge. |
| Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | WLAN-Interface inaktiv schalten, ohne es zu löschen. | | Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | WLAN-Interface inaktiv schalten, ohne es zu löschen. |
<a id="schema-caps-man-manager"></a>
#### CAPsMAN-Zentrale #### CAPsMAN-Zentrale
RouterOS-Menü: `/caps-man manager` · REST-Pfad: `caps-man/manager` RouterOS-Menü: `/caps-man manager` · REST-Pfad: `caps-man/manager`
@@ -871,6 +916,7 @@ Nur relevant mit mehreren WLAN-Access-Points, die zentral verwaltet werden solle
### Firewall: Filter-Regeln ### Firewall: Filter-Regeln
<a id="schema-ip-firewall-filter"></a>
#### Filter-Regeln #### Filter-Regeln
RouterOS-Menü: `/ip firewall filter` · REST-Pfad: `ip/firewall/filter` RouterOS-Menü: `/ip firewall filter` · REST-Pfad: `ip/firewall/filter`
@@ -900,6 +946,7 @@ input = Zugriffe auf den Router selbst, forward = Verkehr, der durch den Router
### Firewall: NAT (Portweiterleitung etc.) ### Firewall: NAT (Portweiterleitung etc.)
<a id="schema-ip-firewall-nat"></a>
#### NAT-Regeln #### NAT-Regeln
RouterOS-Menü: `/ip firewall nat` · REST-Pfad: `ip/firewall/nat` RouterOS-Menü: `/ip firewall nat` · REST-Pfad: `ip/firewall/nat`
@@ -927,6 +974,7 @@ srcnat ändert die Absenderadresse ausgehender Pakete (z.B. private LAN-IP →
### Firewall: Mangle (Markierung/QoS-Vorbereitung) ### Firewall: Mangle (Markierung/QoS-Vorbereitung)
<a id="schema-ip-firewall-mangle"></a>
#### Mangle-Regeln #### Mangle-Regeln
RouterOS-Menü: `/ip firewall mangle` · REST-Pfad: `ip/firewall/mangle` RouterOS-Menü: `/ip firewall mangle` · REST-Pfad: `ip/firewall/mangle`
@@ -955,6 +1003,7 @@ Mangle selbst verändert nicht, wie ein Paket behandelt wird — es klebt nur ei
### Firewall: Raw (vor Connection-Tracking) ### Firewall: Raw (vor Connection-Tracking)
<a id="schema-ip-firewall-raw"></a>
#### Raw-Regeln #### Raw-Regeln
RouterOS-Menü: `/ip firewall raw` · REST-Pfad: `ip/firewall/raw` RouterOS-Menü: `/ip firewall raw` · REST-Pfad: `ip/firewall/raw`
@@ -979,6 +1028,7 @@ Regeln hier greifen, bevor RouterOS eine Verbindung überhaupt "kennt" (Connecti
### Firewall: Adress-Listen ### Firewall: Adress-Listen
<a id="schema-ip-firewall-address-list"></a>
#### Adress-Listen #### Adress-Listen
RouterOS-Menü: `/ip firewall address-list` · REST-Pfad: `ip/firewall/address-list` RouterOS-Menü: `/ip firewall address-list` · REST-Pfad: `ip/firewall/address-list`
@@ -996,6 +1046,7 @@ Statt in jeder Regel einzelne IPs aufzuzählen, legst du hier eine benannte List
### Queues / Bandbreiten-Steuerung ### Queues / Bandbreiten-Steuerung
<a id="schema-queue-simple"></a>
#### Einfache Bandbreiten-Begrenzung #### Einfache Bandbreiten-Begrenzung
RouterOS-Menü: `/queue simple` · REST-Pfad: `queue/simple` RouterOS-Menü: `/queue simple` · REST-Pfad: `queue/simple`
@@ -1011,6 +1062,7 @@ Reicht für die meisten Heim-/Kleinnetz-Fälle ohne Mangle-Markierungen aus.
| Burst-Bandbreite | `burst-limit` | Text | Nein | — | Optional: kurzzeitig erlaubte höhere Bandbreite, z.B. 15M/60M. | | Burst-Bandbreite | `burst-limit` | Text | Nein | — | Optional: kurzzeitig erlaubte höhere Bandbreite, z.B. 15M/60M. |
| Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | Begrenzung inaktiv schalten, ohne sie zu löschen. | | Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | Begrenzung inaktiv schalten, ohne sie zu löschen. |
<a id="schema-queue-tree"></a>
#### Queue-Baum #### Queue-Baum
RouterOS-Menü: `/queue tree` · REST-Pfad: `queue/tree` RouterOS-Menü: `/queue tree` · REST-Pfad: `queue/tree`
@@ -1031,6 +1083,7 @@ Statt einer festen Adresse wirkt ein Queue-Baum-Eintrag auf Verkehr mit einer be
### Werkzeuge & Überwachung ### Werkzeuge & Überwachung
<a id="schema-tool-netwatch"></a>
#### Netwatch #### Netwatch
RouterOS-Menü: `/tool netwatch` · REST-Pfad: `tool/netwatch` RouterOS-Menü: `/tool netwatch` · REST-Pfad: `tool/netwatch`
@@ -1044,6 +1097,7 @@ RouterOS-Menü: `/tool netwatch` · REST-Pfad: `tool/netwatch`
| Skript bei Nichterreichbarkeit | `down-script` | Text | Nein | — | Name eines Skripts unter "Skripte". | | Skript bei Nichterreichbarkeit | `down-script` | Text | Nein | — | Name eines Skripts unter "Skripte". |
| Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | Überwachung inaktiv schalten, ohne sie zu löschen. | | Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | Überwachung inaktiv schalten, ohne sie zu löschen. |
<a id="schema-tool-e-mail"></a>
#### E-Mail-Versand *(Einstellungsmenü — genau ein Eintrag, kein Anlegen/Löschen)* #### E-Mail-Versand *(Einstellungsmenü — genau ein Eintrag, kein Anlegen/Löschen)*
RouterOS-Menü: `/tool e-mail` · REST-Pfad: `tool/e-mail` RouterOS-Menü: `/tool e-mail` · REST-Pfad: `tool/e-mail`
@@ -1063,6 +1117,7 @@ Ein einzelner, geräteweiter Satz Einstellungen — kein Menü mit mehreren Eint
--- ---
<a id="tab-backup"></a>
## 6. Sicherungen ## 6. Sicherungen
![Sicherung/Wiederherstellung](Manual-assets/backup_restore.png) ![Sicherung/Wiederherstellung](Manual-assets/backup_restore.png)
@@ -1090,6 +1145,7 @@ Ein einzelner, geräteweiter Satz Einstellungen — kein Menü mit mehreren Eint
--- ---
<a id="tab-settings"></a>
## 7. Einstellungen ## 7. Einstellungen
Erreichbar über das App-Menü **RouterOS Assistant → Einstellungen…** Erreichbar über das App-Menü **RouterOS Assistant → Einstellungen…**
@@ -1130,66 +1186,3 @@ mit drei Reitern. Jede Änderung wirkt sofort, ohne Neustart der App.
- **Sparkline-Zeitfenster** (10 s / 30 s / 60 s) und - **Sparkline-Zeitfenster** (10 s / 30 s / 60 s) und
**Sparkline-Breite** (100300pt): Länge bzw. Breite der kleinen **Sparkline-Breite** (100300pt): Länge bzw. Breite der kleinen
Traffic-Verlaufsgrafik neben jeder Port-Überschrift im LAN-Scanner. Traffic-Verlaufsgrafik neben jeder Port-Überschrift im LAN-Scanner.
---
## 8. English summary
RouterOS Assistant is a native macOS app that sets up and manages
MikroTik RouterOS routers through a guided wizard and six tabs: Connect,
Setup, Topology, LAN Scanner, Expert, Backups. The app talks to the
router over its REST API (preferred, RouterOS ≥ 7.1) or SSH (fallback,
and mandatory for backup/update-check/firmware-update, which only exist
as CLI commands) — chosen automatically, no configuration needed. A
DE/EN toggle button (flag icon) in the toolbar switches the app's
language and persists across restarts.
- **Connect**: enter host/username/password; first connection shows a
trust-on-first-use dialog for an unknown TLS certificate (REST) or SSH
host key (SSH) — for a REST connection, both can appear in sequence,
since backup/update-check/firmware always need SSH regardless of the
main connection path. "Known Routers" remembers host+username+serial
number per device (so two routers sharing MikroTik's factory default
don't collide), with a separate keychain entry per device and an
editable display name/location field.
- **Setup Wizard**: Simple mode (WAN + one LAN + WLAN + a fixed basic
firewall, no VLANs/isolation) or Expert mode (multiple LAN networks
each with their own DHCP server, VLANs, network isolation, full
firewall control). Every step's fields carry the exact same help text
as the German version (see chapter 2) — this summary doesn't duplicate
them. Review/Apply shows every command before running it and takes an
automatic backup first.
- **Topology**: a live diagram of the router's complete current state —
five columns (Interfaces → IP addresses → Pools & DHCP → Routes →
Firewall & NAT) connected by real dependency lines. Clicking a node
opens **Focus Mode**: a floating popup showing that node's complete
connected chain (full transitive closure) neatly re-laid-out, with
everything else in the main diagram dimmed — closable via its own X
button, a click on empty space, or re-clicking the same node. The
right-hand sidebar stays interactive while focused (it's a non-modal
overlay, not a system sheet), so a chained node can be edited directly
from the popup. Most node kinds are directly editable in place.
- **LAN Scanner**: every device from DHCP leases + ARP, grouped by
physical port, with live per-port throughput. Per-device actions:
assign/remove a static IP, and network tools (ping/traceroute/DNS
lookup run from the router itself over SSH; a port scan runs from this
Mac directly). All three result popups share the same fixed
header-with-close-button layout as Topology's focus popup and the
Expert tab's edit form.
- **Expert**: curated, tooltip-annotated direct access to most RouterOS
areas (see chapter 5 for the full field-by-field German reference,
extracted verbatim from the app's source), plus a free-form "custom
menu path" for anything not yet curated. Every change shows a
confirmation dialog with the exact command that will run first.
- **Backups**: create/restore backups over SFTP (model-checked before
restoring, to avoid bricking the device with the wrong backup),
configurable save location, and a danger-zone factory-reset switch
(auto-backs-up first).
- **Settings** (⌘,, native macOS settings window, not a tab): language,
auto-check for RouterOS updates on connect, a color theme (Standard/
High Contrast — Topology diagram, LAN Scanner status colors, Expert
sidebar categories), text size (with two deliberate exceptions: the
Topology diagram's node cards and the LAN Scanner's fixed-width table
columns stay fixed-size, or their layout would break), control-element
size, and the LAN Scanner's refresh rate/sparkline window/sparkline
width. Every change applies live, no restart needed.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+1
View File
@@ -170,6 +170,7 @@ nur die zugehörigen Passwörter liegen weiterhin im macOS-Schlüsselbund.
| M28 | Übersicht-Tab: Fokus-Modus (Klick auf Knoten → Kette im schwebenden Popup, Rest abgedunkelt); Close-Button-Layout in allen vier Popups vereinheitlicht | ✅ live verifiziert | | M28 | Übersicht-Tab: Fokus-Modus (Klick auf Knoten → Kette im schwebenden Popup, Rest abgedunkelt); Close-Button-Layout in allen vier Popups vereinheitlicht | ✅ live verifiziert |
| M29 | Einstellungen-Fenster (⌘,): Sprache, Update-Auto-Check, Farbschema, Textgröße, Bedienelement-Größe, LAN-Scanner-Refreshraten | ✅ live verifiziert | | M29 | Einstellungen-Fenster (⌘,): Sprache, Update-Auto-Check, Farbschema, Textgröße, Bedienelement-Größe, LAN-Scanner-Refreshraten | ✅ live verifiziert |
| M30 | Experte-Tab: "Mode-Taste"-Menü (`/system routerboard mode-button`), inkl. SSH-Zwangsweg für REST-Deckungslücken | ✅ live verifiziert | | M30 | Experte-Tab: "Mode-Taste"-Menü (`/system routerboard mode-button`), inkl. SSH-Zwangsweg für REST-Deckungslücken | ✅ live verifiziert |
| M31 | Handbuch in der App (⌘? -Buttons, Textanker, Übersicht/Wizard/45 Experte-Menüs), DE+EN vollständig übersetzt | ✅ live verifiziert |
Ausführlicher Stand inkl. aller gefundenen Bugs, offener Punkte und Ausführlicher Stand inkl. aller gefundenen Bugs, offener Punkte und
Session-Verlauf: [`HANDOFF.md`](HANDOFF.md) / [`CHATLOG.md`](CHATLOG.md). Session-Verlauf: [`HANDOFF.md`](HANDOFF.md) / [`CHATLOG.md`](CHATLOG.md).
@@ -3,6 +3,7 @@ import SwiftUI
@main @main
struct RouterOSAssistantApp: App { struct RouterOSAssistantApp: App {
@StateObject private var connectionService = ConnectionService() @StateObject private var connectionService = ConnectionService()
@StateObject private var manualNavigator = ManualNavigator()
/// Manual language override independent of the system locale, per the user's explicit /// Manual language override independent of the system locale, per the user's explicit
/// request for an in-app toggle button. `.environment(\.locale, ...)` does NOT make /// request for an in-app toggle button. `.environment(\.locale, ...)` does NOT make
/// `Text(LocalizedStringKey)` re-resolve against Localizable.xcstrings at runtime (confirmed /// `Text(LocalizedStringKey)` re-resolve against Localizable.xcstrings at runtime (confirmed
@@ -53,11 +54,18 @@ struct RouterOSAssistantApp: App {
// (\.font, ...)` only ever supplies a *default*: anywhere `.appFont(...)` already sets // (\.font, ...)` only ever supplies a *default*: anywhere `.appFont(...)` already sets
// an explicit font, that still wins. // an explicit font, that still wins.
.environment(\.font, .system(size: 13 * ((AppTextSize(rawValue: textSizeRaw) ?? .standard).scale))) .environment(\.font, .system(size: 13 * ((AppTextSize(rawValue: textSizeRaw) ?? .standard).scale)))
.environmentObject(manualNavigator)
} }
.environment(\.locale, Locale(identifier: appLanguage)) .environment(\.locale, Locale(identifier: appLanguage))
Settings { Settings {
SettingsView() SettingsView()
.environmentObject(manualNavigator)
}
Window(L10n.t("Handbuch", appLanguage), id: "manual") {
ManualView()
.environmentObject(manualNavigator)
} }
} }
} }
@@ -16,6 +16,8 @@ enum L10n {
private static let translations: [String: String] = [ private static let translations: [String: String] = [
"Verbinden": "Connect", "Verbinden": "Connect",
"Handbuch": "Manual",
"Hilfe zu diesem Bereich im Handbuch öffnen": "Open help for this area in the manual",
"Einrichten": "Setup", "Einrichten": "Setup",
"Übersicht": "Topology", "Übersicht": "Topology",
"LAN-Scanner": "LAN Scanner", "LAN-Scanner": "LAN Scanner",
@@ -212,6 +212,7 @@ struct BackupListView: View {
} }
} }
.navigationTitle(LocalizedStringKey(L10n.t("Sicherungen", appLanguage))) .navigationTitle(LocalizedStringKey(L10n.t("Sicherungen", appLanguage)))
.toolbar { ToolbarItem { ManualHelpButton(anchor: ManualAnchor.tabBackup) } }
.toolbar { .toolbar {
ToolbarItem { ToolbarItem {
Button { Button {
@@ -120,6 +120,7 @@ struct DevicesView: View {
} }
} }
.navigationTitle(LocalizedStringKey(L10n.t("LAN-Scanner", appLanguage))) .navigationTitle(LocalizedStringKey(L10n.t("LAN-Scanner", appLanguage)))
.toolbar { ToolbarItem { ManualHelpButton(anchor: ManualAnchor.tabDevices) } }
.toolbar { .toolbar {
ToolbarItem { ToolbarItem {
Button { Button {
@@ -79,6 +79,7 @@ struct ExpertMenuDetailView: View {
// See DevicesView's identical fix: `.formStyle(.grouped)` paints an opaque background // See DevicesView's identical fix: `.formStyle(.grouped)` paints an opaque background
// over each Section's rows, hiding `.listRowBackground` (Zebra-Streifen) underneath it. // over each Section's rows, hiding `.listRowBackground` (Zebra-Streifen) underneath it.
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.toolbar { ToolbarItem { ManualHelpButton(anchor: ManualAnchor.schema(schema.menuPath)) } }
.navigationTitle(LocalizedStringKey(L10n.t(schema.displayName, appLanguage))) .navigationTitle(LocalizedStringKey(L10n.t(schema.displayName, appLanguage)))
.task(id: schema.id) { await viewModel.reloadItems() } .task(id: schema.id) { await viewModel.reloadItems() }
.sheet(item: $viewModel.editingItem) { item in .sheet(item: $viewModel.editingItem) { item in
@@ -84,6 +84,7 @@ struct ExpertView: View {
} }
.listStyle(.sidebar) .listStyle(.sidebar)
.navigationTitle(LocalizedStringKey(L10n.t("Experte", appLanguage))) .navigationTitle(LocalizedStringKey(L10n.t("Experte", appLanguage)))
.toolbar { ToolbarItem { ManualHelpButton(anchor: ManualAnchor.tabExpert) } }
} detail: { } detail: {
if let schema = viewModel.selectedSchema { if let schema = viewModel.selectedSchema {
ExpertMenuDetailView( ExpertMenuDetailView(
@@ -0,0 +1,118 @@
import SwiftUI
import WebKit
/// Static/stable anchor ids matching the `<a id="...">` markers hand-placed in `Manual.md`
/// (tab-/step-anchors) and auto-generated by `build-manual.py`'s `schema_anchor()` (Experte
/// menus) the two sides are kept in sync by construction: this is the exact same string
/// transform as the Python function, not a generated file, so there's nothing to regenerate
/// when a schema's `menuPath` doesn't change.
enum ManualAnchor {
static let tabConnect = "tab-connect"
static let stepWAN = "step-wan"
static let stepLAN = "step-lan"
static let stepVLAN = "step-vlan"
static let stepWifi = "step-wifi"
static let stepFirewall = "step-firewall"
static let stepReview = "step-review"
static let tabOverview = "tab-overview"
static let tabDevices = "tab-devices"
static let tabExpert = "tab-expert"
static let tabBackup = "tab-backup"
static let tabSettings = "tab-settings"
/// Mirrors `build-manual.py`'s `schema_anchor(menu_path)` exactly.
static func schema(_ menuPath: String) -> String {
let trimmed = menuPath.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
return "schema-" + trimmed.replacingOccurrences(of: " ", with: "-")
}
}
/// Shared across the app (one instance, injected as an `@StateObject` in
/// `RouterOSAssistantApp`) so any "?"-help button anywhere can request the Handbuch window
/// jump to a specific section, whether or not that window is already open.
@MainActor
final class ManualNavigator: ObservableObject {
@Published var pendingAnchor: String?
func go(to anchor: String) {
// Reassigning the same value wouldn't trigger `didSet`/a fresh `onChange` in
// `ManualView` if the user clicks the same help button twice in a row nil the
// slot first so every request, including a repeat, actually re-scrolls.
pendingAnchor = nil
DispatchQueue.main.async { self.pendingAnchor = anchor }
}
}
private struct ManualWebView: NSViewRepresentable {
@ObservedObject var navigator: ManualNavigator
let language: String
/// One HTML file per language (`Manual.html` for "de", `Manual_en.html`, later
/// `Manual_es.html`, ...) `build-manual.py` names them the same way (`suffix = ""
/// if lang == "de" else f"_{lang}"`), so a new language needs no Swift change here,
/// only a new `Manual.<lang>.md` + an entry in `LANGUAGES` in that script.
final class Coordinator {
var loadedLanguage: String?
}
func makeCoordinator() -> Coordinator { Coordinator() }
func makeNSView(context: Context) -> WKWebView {
let webView = WKWebView()
load(language, into: webView, coordinator: context.coordinator)
return webView
}
func updateNSView(_ webView: WKWebView, context: Context) {
if context.coordinator.loadedLanguage != language {
load(language, into: webView, coordinator: context.coordinator)
}
guard let anchor = navigator.pendingAnchor else { return }
// The page is already loaded scrolling via JS instead of re-navigating to
// "Manual.html#anchor" avoids a full page reload/flash on every help-button click.
webView.evaluateJavaScript(
"document.getElementById(\(String(reflecting: anchor)))?.scrollIntoView({behavior: 'smooth', block: 'start'});"
)
}
private func load(_ language: String, into webView: WKWebView, coordinator: Coordinator) {
let resourceName = language == "de" ? "Manual" : "Manual_\(language)"
// Falls back to the German file if this language has no translated manual yet
// (e.g. a language the app UI supports but the manual hasn't been translated
// into) rather than showing a blank window.
guard let url = Bundle.main.url(forResource: resourceName, withExtension: "html")
?? Bundle.main.url(forResource: "Manual", withExtension: "html") else { return }
webView.loadFileURL(url, allowingReadAccessTo: url)
coordinator.loadedLanguage = language
}
}
struct ManualView: View {
@EnvironmentObject private var navigator: ManualNavigator
@AppStorage("appLanguage") private var appLanguage: String = "de"
var body: some View {
ManualWebView(navigator: navigator, language: appLanguage)
.frame(minWidth: 640, minHeight: 480)
}
}
/// Drop-in "?" toolbar button for any screen opens (or focuses, if already open) the
/// Handbuch window scrolled straight to `anchor`. One shared implementation so every tab/
/// wizard-step/Experte-schema screen wires help the same way.
struct ManualHelpButton: View {
let anchor: String
@AppStorage("appLanguage") private var appLanguage: String = "de"
@EnvironmentObject private var navigator: ManualNavigator
@Environment(\.openWindow) private var openWindow
var body: some View {
Button {
navigator.go(to: anchor)
openWindow(id: "manual")
} label: {
Image(systemName: "questionmark.circle")
}
.help(L10n.t("Hilfe zu diesem Bereich im Handbuch öffnen", appLanguage))
}
}
@@ -166,6 +166,7 @@ struct OverviewView: View {
} }
} }
.navigationTitle(LocalizedStringKey(L10n.t("Übersicht", appLanguage))) .navigationTitle(LocalizedStringKey(L10n.t("Übersicht", appLanguage)))
.toolbar { ToolbarItem { ManualHelpButton(anchor: ManualAnchor.tabOverview) } }
.toolbar { .toolbar {
ToolbarItemGroup { ToolbarItemGroup {
if viewModel.isLoading { if viewModel.isLoading {
@@ -107,6 +107,7 @@ struct ConnectView: View {
statusSection statusSection
} }
.formStyle(.grouped) .formStyle(.grouped)
.toolbar { ToolbarItem { ManualHelpButton(anchor: ManualAnchor.tabConnect) } }
.frame(minWidth: 320) .frame(minWidth: 320)
} detail: { } detail: {
deviceDetail deviceDetail
@@ -368,6 +369,7 @@ struct ConnectView: View {
} }
} }
.formStyle(.grouped) .formStyle(.grouped)
.toolbar { ToolbarItem { ManualHelpButton(anchor: ManualAnchor.tabConnect) } }
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
} else { } else {
ContentUnavailableView( ContentUnavailableView(
@@ -61,6 +61,7 @@ struct FirewallStepView: View {
} }
.formStyle(.grouped) .formStyle(.grouped)
.navigationTitle(LocalizedStringKey(L10n.t("Firewall (optional)", appLanguage))) .navigationTitle(LocalizedStringKey(L10n.t("Firewall (optional)", appLanguage)))
.toolbar { ToolbarItem { ManualHelpButton(anchor: ManualAnchor.stepFirewall) } }
.onAppear { .onAppear {
if viewModel.firewallSectionEnabled { if viewModel.firewallSectionEnabled {
viewModel.loadExistingFirewallRuleCounts() viewModel.loadExistingFirewallRuleCounts()
@@ -85,6 +85,7 @@ struct LanStepView: View {
} }
.formStyle(.grouped) .formStyle(.grouped)
.navigationTitle(LocalizedStringKey(L10n.t("Heimnetzwerk einrichten", appLanguage))) .navigationTitle(LocalizedStringKey(L10n.t("Heimnetzwerk einrichten", appLanguage)))
.toolbar { ToolbarItem { ManualHelpButton(anchor: ManualAnchor.stepLAN) } }
} }
private var isStepValid: Bool { private var isStepValid: Bool {
@@ -74,6 +74,7 @@ struct ReviewApplyView: View {
.formStyle(.grouped) .formStyle(.grouped)
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
.navigationTitle(LocalizedStringKey(L10n.t("Übersicht & Anwenden", appLanguage))) .navigationTitle(LocalizedStringKey(L10n.t("Übersicht & Anwenden", appLanguage)))
.toolbar { ToolbarItem { ManualHelpButton(anchor: ManualAnchor.stepReview) } }
.alert( .alert(
L10n.t("Anwenden fehlgeschlagen", appLanguage), L10n.t("Anwenden fehlgeschlagen", appLanguage),
isPresented: Binding( isPresented: Binding(
@@ -68,6 +68,7 @@ struct VlanStepView: View {
} }
.formStyle(.grouped) .formStyle(.grouped)
.navigationTitle(LocalizedStringKey(L10n.t("Zusätzliche Netzwerke (VLAN)", appLanguage))) .navigationTitle(LocalizedStringKey(L10n.t("Zusätzliche Netzwerke (VLAN)", appLanguage)))
.toolbar { ToolbarItem { ManualHelpButton(anchor: ManualAnchor.stepVLAN) } }
} }
/// Only gates on filled-in fields while the VLAN section is actually enabled with it off, /// Only gates on filled-in fields while the VLAN section is actually enabled with it off,
@@ -60,6 +60,7 @@ struct WanStepView: View {
} }
.formStyle(.grouped) .formStyle(.grouped)
.navigationTitle(LocalizedStringKey(L10n.t("Internet einrichten", appLanguage))) .navigationTitle(LocalizedStringKey(L10n.t("Internet einrichten", appLanguage)))
.toolbar { ToolbarItem { ManualHelpButton(anchor: ManualAnchor.stepWAN) } }
} }
private var isStepValid: Bool { private var isStepValid: Bool {
@@ -49,6 +49,7 @@ struct WifiStepView: View {
} }
.formStyle(.grouped) .formStyle(.grouped)
.navigationTitle(LocalizedStringKey(L10n.t("WLAN (falls vorhanden)", appLanguage))) .navigationTitle(LocalizedStringKey(L10n.t("WLAN (falls vorhanden)", appLanguage)))
.toolbar { ToolbarItem { ManualHelpButton(anchor: ManualAnchor.stepWifi) } }
} }
private var isStepValid: Bool { private var isStepValid: Bool {
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+218 -63
View File
@@ -19,12 +19,72 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parent ROOT = Path(__file__).resolve().parent
SCHEMA_SWIFT = ROOT / "RouterOSAssistant/Core/Models/RouterOSSchemaCatalog.swift" SCHEMA_SWIFT = ROOT / "RouterOSAssistant/Core/Models/RouterOSSchemaCatalog.swift"
MANUAL_MD = ROOT / "Manual.md" L10N_SWIFT = ROOT / "RouterOSAssistant/Core/Localization/L10n.swift"
MANUAL_PDF = ROOT / "Manual.pdf"
ASSETS = ROOT / "Manual-assets" ASSETS = ROOT / "Manual-assets"
DIAGRAMS = ASSETS / "diagrams" DIAGRAMS = ASSETS / "diagrams"
CHROME = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" CHROME = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
# One entry per manual language. "de" has no translation dict (it *is* the source
# language everything else is written against); every other language reuses the same
# DE->lang dictionary the app itself ships in L10n.swift for its own UI — Chapter 5
# (Expert reference) is generated per language straight from RouterOSSchemaCatalog.swift's
# German strings run through that dictionary, so a new language needs nothing here beyond
# adding its md_source and appending translations to L10n.swift; no new Python code.
LANGUAGES = {
"de": {"md_source": ROOT / "Manual.md", "pdf": ROOT / "Manual.pdf"},
"en": {"md_source": ROOT / "Manual.en.md", "pdf": ROOT / "Manual_en.pdf"},
}
def load_l10n_translations():
"""Parses `private static let translations: [String: String] = [...]` out of
L10n.swift the exact same DE->EN dictionary the running app uses for its own UI
strings so Chapter 5's field labels/help texts translate identically to what the
app itself shows, instead of a second, separately-maintained translation."""
src = L10N_SWIFT.read_text(encoding="utf-8")
marker = "translations: [String: String] = ["
start = src.index(marker) + len(marker)
end = find_matching_bracket(src, start - 1)
body = src[start:end]
translations = {}
i = 0
while i < len(body):
if body[i] == '"':
key, i = read_string_literal(body, i)
j = body.index(":", i) + 1
while body[j] in " \t\n":
j += 1
value, i = read_string_literal(body, j)
translations[key] = value
else:
i += 1
return translations
def find_matching_bracket(s, start):
depth, i, in_string, escape = 0, start, False, False
while i < len(s):
c = s[i]
if in_string:
if escape:
escape = False
elif c == "\\":
escape = True
elif c == '"':
in_string = False
else:
if c == '"':
in_string = True
elif c == "[":
depth += 1
elif c == "]":
depth -= 1
if depth == 0:
return i
i += 1
raise ValueError("unbalanced brackets starting at %d" % start)
CATEGORY_NAMES = { CATEGORY_NAMES = {
"firewallFilter": "Firewall: Filter-Regeln", "firewallFilter": "Firewall: Filter-Regeln",
"firewallNat": "Firewall: NAT (Portweiterleitung etc.)", "firewallNat": "Firewall: NAT (Portweiterleitung etc.)",
@@ -268,20 +328,56 @@ def parse_schema(src):
return ordered return ordered
def kind_label(kind): STATIC_LABELS = {
t = kind.get("type") "de": {
if t in ("text", "bool", "int", "duration", "interfacePick"): "singleton_note": " *(Einstellungsmenü — genau ein Eintrag, kein Anlegen/Löschen)*",
return { "menu_line": "RouterOS-Menü: `{menu}` · REST-Pfad: `{rest}`\n",
"text": "Text", "warning": "> ⚠️ **Achtung:** {text}\n",
"bool": "Ja/Nein", "generic_note": (
"int": "Zahl", "*Noch kein kuratiertes Formular — alle Felder erscheinen als freie "
"Schlüssel/Wert-Paare (siehe „Eigener Menüpfad“).*\n"
),
"table_header": "| Feld | RouterOS-Parameter | Typ | Pflicht | Standard | Hilfetext |",
"yes": "Ja", "no": "Nein",
"no_fields": "*Keine kuratierten Felder — generischer Schlüssel/Wert-Zugriff.*\n",
"kind": {
"text": "Text", "bool": "Ja/Nein", "int": "Zahl",
"duration": "Zeitdauer (Tage/Std/Min/Sek, per Stepper)", "duration": "Zeitdauer (Tage/Std/Min/Sek, per Stepper)",
"interfacePick": "Auswahl aus Live-Interface-Liste des Routers", "interfacePick": "Auswahl aus Live-Interface-Liste des Routers",
}[t] "enumPick": "Auswahl (fest): ",
"menuItemPick": "Verweis auf bestehenden Eintrag unter `{menu}`",
},
},
"en": {
"singleton_note": " *(settings menu — exactly one entry, no add/remove)*",
"menu_line": "RouterOS menu: `{menu}` · REST path: `{rest}`\n",
"warning": "> ⚠️ **Warning:** {text}\n",
"generic_note": (
"*No curated form yet — every field appears as a free-form "
"key/value pair (see \"Custom Menu Path\").*\n"
),
"table_header": "| Field | RouterOS Parameter | Type | Required | Default | Help Text |",
"yes": "Yes", "no": "No",
"no_fields": "*No curated fields — generic key/value access.*\n",
"kind": {
"text": "Text", "bool": "Yes/No", "int": "Number",
"duration": "Time duration (days/hrs/min/sec, via stepper)",
"interfacePick": "Picker from the router's live interface list",
"enumPick": "Fixed choice: ",
"menuItemPick": "Reference to an existing entry under `{menu}`",
},
},
}
def kind_label(kind, labels):
t = kind.get("type")
if t in labels["kind"] and t != "enumPick" and t != "menuItemPick":
return labels["kind"][t]
if t == "enumPick": if t == "enumPick":
return "Auswahl (fest): " + ", ".join(f"`{o}`" for o in kind.get("options", [])) return labels["kind"]["enumPick"] + ", ".join(f"`{o}`" for o in kind.get("options", []))
if t == "menuItemPick": if t == "menuItemPick":
return f"Verweis auf bestehenden Eintrag unter `{kind.get('menuPath')}`" return labels["kind"]["menuItemPick"].format(menu=kind.get("menuPath"))
return "" return ""
@@ -289,7 +385,19 @@ def esc(s):
return "" if s is None else s.replace("|", "\\|").replace("\n", " ") return "" if s is None else s.replace("|", "\\|").replace("\n", " ")
def render_expert_reference(menus): def schema_anchor(menu_path):
"""Deterministic HTML anchor id for one Experte-menu's Manual heading, from its
RouterOS menu path the same transform is duplicated in Swift (ManualAnchors.swift)
so the app can compute a schema's anchor from `RouterOSMenuSchema.menuPath` alone,
with no generated mapping file to keep in sync."""
return "schema-" + menu_path.strip("/").replace(" ", "-")
def render_expert_reference(menus, lang, translate):
"""`translate(s)` maps a German source string (displayName/summary/explanation/
warning/field label/help/category name) to this language's text — identity for
"de", the app's own L10n.swift dictionary for every other language."""
labels = STATIC_LABELS[lang]
by_cat = {} by_cat = {}
for m in menus: for m in menus:
by_cat.setdefault(m["category"], []).append(m) by_cat.setdefault(m["category"], []).append(m)
@@ -298,50 +406,49 @@ def render_expert_reference(menus):
for cat in CATEGORY_ORDER: for cat in CATEGORY_ORDER:
if cat not in by_cat: if cat not in by_cat:
continue continue
lines.append(f"### {cat}\n") lines.append(f"### {translate(cat)}\n")
for m in by_cat[cat]: for m in by_cat[cat]:
note = " *(Einstellungsmenü — genau ein Eintrag, kein Anlegen/Löschen)*" if m["isSingleton"] else "" note = labels["singleton_note"] if m["isSingleton"] else ""
lines.append(f"#### {m['displayName']}{note}") lines.append(f'<a id="{schema_anchor(m["menuPath"])}"></a>')
lines.append(f"RouterOS-Menü: `{m['menuPath']}` · REST-Pfad: `{m['restPath']}`\n") lines.append(f"#### {translate(m['displayName'])}{note}")
lines.append(labels["menu_line"].format(menu=m["menuPath"], rest=m["restPath"]))
if m["summary"]: if m["summary"]:
lines.append(f"{m['summary']}\n") lines.append(f"{translate(m['summary'])}\n")
if m["explanation"] and m["explanation"] != m["summary"]: if m["explanation"] and m["explanation"] != m["summary"]:
lines.append(f"{m['explanation']}\n") lines.append(f"{translate(m['explanation'])}\n")
if m.get("warning"): if m.get("warning"):
lines.append(f"> ⚠️ **Achtung:** {m['warning']}\n") lines.append(labels["warning"].format(text=translate(m["warning"])))
if m["generic"]: if m["generic"]:
lines.append( lines.append(labels["generic_note"])
"*Noch kein kuratiertes Formular — alle Felder erscheinen als freie "
"Schlüssel/Wert-Paare (siehe „Eigener Menüpfad“).*\n"
)
elif m["fields"]: elif m["fields"]:
lines.append("| Feld | RouterOS-Parameter | Typ | Pflicht | Standard | Hilfetext |") lines.append(labels["table_header"])
lines.append("|---|---|---|---|---|---|") lines.append("|---|---|---|---|---|---|")
for f in m["fields"]: for f in m["fields"]:
default = f"`{esc(f['defaultValue'])}`" if f["defaultValue"] else "" default = f"`{esc(f['defaultValue'])}`" if f["defaultValue"] else ""
required = labels["yes"] if f["required"] else labels["no"]
lines.append( lines.append(
f"| {esc(f['label'])} | `{esc(f['key'])}` | {kind_label(f['kind'])} " f"| {esc(translate(f['label']))} | `{esc(f['key'])}` | {kind_label(f['kind'], labels)} "
f"| {'Ja' if f['required'] else 'Nein'} | {default} | {esc(f['help'])} |" f"| {required} | {default} | {esc(translate(f['help']))} |"
) )
lines.append("") lines.append("")
else: else:
lines.append("*Keine kuratierten Felder — generischer Schlüssel/Wert-Zugriff.*\n") lines.append(labels["no_fields"])
lines.append("") lines.append("")
return "\n".join(lines).strip() return "\n".join(lines).strip()
def update_expert_reference_section(): def update_expert_reference_section(lang, md_path, translate):
src = SCHEMA_SWIFT.read_text(encoding="utf-8") src = SCHEMA_SWIFT.read_text(encoding="utf-8")
menus = parse_schema(src) menus = parse_schema(src)
ref_md = render_expert_reference(menus) ref_md = render_expert_reference(menus, lang, translate)
manual = MANUAL_MD.read_text(encoding="utf-8") manual = md_path.read_text(encoding="utf-8")
start_marker, end_marker = "<!-- EXPERT_REFERENCE_START -->", "<!-- EXPERT_REFERENCE_END -->" start_marker, end_marker = "<!-- EXPERT_REFERENCE_START -->", "<!-- EXPERT_REFERENCE_END -->"
start = manual.index(start_marker) + len(start_marker) start = manual.index(start_marker) + len(start_marker)
end = manual.index(end_marker) end = manual.index(end_marker)
manual = manual[:start] + "\n\n" + ref_md + "\n\n" + manual[end:] manual = manual[:start] + "\n\n" + ref_md + "\n\n" + manual[end:]
MANUAL_MD.write_text(manual, encoding="utf-8") md_path.write_text(manual, encoding="utf-8")
print(f"Expert reference updated: {len(menus)} menus, {sum(len(m['fields']) for m in menus)} fields") print(f"[{lang}] Expert reference updated: {len(menus)} menus, {sum(len(m['fields']) for m in menus)} fields")
# --- Diagram rendering --- # --- Diagram rendering ---
@@ -363,57 +470,105 @@ def render_diagrams():
# --- Markdown -> HTML -> PDF --- # --- Markdown -> HTML -> PDF ---
def render_pdf(): STYLE = """
body { font-family: -apple-system, "Helvetica Neue", Arial, sans-serif; font-size: 10.5pt; line-height: 1.5; color: #1a1a1a; }
h1 { font-size: 20pt; border-bottom: 2px solid #2f6fb0; padding-bottom: 6px; }
h2 { font-size: 15pt; color: #2f6fb0; margin-top: 28px; border-bottom: 1px solid #ccc; padding-bottom: 3px; }
h3 { font-size: 12.5pt; color: #1a4a75; margin-top: 20px; }
h4 { font-size: 11pt; margin-top: 14px; margin-bottom: 4px; }
code { background: #f2f2f2; padding: 1px 4px; border-radius: 3px; font-size: 92%; }
table { border-collapse: collapse; width: 100%; margin: 8px 0 16px 0; font-size: 9pt; }
th, td { border: 1px solid #ccc; padding: 4px 6px; text-align: left; vertical-align: top; }
th { background: #eef4fb; }
tr { page-break-inside: avoid; }
blockquote { border-left: 4px solid #d9a441; background: #fff8ea; margin: 10px 0; padding: 6px 12px; }
img { max-width: 90%; display: block; margin: 14px auto; }
a { color: #2f6fb0; }
hr { border: none; border-top: 1px solid #ddd; margin: 24px 0; }
"""
def render_body_html(md_path):
from markdown_it import MarkdownIt from markdown_it import MarkdownIt
md_text = MANUAL_MD.read_text(encoding="utf-8") md_text = md_path.read_text(encoding="utf-8")
md = MarkdownIt("gfm-like").enable("table") # html=True: lets the `<a id="...">` anchors inserted before each heading (see
body_html = md.render(md_text) # schema_anchor() and Manual.md's hand-placed tab-/step-anchors) pass through as
# real DOM ids instead of being escaped as literal text — CommonMark disables raw
# HTML by default, this is the one option that matters for that here.
md = MarkdownIt("gfm-like", {"html": True}).enable("table")
return md.render(md_text)
def fix_img(m):
def fix_img_srcs(body_html, embed_as_data_uri):
import base64
import mimetypes
def fix(m):
prefix, path, suffix = m.group(1), m.group(2), m.group(3) prefix, path, suffix = m.group(1), m.group(2), m.group(3)
if path.startswith(("http://", "https://", "file://", "/")): if path.startswith(("http://", "https://", "file://", "data:", "/")):
return m.group(0) return m.group(0)
return f'{prefix}file://{(ROOT / path).resolve()}{suffix}' full = (ROOT / path).resolve()
if embed_as_data_uri:
mime = mimetypes.guess_type(str(full))[0] or "image/png"
data = base64.b64encode(full.read_bytes()).decode("ascii")
return f'{prefix}data:{mime};base64,{data}{suffix}'
return f'{prefix}file://{full}{suffix}'
body_html = re.sub(r'(<img[^>]*src=")([^"]+)("[^>]*>)', fix_img, body_html) return re.sub(r'(<img[^>]*src=")([^"]+)("[^>]*>)', fix, body_html)
html = f"""<!DOCTYPE html><html lang="de"><head><meta charset="utf-8">
<title>RouterOS Assistant Bedienungsanleitung</title> def render_pdf(lang, md_path, pdf_path):
body_html = fix_img_srcs(render_body_html(md_path), embed_as_data_uri=False)
html = f"""<!DOCTYPE html><html lang="{lang}"><head><meta charset="utf-8">
<title>RouterOS Assistant Manual</title>
<style> <style>
@page {{ size: A4; margin: 18mm 16mm; }} @page {{ size: A4; margin: 18mm 16mm; }}
body {{ font-family: -apple-system, "Helvetica Neue", Arial, sans-serif; font-size: 10.5pt; line-height: 1.5; color: #1a1a1a; }} {STYLE}</style></head><body>{body_html}</body></html>"""
h1 {{ font-size: 20pt; border-bottom: 2px solid #2f6fb0; padding-bottom: 6px; }}
h2 {{ font-size: 15pt; color: #2f6fb0; margin-top: 28px; border-bottom: 1px solid #ccc; padding-bottom: 3px; }}
h3 {{ font-size: 12.5pt; color: #1a4a75; margin-top: 20px; }}
h4 {{ font-size: 11pt; margin-top: 14px; margin-bottom: 4px; }}
code {{ background: #f2f2f2; padding: 1px 4px; border-radius: 3px; font-size: 92%; }}
table {{ border-collapse: collapse; width: 100%; margin: 8px 0 16px 0; font-size: 9pt; }}
th, td {{ border: 1px solid #ccc; padding: 4px 6px; text-align: left; vertical-align: top; }}
th {{ background: #eef4fb; }}
tr {{ page-break-inside: avoid; }}
blockquote {{ border-left: 4px solid #d9a441; background: #fff8ea; margin: 10px 0; padding: 6px 12px; }}
img {{ max-width: 90%; display: block; margin: 14px auto; }}
a {{ color: #2f6fb0; }}
hr {{ border: none; border-top: 1px solid #ddd; margin: 24px 0; }}
</style></head><body>{body_html}</body></html>"""
html_path = ASSETS / "_manual_build.html" html_path = ASSETS / f"_manual_build_{lang}.html"
html_path.write_text(html, encoding="utf-8") html_path.write_text(html, encoding="utf-8")
subprocess.run( subprocess.run(
[CHROME, "--headless", "--disable-gpu", "--no-pdf-header-footer", [CHROME, "--headless", "--disable-gpu", "--no-pdf-header-footer",
f"--print-to-pdf={MANUAL_PDF}", f"file://{html_path}"], f"--print-to-pdf={pdf_path}", f"file://{html_path}"],
check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
) )
html_path.unlink() html_path.unlink()
print(f"PDF written: {MANUAL_PDF} ({MANUAL_PDF.stat().st_size // 1024} KB)") print(f"[{lang}] PDF written: {pdf_path} ({pdf_path.stat().st_size // 1024} KB)")
def render_app_html(lang, md_path):
"""Self-contained Manual[_lang].html (images embedded as base64 data URIs, no
external file references) bundled straight into the app as a resource see
ManualView.swift. Self-contained because once inside the .app bundle there's no
Manual-assets/ directory next to it to resolve relative image paths against."""
body_html = fix_img_srcs(render_body_html(md_path), embed_as_data_uri=True)
html = f"""<!DOCTYPE html><html lang="{lang}"><head><meta charset="utf-8">
<title>RouterOS Assistant Manual</title>
<style>
body {{ margin: 0; padding: 24px 32px; }}
{STYLE}</style></head><body>{body_html}</body></html>"""
suffix = "" if lang == "de" else f"_{lang}"
out = ROOT / f"RouterOSAssistant/Resources/Manual{suffix}.html"
out.write_text(html, encoding="utf-8")
print(f"[{lang}] App HTML written: {out} ({out.stat().st_size // 1024} KB)")
def main(): def main():
update_expert_reference_section() translations = load_l10n_translations()
def translate_en(german):
return translations.get(german, german)
translators = {"de": lambda s: s, "en": translate_en}
render_diagrams() render_diagrams()
render_pdf() for lang, cfg in LANGUAGES.items():
update_expert_reference_section(lang, cfg["md_source"], translators[lang])
render_pdf(lang, cfg["md_source"], cfg["pdf"])
render_app_html(lang, cfg["md_source"])
if __name__ == "__main__": if __name__ == "__main__":
+28 -1
View File
@@ -167,7 +167,29 @@ Ursache: `focusPanel(subGraph:subLayout:)` in `OverviewView.swift` rendert Kante
Build+alle 99 Unit-Tests grün, live bestätigt. Build+alle 99 Unit-Tests grün, live bestätigt.
### 7. Manual direkt in die App integrieren ### 7. Manual direkt in die App integrieren
**Status:** offen **Status:** fixed (live bestätigt)
Wunsch: Manual so in die App integrieren, dass ein Hilfepunkt in der aktuell geöffneten Sektion direkt zur passenden Stelle im Manual springt (Textanker).
Rückfrage vorab geklärt (AskUserQuestion): eingebettetes HTML (WebView) statt PDF/PDFKit; eigenes Hilfe-Fenster (wie Settings ⌘,) statt eigener Tab; Sprungmarken pro Wizard-Schritt + pro Experte-Schema, nicht nur pro Haupt-Tab.
Umsetzung:
- **`build-manual.py`**: `<a id="schema-...">`-Anker automatisch vor jede Experte-Menü-Überschrift eingefügt (`schema_anchor(menuPath)`, deterministisch aus dem RouterOS-Menüpfad). `Manual.md` bekam zusätzlich 12 handgesetzte `<a id="tab-..."/"step-...">`-Anker vor den 6 Haupt-Tab- und 6 Wizard-Schritt-Überschriften. Neue Funktion `render_app_html()` erzeugt ein eigenständiges `RouterOSAssistant/Resources/Manual.html` (Bilder als Base64 eingebettet, kein `Manual-assets/`-Ordner nötig im App-Bundle) — MarkdownIt jetzt mit `html=True`, sonst wären die `<a id>`-Tags als Text escaped statt als echte DOM-IDs zu rendern.
- **`ManualView.swift`** (neu, `Features/Manual/`): `ManualAnchor`-Enum mit den 12 statischen Ankern + `schema(menuPath:)` (spiegelt `schema_anchor()` exakt — kein generiertes Mapping nötig, beide Seiten wenden dieselbe simple Transformation an). `ManualNavigator` (ObservableObject, app-weit als `@StateObject` in `RouterOSAssistantApp`) hält den angeforderten Anker. `ManualWebView` (WKWebView via `NSViewRepresentable`) lädt das gebündelte `Manual.html` einmalig, springt bei Anker-Änderung per JavaScript `scrollIntoView` (kein Reload/Flackern). Neues `Window("Handbuch", id: "manual")` in `RouterOSAssistantApp.swift`.
- **`ManualHelpButton`**: wiederverwendbarer "?"-Toolbar-Button, öffnet/fokussiert das Handbuch-Fenster und springt zum übergebenen Anker. Eingebaut in: alle 6 Haupt-Tabs (Verbinden, Übersicht, LAN-Scanner, Experte, Sicherungen — je Toolbar), alle 6 Wizard-Schritte (WAN/LAN/VLAN/WLAN/Firewall/Review), und `ExpertMenuDetailView` bekommt pro geöffnetem Schema automatisch den passenden Anker (`ManualAnchor.schema(schema.menuPath)`) — alle 45 Experte-Menüs sind damit einzeln verlinkt, nicht nur der Experte-Tab pauschal.
Bewusst nicht umgesetzt: Settings-Fenster (⌘,) hat noch keinen Hilfe-Button — macOS-Settings-Fenster haben konventionell keine Toolbar, ein Button dort hätte nicht ins native Bild gepasst; bei Bedarf nachrüstbar (Kapitel 7 im Manual existiert bereits, Anker `tab-settings` ist gesetzt).
Build grün, alle 99 Unit-Tests grün, `Manual.html` bestätigt im App-Bundle (`Contents/Resources/Manual.html`). Noch nicht live geprüft — bitte: Hilfe-Buttons in mehreren Tabs/Wizard-Schritten/Experte-Menüs anklicken, prüfen ob das Handbuch-Fenster öffnet und zur richtigen Stelle springt.
Nachbesserung 1 (User-Feedback: "das manual schaltet aber nicht die Sprache in englisch um, denk dran weitere Sprachen folgen"): Rückfrage geklärt — komplette Handübersetzung aller Kapitel (nicht nur Kapitel 5 automatisch), Mechanismus generisch für beliebig viele Sprachen statt hart DE/EN.
Umsetzung:
- **`Manual.en.md`** (neu): vollständige Handübersetzung aller Fließtext-Kapitel (0,1,2,3,4,6,7) — dieselbe Struktur/Anker wie `Manual.md`. Kapitel 8 "English summary" (bisheriger Behelf) aus `Manual.md` entfernt, da jetzt redundant.
- **`build-manual.py`** generalisiert: neues `LANGUAGES`-Dict (`{"de": Manual.md, "en": Manual.en.md}`, für weitere Sprachen nur ein neuer Eintrag + `Manual.<code>.md` nötig, kein neuer Python-Code). Kapitel 5 (Experte-Referenz, automatisch generiert) wird jetzt pro Sprache übersetzt, indem `L10n.swift`s eigenes DE→EN-Übersetzungs-Dictionary der App selbst wiederverwendet wird (per Regex aus dem Swift-Quelltext geparst, 714 Einträge) — Feldlabels/Hilfetexte im Manual stimmen dadurch exakt mit dem überein, was die App in Englisch zeigt, keine zweite, separat gepflegte Übersetzung. Bug beim ersten Anlauf: Klammersuche fand die falsche `[` (die des Typannotation `[String: String]`, nicht die des Array-Literals) — 0 Übersetzungen geladen, dadurch blieb Kapitel 5 komplett Deutsch. Gefixt, Gegenprobe: 714 Einträge geladen.
- **`ManualView.swift`**: `ManualWebView` lädt jetzt `Manual.html` (de) oder `Manual_<sprache>.html` (alles andere), abhängig von `@AppStorage("appLanguage")`, mit Fallback auf Deutsch falls eine Sprache (noch) keine Übersetzung hat. Lädt bei Sprachwechsel neu (`Coordinator` merkt sich die zuletzt geladene Sprache), springt danach weiterhin zum aktuellen Anker.
Build grün, alle 99 Unit-Tests grün, `Manual.html` + `Manual_en.html` beide bestätigt im App-Bundle. Bitte nochmal live testen: Sprache auf Englisch umschalten, Hilfe-Button klicken, prüfen ob Handbuch-Fenster jetzt englisch anzeigt.
### 8. Abwechselnde Farbkombis bei Tabellenansichten ### 8. Abwechselnde Farbkombis bei Tabellenansichten
**Status:** fixed (live bestätigt: "passt") **Status:** fixed (live bestätigt: "passt")
@@ -187,3 +209,8 @@ Build grün, alle 99 Unit-Tests grün. Achtung beim Live-Test: kann auch den "Ka
Nachbesserung 2 (User-Feedback: auch das "Weitere Parameter"-Grid im Experte-Bearbeiten-Sheet soll Zeilen abwechselnd einfärben — Zeile 1,3,5... —, "bitte für das komplette Projekt umsetzen"): Grid-Zeilen (2 Parameter pro sichtbarer Zeile) bekommen jetzt `.background(TableZebra.color(for: rowIndex))` pro `GridRow`. Zusätzlich auf jede weitere echte Tabellen-/Listenansicht im Projekt ausgeweitet: "Bekannte Router"-Liste + Interface-Liste (Verbinden-Tab), Sicherungsliste (Sicherungen-Tab), Rohfelder-Sheet + Port-Scan-Ergebnisliste (LAN-Scanner), geplante Änderungen + Ablauf-Log (Einrichten-Wizard Review/Apply), Feld-Details + Verbindungsliste im Übersicht-Knoten-Detailpanel. Nicht angefasst: Experte-Sidebar (Navigationsliste, keine Datentabelle) und die Setup-Wizard-Konfigurationsformulare (VLAN/WLAN/LAN/WAN — je Zeile ein Mehrfeld-Unterformular, keine gleichförmigen Datenzeilen, Zebra-Streifen würden dort eher verwirren). Nachbesserung 2 (User-Feedback: auch das "Weitere Parameter"-Grid im Experte-Bearbeiten-Sheet soll Zeilen abwechselnd einfärben — Zeile 1,3,5... —, "bitte für das komplette Projekt umsetzen"): Grid-Zeilen (2 Parameter pro sichtbarer Zeile) bekommen jetzt `.background(TableZebra.color(for: rowIndex))` pro `GridRow`. Zusätzlich auf jede weitere echte Tabellen-/Listenansicht im Projekt ausgeweitet: "Bekannte Router"-Liste + Interface-Liste (Verbinden-Tab), Sicherungsliste (Sicherungen-Tab), Rohfelder-Sheet + Port-Scan-Ergebnisliste (LAN-Scanner), geplante Änderungen + Ablauf-Log (Einrichten-Wizard Review/Apply), Feld-Details + Verbindungsliste im Übersicht-Knoten-Detailpanel. Nicht angefasst: Experte-Sidebar (Navigationsliste, keine Datentabelle) und die Setup-Wizard-Konfigurationsformulare (VLAN/WLAN/LAN/WAN — je Zeile ein Mehrfeld-Unterformular, keine gleichförmigen Datenzeilen, Zebra-Streifen würden dort eher verwirren).
Build grün, alle 99 Unit-Tests grün. Bitte erneut live testen. Build grün, alle 99 Unit-Tests grün. Bitte erneut live testen.
### 10. Selbständiger Wiederverbindungsversuch nach Disconnect
**Status:** offen
Wunsch: fällt die Verbindung zum Router weg (z.B. während einer laufenden Sitzung), soll die App selbständig versuchen, die Verbindung wiederherzustellen, statt einfach im getrennten Zustand zu bleiben.