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:
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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:
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
<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
|
||||||
|
|
||||||

|

|
||||||
@@ -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** (100–300pt): Länge bzw. Breite der kleinen
|
**Sparkline-Breite** (100–300pt): 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
Binary file not shown.
Binary file not shown.
@@ -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
@@ -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__":
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user