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.
|
||||
/Backups/
|
||||
*.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)
|
||||
6. [Sicherungen](#6-sicherungen)
|
||||
7. [Einstellungen](#7-einstellungen)
|
||||
8. [English summary](#8-english-summary)
|
||||
|
||||
---
|
||||
|
||||
@@ -62,6 +61,7 @@ bleiben unübersetzt.
|
||||
|
||||
---
|
||||
|
||||
<a id="tab-connect"></a>
|
||||
## 1. Verbinden
|
||||
|
||||
### Verbindungsaufbau
|
||||
@@ -145,6 +145,7 @@ Modus-Schalter zu Beginn wählt zwischen:
|
||||
|
||||

|
||||
|
||||
<a id="step-wan"></a>
|
||||
### WAN (Internetanschluss)
|
||||
|
||||
| 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-Passwort | „Das zum Benutzernamen gehörende Passwort von deinem Internetanbieter.“ |
|
||||
|
||||
<a id="step-lan"></a>
|
||||
### LAN (ein oder mehrere Netzwerke)
|
||||
|
||||
| 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
|
||||
oben ein anderer Port gewählt wird.
|
||||
|
||||
<a id="step-vlan"></a>
|
||||
### VLAN (optional)
|
||||
|
||||
„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.“ |
|
||||
| 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)
|
||||
|
||||
„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.“ |
|
||||
| Passwort | „Das WLAN-Passwort (WPA2). Muss mindestens 8 Zeichen lang sein.“ |
|
||||
|
||||
<a id="step-firewall"></a>
|
||||
### Firewall-Grundschutz
|
||||
|
||||
„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
|
||||
print‘.“
|
||||
|
||||
<a id="step-review"></a>
|
||||
### Review / Apply
|
||||
|
||||
„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
|
||||
|
||||
Grafisches Diagramm des kompletten aktuellen Router-Zustands (IST-Zustand)
|
||||
@@ -322,6 +329,7 @@ selbst verwaltet.
|
||||
|
||||
---
|
||||
|
||||
<a id="tab-devices"></a>
|
||||
## 4. LAN-Scanner
|
||||
|
||||
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
|
||||
|
||||
Direkter, kuratierter Zugriff auf die meisten RouterOS-Bereiche. Für
|
||||
@@ -398,6 +407,7 @@ aktuell am Router vorhandenen Interfaces.
|
||||
|
||||
### System
|
||||
|
||||
<a id="schema-system-identity"></a>
|
||||
#### Router-Name *(Einstellungsmenü — genau ein Eintrag, kein Anlegen/Löschen)*
|
||||
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. |
|
||||
|
||||
<a id="schema-system-clock"></a>
|
||||
#### Uhrzeit/Zeitzone *(Einstellungsmenü — genau ein Eintrag, kein Anlegen/Löschen)*
|
||||
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. |
|
||||
|
||||
<a id="schema-system-routerboard-mode-button"></a>
|
||||
#### Mode-Taste *(Einstellungsmenü — genau ein Eintrag, kein Anlegen/Löschen)*
|
||||
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. |
|
||||
| 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)*
|
||||
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. |
|
||||
| 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
|
||||
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. |
|
||||
| Kommentar | `comment` | Text | Nein | — | Nur zur eigenen Wiedererkennung. |
|
||||
|
||||
<a id="schema-system-scheduler"></a>
|
||||
#### Zeitplaner
|
||||
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. |
|
||||
| Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | Zeitplan inaktiv schalten, ohne ihn zu löschen. |
|
||||
|
||||
<a id="schema-system-script"></a>
|
||||
#### Skripte
|
||||
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. |
|
||||
| Skript-Inhalt | `source` | Text | Nein | — | RouterOS-Befehle, z.B. ":log info \"Test\"". |
|
||||
|
||||
<a id="schema-user"></a>
|
||||
#### Benutzerkonten
|
||||
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. |
|
||||
| Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | Konto inaktiv schalten, ohne es zu löschen. |
|
||||
|
||||
<a id="schema-system-logging"></a>
|
||||
#### Protokollierung
|
||||
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...)
|
||||
|
||||
<a id="schema-interface"></a>
|
||||
#### Alle Interfaces (generisch)
|
||||
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. |
|
||||
| Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | Interface inaktiv schalten, ohne es zu löschen. |
|
||||
|
||||
<a id="schema-interface-bridge"></a>
|
||||
#### 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. |
|
||||
| Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | Bridge inaktiv schalten, ohne sie zu löschen. |
|
||||
|
||||
<a id="schema-interface-bridge-port"></a>
|
||||
#### Bridge-Ports
|
||||
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. |
|
||||
| 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
|
||||
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. |
|
||||
| Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | VLAN-Interface inaktiv schalten, ohne es zu löschen. |
|
||||
|
||||
<a id="schema-interface-wireguard"></a>
|
||||
#### WireGuard-Interfaces
|
||||
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. |
|
||||
| Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | Interface inaktiv schalten, ohne es zu löschen. |
|
||||
|
||||
<a id="schema-interface-wireguard-peers"></a>
|
||||
#### 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. |
|
||||
| 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
|
||||
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. |
|
||||
| Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | Einwahl inaktiv schalten, ohne sie zu löschen. |
|
||||
|
||||
<a id="schema-interface-bonding"></a>
|
||||
#### 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
|
||||
|
||||
<a id="schema-ip-address"></a>
|
||||
#### IP-Adressen
|
||||
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. |
|
||||
| Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | Adresse inaktiv schalten, ohne sie zu löschen. |
|
||||
|
||||
<a id="schema-ip-pool"></a>
|
||||
#### Adress-Pools
|
||||
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. |
|
||||
| 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
|
||||
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. |
|
||||
| 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
|
||||
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. |
|
||||
| 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)
|
||||
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. |
|
||||
| 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)*
|
||||
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. |
|
||||
| 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
|
||||
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. |
|
||||
| Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | Dienst inaktiv schalten, ohne den Eintrag zu löschen. |
|
||||
|
||||
<a id="schema-ip-hotspot"></a>
|
||||
#### Hotspot
|
||||
RouterOS-Menü: `/ip hotspot` · REST-Pfad: `ip/hotspot`
|
||||
|
||||
@@ -726,6 +760,7 @@ Besteht aus mehreren zusammenhängenden Teilen (Server, Server-Profil, Benutzer-
|
||||
|
||||
### Routing
|
||||
|
||||
<a id="schema-ip-route"></a>
|
||||
#### Statische Routen
|
||||
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. |
|
||||
| Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | Route inaktiv schalten, ohne sie zu löschen. |
|
||||
|
||||
<a id="schema-routing-ospf-instance"></a>
|
||||
#### OSPF-Instanzen
|
||||
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“).*
|
||||
|
||||
<a id="schema-routing-bgp-connection"></a>
|
||||
#### BGP-Verbindungen
|
||||
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
|
||||
|
||||
<a id="schema-ppp-secret"></a>
|
||||
#### PPP-Benutzer
|
||||
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. |
|
||||
| Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | Benutzer inaktiv schalten, ohne ihn zu löschen. |
|
||||
|
||||
<a id="schema-ppp-profile"></a>
|
||||
#### 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. |
|
||||
| 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
|
||||
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“).*
|
||||
|
||||
<a id="schema-interface-ovpn-server-server"></a>
|
||||
#### OpenVPN-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
|
||||
|
||||
<a id="schema-interface-wireless"></a>
|
||||
#### WLAN (Legacy-Treiber)
|
||||
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. |
|
||||
| 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
|
||||
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). |
|
||||
| WPA2-Passwort | `wpa2-pre-shared-key` | Text | Nein | — | Mindestens 8 Zeichen. |
|
||||
|
||||
<a id="schema-interface-wifi"></a>
|
||||
#### WLAN (neuer wifiwave2/802.11ax-Treiber)
|
||||
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. |
|
||||
| Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | WLAN-Interface inaktiv schalten, ohne es zu löschen. |
|
||||
|
||||
<a id="schema-caps-man-manager"></a>
|
||||
#### CAPsMAN-Zentrale
|
||||
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
|
||||
|
||||
<a id="schema-ip-firewall-filter"></a>
|
||||
#### Filter-Regeln
|
||||
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.)
|
||||
|
||||
<a id="schema-ip-firewall-nat"></a>
|
||||
#### NAT-Regeln
|
||||
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)
|
||||
|
||||
<a id="schema-ip-firewall-mangle"></a>
|
||||
#### Mangle-Regeln
|
||||
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)
|
||||
|
||||
<a id="schema-ip-firewall-raw"></a>
|
||||
#### Raw-Regeln
|
||||
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
|
||||
|
||||
<a id="schema-ip-firewall-address-list"></a>
|
||||
#### Adress-Listen
|
||||
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
|
||||
|
||||
<a id="schema-queue-simple"></a>
|
||||
#### Einfache Bandbreiten-Begrenzung
|
||||
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. |
|
||||
| Deaktiviert | `disabled` | Ja/Nein | Nein | `no` | Begrenzung inaktiv schalten, ohne sie zu löschen. |
|
||||
|
||||
<a id="schema-queue-tree"></a>
|
||||
#### Queue-Baum
|
||||
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
|
||||
|
||||
<a id="schema-tool-netwatch"></a>
|
||||
#### 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". |
|
||||
| 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)*
|
||||
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
|
||||
|
||||

|
||||
@@ -1090,6 +1145,7 @@ Ein einzelner, geräteweiter Satz Einstellungen — kein Menü mit mehreren Eint
|
||||
|
||||
---
|
||||
|
||||
<a id="tab-settings"></a>
|
||||
## 7. 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-Breite** (100–300pt): Länge bzw. Breite der kleinen
|
||||
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 |
|
||||
| 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 |
|
||||
| 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
|
||||
Session-Verlauf: [`HANDOFF.md`](HANDOFF.md) / [`CHATLOG.md`](CHATLOG.md).
|
||||
|
||||
@@ -3,6 +3,7 @@ import SwiftUI
|
||||
@main
|
||||
struct RouterOSAssistantApp: App {
|
||||
@StateObject private var connectionService = ConnectionService()
|
||||
@StateObject private var manualNavigator = ManualNavigator()
|
||||
/// 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
|
||||
/// `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
|
||||
// an explicit font, that still wins.
|
||||
.environment(\.font, .system(size: 13 * ((AppTextSize(rawValue: textSizeRaw) ?? .standard).scale)))
|
||||
.environmentObject(manualNavigator)
|
||||
}
|
||||
.environment(\.locale, Locale(identifier: appLanguage))
|
||||
|
||||
Settings {
|
||||
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] = [
|
||||
"Verbinden": "Connect",
|
||||
"Handbuch": "Manual",
|
||||
"Hilfe zu diesem Bereich im Handbuch öffnen": "Open help for this area in the manual",
|
||||
"Einrichten": "Setup",
|
||||
"Übersicht": "Topology",
|
||||
"LAN-Scanner": "LAN Scanner",
|
||||
|
||||
@@ -212,6 +212,7 @@ struct BackupListView: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle(LocalizedStringKey(L10n.t("Sicherungen", appLanguage)))
|
||||
.toolbar { ToolbarItem { ManualHelpButton(anchor: ManualAnchor.tabBackup) } }
|
||||
.toolbar {
|
||||
ToolbarItem {
|
||||
Button {
|
||||
|
||||
@@ -120,6 +120,7 @@ struct DevicesView: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle(LocalizedStringKey(L10n.t("LAN-Scanner", appLanguage)))
|
||||
.toolbar { ToolbarItem { ManualHelpButton(anchor: ManualAnchor.tabDevices) } }
|
||||
.toolbar {
|
||||
ToolbarItem {
|
||||
Button {
|
||||
|
||||
@@ -79,6 +79,7 @@ struct ExpertMenuDetailView: View {
|
||||
// See DevicesView's identical fix: `.formStyle(.grouped)` paints an opaque background
|
||||
// over each Section's rows, hiding `.listRowBackground` (Zebra-Streifen) underneath it.
|
||||
.scrollContentBackground(.hidden)
|
||||
.toolbar { ToolbarItem { ManualHelpButton(anchor: ManualAnchor.schema(schema.menuPath)) } }
|
||||
.navigationTitle(LocalizedStringKey(L10n.t(schema.displayName, appLanguage)))
|
||||
.task(id: schema.id) { await viewModel.reloadItems() }
|
||||
.sheet(item: $viewModel.editingItem) { item in
|
||||
|
||||
@@ -84,6 +84,7 @@ struct ExpertView: View {
|
||||
}
|
||||
.listStyle(.sidebar)
|
||||
.navigationTitle(LocalizedStringKey(L10n.t("Experte", appLanguage)))
|
||||
.toolbar { ToolbarItem { ManualHelpButton(anchor: ManualAnchor.tabExpert) } }
|
||||
} detail: {
|
||||
if let schema = viewModel.selectedSchema {
|
||||
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)))
|
||||
.toolbar { ToolbarItem { ManualHelpButton(anchor: ManualAnchor.tabOverview) } }
|
||||
.toolbar {
|
||||
ToolbarItemGroup {
|
||||
if viewModel.isLoading {
|
||||
|
||||
@@ -107,6 +107,7 @@ struct ConnectView: View {
|
||||
statusSection
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
.toolbar { ToolbarItem { ManualHelpButton(anchor: ManualAnchor.tabConnect) } }
|
||||
.frame(minWidth: 320)
|
||||
} detail: {
|
||||
deviceDetail
|
||||
@@ -368,6 +369,7 @@ struct ConnectView: View {
|
||||
}
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
.toolbar { ToolbarItem { ManualHelpButton(anchor: ManualAnchor.tabConnect) } }
|
||||
.scrollContentBackground(.hidden)
|
||||
} else {
|
||||
ContentUnavailableView(
|
||||
|
||||
@@ -61,6 +61,7 @@ struct FirewallStepView: View {
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
.navigationTitle(LocalizedStringKey(L10n.t("Firewall (optional)", appLanguage)))
|
||||
.toolbar { ToolbarItem { ManualHelpButton(anchor: ManualAnchor.stepFirewall) } }
|
||||
.onAppear {
|
||||
if viewModel.firewallSectionEnabled {
|
||||
viewModel.loadExistingFirewallRuleCounts()
|
||||
|
||||
@@ -85,6 +85,7 @@ struct LanStepView: View {
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
.navigationTitle(LocalizedStringKey(L10n.t("Heimnetzwerk einrichten", appLanguage)))
|
||||
.toolbar { ToolbarItem { ManualHelpButton(anchor: ManualAnchor.stepLAN) } }
|
||||
}
|
||||
|
||||
private var isStepValid: Bool {
|
||||
|
||||
@@ -74,6 +74,7 @@ struct ReviewApplyView: View {
|
||||
.formStyle(.grouped)
|
||||
.scrollContentBackground(.hidden)
|
||||
.navigationTitle(LocalizedStringKey(L10n.t("Übersicht & Anwenden", appLanguage)))
|
||||
.toolbar { ToolbarItem { ManualHelpButton(anchor: ManualAnchor.stepReview) } }
|
||||
.alert(
|
||||
L10n.t("Anwenden fehlgeschlagen", appLanguage),
|
||||
isPresented: Binding(
|
||||
|
||||
@@ -68,6 +68,7 @@ struct VlanStepView: View {
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
.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,
|
||||
|
||||
@@ -60,6 +60,7 @@ struct WanStepView: View {
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
.navigationTitle(LocalizedStringKey(L10n.t("Internet einrichten", appLanguage)))
|
||||
.toolbar { ToolbarItem { ManualHelpButton(anchor: ManualAnchor.stepWAN) } }
|
||||
}
|
||||
|
||||
private var isStepValid: Bool {
|
||||
|
||||
@@ -49,6 +49,7 @@ struct WifiStepView: View {
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
.navigationTitle(LocalizedStringKey(L10n.t("WLAN (falls vorhanden)", appLanguage)))
|
||||
.toolbar { ToolbarItem { ManualHelpButton(anchor: ManualAnchor.stepWifi) } }
|
||||
}
|
||||
|
||||
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
|
||||
SCHEMA_SWIFT = ROOT / "RouterOSAssistant/Core/Models/RouterOSSchemaCatalog.swift"
|
||||
MANUAL_MD = ROOT / "Manual.md"
|
||||
MANUAL_PDF = ROOT / "Manual.pdf"
|
||||
L10N_SWIFT = ROOT / "RouterOSAssistant/Core/Localization/L10n.swift"
|
||||
ASSETS = ROOT / "Manual-assets"
|
||||
DIAGRAMS = ASSETS / "diagrams"
|
||||
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 = {
|
||||
"firewallFilter": "Firewall: Filter-Regeln",
|
||||
"firewallNat": "Firewall: NAT (Portweiterleitung etc.)",
|
||||
@@ -268,20 +328,56 @@ def parse_schema(src):
|
||||
return ordered
|
||||
|
||||
|
||||
def kind_label(kind):
|
||||
t = kind.get("type")
|
||||
if t in ("text", "bool", "int", "duration", "interfacePick"):
|
||||
return {
|
||||
"text": "Text",
|
||||
"bool": "Ja/Nein",
|
||||
"int": "Zahl",
|
||||
STATIC_LABELS = {
|
||||
"de": {
|
||||
"singleton_note": " *(Einstellungsmenü — genau ein Eintrag, kein Anlegen/Löschen)*",
|
||||
"menu_line": "RouterOS-Menü: `{menu}` · REST-Pfad: `{rest}`\n",
|
||||
"warning": "> ⚠️ **Achtung:** {text}\n",
|
||||
"generic_note": (
|
||||
"*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)",
|
||||
"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":
|
||||
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":
|
||||
return f"Verweis auf bestehenden Eintrag unter `{kind.get('menuPath')}`"
|
||||
return labels["kind"]["menuItemPick"].format(menu=kind.get("menuPath"))
|
||||
return "—"
|
||||
|
||||
|
||||
@@ -289,7 +385,19 @@ def esc(s):
|
||||
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 = {}
|
||||
for m in menus:
|
||||
by_cat.setdefault(m["category"], []).append(m)
|
||||
@@ -298,50 +406,49 @@ def render_expert_reference(menus):
|
||||
for cat in CATEGORY_ORDER:
|
||||
if cat not in by_cat:
|
||||
continue
|
||||
lines.append(f"### {cat}\n")
|
||||
lines.append(f"### {translate(cat)}\n")
|
||||
for m in by_cat[cat]:
|
||||
note = " *(Einstellungsmenü — genau ein Eintrag, kein Anlegen/Löschen)*" if m["isSingleton"] else ""
|
||||
lines.append(f"#### {m['displayName']}{note}")
|
||||
lines.append(f"RouterOS-Menü: `{m['menuPath']}` · REST-Pfad: `{m['restPath']}`\n")
|
||||
note = labels["singleton_note"] if m["isSingleton"] else ""
|
||||
lines.append(f'<a id="{schema_anchor(m["menuPath"])}"></a>')
|
||||
lines.append(f"#### {translate(m['displayName'])}{note}")
|
||||
lines.append(labels["menu_line"].format(menu=m["menuPath"], rest=m["restPath"]))
|
||||
if m["summary"]:
|
||||
lines.append(f"{m['summary']}\n")
|
||||
lines.append(f"{translate(m['summary'])}\n")
|
||||
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"):
|
||||
lines.append(f"> ⚠️ **Achtung:** {m['warning']}\n")
|
||||
lines.append(labels["warning"].format(text=translate(m["warning"])))
|
||||
if m["generic"]:
|
||||
lines.append(
|
||||
"*Noch kein kuratiertes Formular — alle Felder erscheinen als freie "
|
||||
"Schlüssel/Wert-Paare (siehe „Eigener Menüpfad“).*\n"
|
||||
)
|
||||
lines.append(labels["generic_note"])
|
||||
elif m["fields"]:
|
||||
lines.append("| Feld | RouterOS-Parameter | Typ | Pflicht | Standard | Hilfetext |")
|
||||
lines.append(labels["table_header"])
|
||||
lines.append("|---|---|---|---|---|---|")
|
||||
for f in m["fields"]:
|
||||
default = f"`{esc(f['defaultValue'])}`" if f["defaultValue"] else "—"
|
||||
required = labels["yes"] if f["required"] else labels["no"]
|
||||
lines.append(
|
||||
f"| {esc(f['label'])} | `{esc(f['key'])}` | {kind_label(f['kind'])} "
|
||||
f"| {'Ja' if f['required'] else 'Nein'} | {default} | {esc(f['help'])} |"
|
||||
f"| {esc(translate(f['label']))} | `{esc(f['key'])}` | {kind_label(f['kind'], labels)} "
|
||||
f"| {required} | {default} | {esc(translate(f['help']))} |"
|
||||
)
|
||||
lines.append("")
|
||||
else:
|
||||
lines.append("*Keine kuratierten Felder — generischer Schlüssel/Wert-Zugriff.*\n")
|
||||
lines.append(labels["no_fields"])
|
||||
lines.append("")
|
||||
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")
|
||||
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 = manual.index(start_marker) + len(start_marker)
|
||||
end = manual.index(end_marker)
|
||||
manual = manual[:start] + "\n\n" + ref_md + "\n\n" + manual[end:]
|
||||
MANUAL_MD.write_text(manual, encoding="utf-8")
|
||||
print(f"Expert reference updated: {len(menus)} menus, {sum(len(m['fields']) for m in menus)} fields")
|
||||
md_path.write_text(manual, encoding="utf-8")
|
||||
print(f"[{lang}] Expert reference updated: {len(menus)} menus, {sum(len(m['fields']) for m in menus)} fields")
|
||||
|
||||
|
||||
# --- Diagram rendering ---
|
||||
@@ -363,57 +470,105 @@ def render_diagrams():
|
||||
# --- 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
|
||||
|
||||
md_text = MANUAL_MD.read_text(encoding="utf-8")
|
||||
md = MarkdownIt("gfm-like").enable("table")
|
||||
body_html = md.render(md_text)
|
||||
md_text = md_path.read_text(encoding="utf-8")
|
||||
# html=True: lets the `<a id="...">` anchors inserted before each heading (see
|
||||
# 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)
|
||||
if path.startswith(("http://", "https://", "file://", "/")):
|
||||
if path.startswith(("http://", "https://", "file://", "data:", "/")):
|
||||
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>
|
||||
@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; }}
|
||||
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>"""
|
||||
{STYLE}</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")
|
||||
|
||||
subprocess.run(
|
||||
[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,
|
||||
)
|
||||
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():
|
||||
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_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__":
|
||||
|
||||
@@ -167,7 +167,29 @@ Ursache: `focusPanel(subGraph:subLayout:)` in `OverviewView.swift` rendert Kante
|
||||
Build+alle 99 Unit-Tests grün, live bestätigt.
|
||||
|
||||
### 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
|
||||
**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).
|
||||
|
||||
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