diff --git a/GeoTurtl_install_v0.9.7-beta.sh b/GeoTurtl_install_v0.9.7-beta.sh new file mode 100644 index 0000000..af0940f --- /dev/null +++ b/GeoTurtl_install_v0.9.7-beta.sh @@ -0,0 +1,833 @@ +#!/bin/zsh +# ============================================================================= +# GeoTurtl_install_v0.9.7-beta.sh +# Selbstentpackendes Installationsscript für GeoTurtl v0.9.7-beta +# +# VERWENDUNG: +# chmod +x ~/Downloads/GeoTurtl_v0.9.7-beta/GeoTurtl_install_v0.9.7-beta.sh +# ~/Downloads/GeoTurtl_v0.9.7-beta/GeoTurtl_install_v0.9.7-beta.sh +# ============================================================================= + +set -e + +VERSION="0.9.7-beta" +TARGET="/Applications/Scripte/python/GeoTurtl" +SCRIPT="$TARGET/GeoTurtl.py" +README_HTML="$TARGET/README.html" +README_MD="$TARGET/README.md" +STARTER="$TARGET/geoturtl_start.sh" +STOPPER="$TARGET/geoturtl_stop.sh" +PLIST_DIR="$HOME/Library/LaunchAgents" +PLIST="$PLIST_DIR/com.user.geoturtl.plist" +LOGFILE="$HOME/Library/Logs/GeoTurtl.log" +PIDFILE="$HOME/Library/Application Support/GeoTurtl.pid" + +# Ordner des Installationsscripts (hier liegen die README-Dateien) +INSTALL_DIR="$(cd "$(dirname "$0")" && pwd)" + +echo "" +echo "╔══════════════════════════════════════════════════╗" +echo "║ 🐢 GeoTurtl v$VERSION – Installation ║" +echo "╚══════════════════════════════════════════════════╝" +echo "" + +# ── Haftungsausschluss ──────────────────────────────────────────────────────── +echo "┌─────────────────────────────────────────────────────┐" +echo "│ ⚠️ HAFTUNGSAUSSCHLUSS │" +echo "├─────────────────────────────────────────────────────┤" +echo "│ GeoTurtl wurde nach bestem Wissen und Gewissen │" +echo "│ entwickelt und kostenlos zur Verfügung gestellt. │" +echo "│ │" +echo "│ • Die Software wird 'so wie sie ist' (as is) │" +echo "│ bereitgestellt – ohne jegliche Garantie. │" +echo "│ • H. Linz übernimmt keine Haftung für Schäden, │" +echo "│ Datenverluste oder Beeinträchtigungen jeglicher │" +echo "│ Art durch Nutzung dieser Software. │" +echo "│ • Drittdienste (ipinfo.io, ipify.org) liegen │" +echo "│ außerhalb der Verantwortung des Autors. │" +echo "│ • Jede Person nutzt diese Software auf eigene │" +echo "│ Gefahr und eigene Verantwortung. │" +echo "│ │" +echo "│ Durch Fortfahren erkennst du diese Bedingungen an. │" +echo "└─────────────────────────────────────────────────────┘" +echo "" +echo -n " Installation fortsetzen? [j/N] " +read CONFIRM +if [[ "$CONFIRM" != "j" && "$CONFIRM" != "J" ]]; then + echo " Installation abgebrochen." + exit 0 +fi +echo "" + +# ── Alte Versionen entfernen ────────────────────────────────────────────────── +echo "→ Prüfe auf alte Versionen …" + +# Alle laufenden ddIP/GeoTurtl-Prozesse beenden +for PROCNAME in "ddIP.py" "GeoTurtl.py"; do + PIDS=$(pgrep -f "$PROCNAME" 2>/dev/null || true) + if [ -n "$PIDS" ]; then + echo " ✓ Beende laufende Instanz: $PROCNAME (PID $PIDS)" + pkill -f "$PROCNAME" 2>/dev/null || true + sleep 1 + fi +done + +# Alte PID-Files entfernen +for PIDFILE in "$HOME/Library/Application Support/ddIP.pid" "$HOME/Library/Application Support/GeoTurtl.pid"; do + [ -f "$PIDFILE" ] && rm -f "$PIDFILE" && echo " ✓ PID-File entfernt: $PIDFILE" +done + +# Alte Lock-Files entfernen +for LOCKFILE in "$HOME/Library/Application Support/GeoTurtl.lock"; do + [ -f "$LOCKFILE" ] && rm -f "$LOCKFILE" && echo " ✓ Lock-File entfernt: $LOCKFILE" +done + +# Alte launchd-Agenten entfernen +for PLIST in "$HOME/Library/LaunchAgents/com.user.ddip.plist" "$HOME/Library/LaunchAgents/com.user.geoturtl.plist"; do + if [ -f "$PLIST" ]; then + launchctl unload "$PLIST" 2>/dev/null || true + rm -f "$PLIST" + echo " ✓ launchd-Agent entfernt: $(basename $PLIST)" + fi +done + +# Alte Programmordner entfernen +for DIR in "/Applications/Scripte/python/ddIP" "/Applications/Scripte/python/GeoTurtl"; do + if [ -d "$DIR" ] && [ "$DIR" != "$TARGET" ]; then + rm -rf "$DIR" + echo " ✓ Alter Programmordner entfernt: $DIR" + fi +done + +# Alte Einzeldateien entfernen (ddIP ohne Unterordner) +for FILE in "/Applications/Scripte/python/ddIP.py" "/Applications/Scripte/python/ddip_start.sh" "/Applications/Scripte/python/ddip_stop.sh" "/Applications/Scripte/python/geoturtl_start.sh" "/Applications/Scripte/python/geoturtl_stop.sh"; do + if [ -f "$FILE" ]; then + rm -f "$FILE" + echo " ✓ Alte Datei entfernt: $FILE" + fi +done + +# Alte Aliases aus ~/.zshrc entfernen +if grep -qE "alias (ddip|geoturtl)" ~/.zshrc 2>/dev/null; then + sed -i '' '/alias ddip/d' ~/.zshrc + sed -i '' '/alias geoturtl/d' ~/.zshrc + echo " ✓ Alte Aliases aus ~/.zshrc entfernt" +fi + +echo " ✓ Aufräumen abgeschlossen" +echo "" + +# ── Python-Pakete ───────────────────────────────────────────────────────────── +echo "→ Installiere Python-Pakete …" +pip3 install --quiet rumps pyobjc-framework-CoreLocation +echo " ✓ Pakete installiert" + +# ── Zielordner ──────────────────────────────────────────────────────────────── +if [ ! -d "$TARGET" ]; then + echo "→ Lege Ordner an: $TARGET" + mkdir -p "$TARGET" +else + echo "→ Ordner existiert bereits: $TARGET" +fi + +# ── README-Dateien kopieren (liegen neben dem Installationsscript) ──────────── +echo "→ Kopiere Dokumentation …" +if [ -f "$INSTALL_DIR/README.html" ]; then + cp "$INSTALL_DIR/README.html" "$README_HTML" + echo " ✓ README.html kopiert" +else + echo " ⚠️ README.html nicht gefunden – wird übersprungen" +fi +if [ -f "$INSTALL_DIR/README.md" ]; then + cp "$INSTALL_DIR/README.md" "$README_MD" + echo " ✓ README.md kopiert" +else + echo " ⚠️ README.md nicht gefunden – wird übersprungen" +fi + +# ── GeoTurtl.py erzeugen ───────────────────────────────────────────────────── +echo "→ Erzeuge GeoTurtl.py …" +cat > "$SCRIPT" << 'PYEOF' +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# ============================================================================= +# yaml +# name: GeoTurtl +# version: 0.9.7-beta +# description: macOS Menüleisten-App – Standort + öffentliche IP-Adressen +# author: H. Linz +# contact: linz.on.tour@gmail.com +# license: MIT +# requires_python: ">=3.6" +# dependencies: +# - rumps +# - pyobjc-framework-CoreLocation +# platform: macOS (Apple Silicon & Intel) +# created: 2026-06-07 +# disclaimer: > +# Diese Software wird ohne jegliche Garantie bereitgestellt. +# Nutzung auf eigene Gefahr. Keine Haftung für Schäden. +# ============================================================================= + +import json +import os +import socket +import subprocess +import sys +import threading +import time +import urllib.request +from datetime import datetime + +import objc +import rumps +from CoreLocation import CLGeocoder, CLLocation, CLLocationManager +from Foundation import NSBundle, NSDate, NSRunLoop + +APP_TITLE = "🐢" +APP_VERSION = "0.9.7-beta" +AUTHOR = "H. Linz" +CONTACT = "linz.on.tour@gmail.com" +UPDATE_INTERVALS = {"Aus": 0, "1 Minute": 60, "5 Minuten": 300, "15 Minuten": 900} +DEFAULT_INTERVAL = "5 Minuten" +TIMEOUT = 8 +LOC_TIMEOUT = 15 +README_HTML = os.path.join(os.path.dirname(os.path.abspath(__file__)), "README.html") + + +# ── CoreLocation Delegate ────────────────────────────────────────────────────── + +class LocationDelegate(objc.lookUpClass("NSObject")): + def init(self): + self = objc.super(LocationDelegate, self).init() + if self is None: + return None + self.location = None + self.error = None + self.done = threading.Event() + return self + + def locationManager_didUpdateLocations_(self, manager, locations): + if locations: + self.location = locations[-1] + manager.stopUpdatingLocation() + self.done.set() + + def locationManager_didFailWithError_(self, manager, error): + self.error = str(error) + manager.stopUpdatingLocation() + self.done.set() + + def locationManagerDidChangeAuthorization_(self, manager): + status = manager.authorizationStatus() + if status in (1, 2): + self.error = "Ortungsdienste verweigert" + self.done.set() + + +def get_location(retries=4, delay=3): + """ + Versucht den Standort zu ermitteln. Bei kCLErrorLocationUnknown (Code=0) + wird bis zu `retries`-mal neu gestartet – typisch wenn WLAN-Ortung + kurz nach dem Login noch nicht bereit ist. + + Fehlercodes: + Code=0 LocationUnknown – kurz warten, nochmal versuchen + Code=1 Denied – Ortungsdienste verweigert + Code=2 Network – Netzwerkfehler bei Ortung + """ + for attempt in range(1, retries + 1): + delegate = LocationDelegate.alloc().init() + manager = CLLocationManager.alloc().init() + manager.setDelegate_(delegate) + manager.setDesiredAccuracy_(10) + manager.requestWhenInUseAuthorization() + manager.startUpdatingLocation() + deadline = time.time() + LOC_TIMEOUT + while not delegate.done.is_set() and time.time() < deadline: + NSRunLoop.currentRunLoop().runUntilDate_(NSDate.dateWithTimeIntervalSinceNow_(0.2)) + + if not delegate.done.is_set(): + raise TimeoutError( + f"Standort-Timeout nach {LOC_TIMEOUT}s (Versuch {attempt}/{retries})") + + if delegate.error: + err = delegate.error + # Code=0: vorübergehend unbekannt → retry + if "Code=0" in err: + if attempt < retries: + _log(f"Standort: kCLErrorLocationUnknown (Versuch {attempt}/{retries}) " + f"– warte {delay}s …", "WARN") + time.sleep(delay) + continue + raise RuntimeError( + "Standort nach mehreren Versuchen nicht ermittelbar " + "(WLAN-Ortung liefert kein Signal)") + # Code=1: Berechtigung verweigert + elif "Code=1" in err: + raise RuntimeError( + "Ortungsdienste verweigert – bitte in Systemeinstellungen " + "→ Datenschutz & Sicherheit → Ortungsdienste → Terminal aktivieren") + # Code=2: Netzwerkfehler + elif "Code=2" in err: + raise RuntimeError( + "Netzwerkfehler bei Standortermittlung (kCLErrorNetwork)") + else: + raise RuntimeError(f"CoreLocation-Fehler: {err}") + + return delegate.location + raise RuntimeError("Standort nach mehreren Versuchen nicht ermittelbar") + + +def reverse_geocode(lat, lon): + result = {"address": None, "done": threading.Event()} + cl_loc = CLLocation.alloc().initWithLatitude_longitude_(lat, lon) + geocoder = CLGeocoder.alloc().init() + + def handler(placemarks, error): + if placemarks: + pm = placemarks[0] + parts = [] + if pm.thoroughfare(): + street = pm.thoroughfare() + if pm.subThoroughfare(): + street += f" {pm.subThoroughfare()}" + parts.append(street) + if pm.postalCode(): + parts.append(pm.postalCode()) + if pm.locality(): + parts.append(pm.locality()) + result["address"] = ", ".join(parts) if parts else "Unbekannte Adresse" + else: + result["address"] = "Adresse nicht verfügbar" + result["done"].set() + + geocoder.reverseGeocodeLocation_completionHandler_(cl_loc, handler) + deadline = time.time() + 10 + while not result["done"].is_set() and time.time() < deadline: + NSRunLoop.currentRunLoop().runUntilDate_(NSDate.dateWithTimeIntervalSinceNow_(0.2)) + return result.get("address") or "—" + + +# ── IP-Dienste ───────────────────────────────────────────────────────────────── + +IPV4_SERVICES = [ + {"url": "https://ipinfo.io/json", + "parse": lambda d: {"ip": d.get("ip"), "org": d.get("org"), + "city": d.get("city"), "region": d.get("region"), + "country": d.get("country")}}, + {"url": "https://api4.ipify.org?format=json", + "parse": lambda d: {"ip": d.get("ip"), "org": None, + "city": None, "region": None, "country": None}}, +] +IPV6_SERVICES = [ + {"url": "https://api6.ipify.org?format=json", + "parse": lambda d: {"ip": d.get("ip")}}, + {"url": "https://ipv6.icanhazip.com", "raw": True, + "parse": lambda d: {"ip": d.get("ip")}}, +] + + +def fetch_ip(service): + try: + req = urllib.request.Request( + service["url"], + headers={"User-Agent": "GeoTurtl/0.9.7-beta (macOS; python3)"}) + with urllib.request.urlopen(req, timeout=TIMEOUT) as resp: + body = resp.read().decode().strip() + return service["parse"]({"ip": body} if service.get("raw") else json.loads(body)) + except Exception: + return None + + +def get_ips(): + v4, v6 = None, None + + def do_v4(): + nonlocal v4 + for svc in IPV4_SERVICES: + r = fetch_ip(svc) + if r and r.get("ip"): + v4 = r; break + + def do_v6(): + nonlocal v6 + for svc in IPV6_SERVICES: + r = fetch_ip(svc) + if r and r.get("ip"): + v6 = r; break + + t4, t6 = threading.Thread(target=do_v4), threading.Thread(target=do_v6) + t4.start(); t6.start() + t4.join(); t6.join() + return v4, v6 + + +# ── Logging ─────────────────────────────────────────────────────────────────── + +def _log(msg, level="INFO"): + """Schreibt eine Zeile mit Zeitstempel und Level nach stdout (→ Log-Datei).""" + ts = datetime.now().strftime("%d.%m.%Y %H:%M:%S") + print(f"[{ts}] [{level}] {msg}") + sys.stdout.flush() + + +# ── Netzwerk-Diagnose ────────────────────────────────────────────────────────── + +def check_internet(): + """ + Gestaffelte Netzwerkprüfung: + 1. TCP-Verbindung zu 8.8.8.8:53 → grundlegende IP-Konnektivität + 2. DNS-Auflösung von google.com → DNS funktioniert + 3. HTTP-Request zu ipinfo.io → Internet + Firewall OK + Gibt (ok: bool, detail: str) zurück. + """ + # Stufe 1: TCP + try: + sock = socket.create_connection(("8.8.8.8", 53), timeout=3) + sock.close() + except OSError: + return False, "Keine Netzwerkverbindung (TCP zu 8.8.8.8:53 fehlgeschlagen)" + + # Stufe 2: DNS + try: + socket.getaddrinfo("google.com", 80) + except socket.gaierror: + return False, "Netzwerk erreichbar, aber DNS nicht funktionsfähig" + + # Stufe 3: HTTP + try: + req = urllib.request.Request( + "https://ipinfo.io/json", + headers={"User-Agent": "GeoTurtl/0.9.7-beta (macOS; python3)"}) + with urllib.request.urlopen(req, timeout=5): + pass + except Exception as e: + return False, f"DNS OK, aber HTTP-Anfragen blockiert oder kein Internet ({e})" + + return True, "OK" + + +def diagnose_network(): + """Führt Netzwerkdiagnose durch und gibt lesbaren Status zurück.""" + ok, detail = check_internet() + if ok: + _log("Netzwerk: Verbindung OK") + else: + _log(f"Netzwerk: {detail}", "WARN") + return ok + + +# ── Menüleisten-App ──────────────────────────────────────────────────────────── + +class GeoTurtlApp(rumps.App): + def __init__(self): + super().__init__(APP_TITLE, quit_button=None) + self._loc_data = None + self._ip_data = None + self._updating = False + self._interval = DEFAULT_INTERVAL + self._timer = None + self._v4_str = None + self._v6_str = None + self._build_menu() + threading.Thread(target=self._update_all, daemon=True).start() + + def _build_menu(self): + self.menu.clear() + + self.menu.add(rumps.MenuItem("🔄 Alles aktualisieren", callback=self._on_refresh)) + self.menu.add(rumps.separator) + + # ── Standort ────────────────────────────────────────────────────────── + self.menu.add(rumps.MenuItem("📍 STANDORT", callback=None)) + self._item_address = rumps.MenuItem(" Adresse …") + self._item_coords = rumps.MenuItem(" Koordinaten …") + self._item_altitude = rumps.MenuItem(" Höhe …") + self._item_accuracy = rumps.MenuItem(" Genauigkeit …") + self.menu.add(self._item_address) + self.menu.add(self._item_coords) + self.menu.add(self._item_altitude) + self.menu.add(self._item_accuracy) + self.menu.add(rumps.separator) + + self._item_gmaps = rumps.MenuItem(" 🗺 Google Maps öffnen", callback=self._open_gmaps) + self._item_amaps = rumps.MenuItem(" 🍎 Apple Maps öffnen", callback=self._open_amaps) + self._item_osm = rumps.MenuItem(" 🌍 OpenStreetMap öffnen", callback=self._open_osm) + self.menu.add(self._item_gmaps) + self.menu.add(self._item_amaps) + self.menu.add(self._item_osm) + self.menu.add(rumps.separator) + + # ── IP-Adressen ─────────────────────────────────────────────────────── + self.menu.add(rumps.MenuItem("🌐 IP-ADRESSEN", callback=None)) + self._item_v4 = rumps.MenuItem(" IPv4 …") + self._item_v4_loc = rumps.MenuItem(" Standort …") + self._item_v4_org = rumps.MenuItem(" Provider …") + self._item_copy_v4 = rumps.MenuItem(" 📋 IPv4 kopieren", callback=self._copy_v4) + self._item_v6 = rumps.MenuItem(" IPv6 …") + self._item_copy_v6 = rumps.MenuItem(" 📋 IPv6 kopieren", callback=self._copy_v6) + self.menu.add(self._item_v4) + self.menu.add(self._item_v4_loc) + self.menu.add(self._item_v4_org) + self.menu.add(self._item_copy_v4) + self.menu.add(rumps.separator) + self.menu.add(self._item_v6) + self.menu.add(self._item_copy_v6) + self.menu.add(rumps.separator) + + # ── Auto-Update ─────────────────────────────────────────────────────── + auto_menu = rumps.MenuItem("⏱ Auto-Update") + for label in UPDATE_INTERVALS: + item = rumps.MenuItem( + f" {label}" + (" ✓" if label == self._interval else ""), + callback=self._on_interval) + auto_menu.add(item) + self.menu.add(auto_menu) + self.menu.add(rumps.separator) + + # ── Hilfe ───────────────────────────────────────────────────────────── + hilfe_menu = rumps.MenuItem("❓ Hilfe") + hilfe_menu.add(rumps.MenuItem(f" GeoTurtl v{APP_VERSION}")) + hilfe_menu.add(rumps.separator) + hilfe_menu.add(rumps.MenuItem(" 📖 Dokumentation öffnen", callback=self._open_readme)) + hilfe_menu.add(rumps.separator) + hilfe_menu.add(rumps.MenuItem(" TERMINAL-BEFEHLE")) + hilfe_menu.add(rumps.MenuItem(" ▶ Starten: geoturtl_start")) + hilfe_menu.add(rumps.MenuItem(" ■ Stoppen: geoturtl_stop")) + hilfe_menu.add(rumps.MenuItem(" 📄 Log: tail -f ~/Library/Logs/GeoTurtl.log")) + hilfe_menu.add(rumps.MenuItem(" 📝 Log öffnen", callback=self._open_log)) + hilfe_menu.add(rumps.separator) + hilfe_menu.add(rumps.MenuItem(" KONTAKT")) + hilfe_menu.add(rumps.MenuItem(f" {AUTHOR}")) + hilfe_menu.add(rumps.MenuItem(f" ✉️ {CONTACT}", callback=self._open_mail)) + self.menu.add(hilfe_menu) + self.menu.add(rumps.separator) + + # ── Zeitstempel + Beenden ───────────────────────────────────────────── + self._item_updated = rumps.MenuItem(" Noch nicht aktualisiert") + self.menu.add(self._item_updated) + self.menu.add(rumps.separator) + self.menu.add(rumps.MenuItem("✖ Beenden", callback=self._on_quit)) + + # ── Callbacks ───────────────────────────────────────────────────────────── + + def _on_refresh(self, _): + if not self._updating: + threading.Thread(target=self._update_all, daemon=True).start() + + def _on_interval(self, sender): + label = sender.title.strip().rstrip(" ✓").strip() + self._interval = label + for item in self.menu["⏱ Auto-Update"].values(): + clean = item.title.strip().rstrip(" ✓").strip() + item.title = f" {clean}" + (" ✓" if clean == label else "") + self._restart_timer() + + def _on_quit(self, _): + rumps.quit_application() + + def _open_readme(self, _): + if os.path.exists(README_HTML): + os.system(f"open '{README_HTML}'") + else: + rumps.notification("GeoTurtl", "Dokumentation nicht gefunden", + README_HTML) + + def _open_gmaps(self, _): + if self._loc_data and "lat" in self._loc_data: + lat, lon = self._loc_data["lat"], self._loc_data["lon"] + os.system(f"open 'https://maps.google.com/?q={lat},{lon}'") + + def _open_amaps(self, _): + if self._loc_data and "lat" in self._loc_data: + lat, lon = self._loc_data["lat"], self._loc_data["lon"] + os.system(f"open 'https://maps.apple.com/?ll={lat},{lon}&q=GeoTurtl'") + + def _open_osm(self, _): + if self._loc_data and "lat" in self._loc_data: + lat, lon = self._loc_data["lat"], self._loc_data["lon"] + os.system(f"open 'https://www.openstreetmap.org/?mlat={lat}&mlon={lon}&zoom=16'") + + def _open_log(self, _): + log = os.path.expanduser("~/Library/Logs/GeoTurtl.log") + if not os.path.exists(log): + # Leere Log-Datei anlegen damit der Editor sie öffnen kann + open(log, "a").close() + os.system(f"open -e '{log}'") # -e = immer TextEdit + + def _open_mail(self, _): + os.system(f"open 'mailto:{CONTACT}?subject=GeoTurtl%20v{APP_VERSION}'") + + def _copy_v4(self, _): + if self._v4_str: + subprocess.run("pbcopy", input=self._v4_str.encode(), check=True) + rumps.notification("GeoTurtl", "IPv4 kopiert", self._v4_str) + + def _copy_v6(self, _): + if self._v6_str: + subprocess.run("pbcopy", input=self._v6_str.encode(), check=True) + rumps.notification("GeoTurtl", "IPv6 kopiert", self._v6_str) + + # ── Timer ────────────────────────────────────────────────────────────────── + + def _restart_timer(self): + if self._timer: + self._timer.stop() + self._timer = None + secs = UPDATE_INTERVALS.get(self._interval, 0) + if secs > 0: + self._timer = rumps.Timer(self._timer_tick, secs) + self._timer.start() + + def _timer_tick(self, _): + if not self._updating: + threading.Thread(target=self._update_all, daemon=True).start() + + # ── Daten aktualisieren ──────────────────────────────────────────────────── + + def _update_all(self): + self._updating = True + self.title = "🐢 ⟳" + loc_thread = threading.Thread(target=self._fetch_location, daemon=True) + ip_thread = threading.Thread(target=self._fetch_ips, daemon=True) + loc_thread.start(); ip_thread.start() + loc_thread.join(); ip_thread.join() + self._refresh_menu() + self._updating = False + self._restart_timer() + + def _fetch_location(self): + try: + loc = get_location() + lat = loc.coordinate().latitude + lon = loc.coordinate().longitude + self._loc_data = { + "lat": lat, + "lon": lon, + "alt": loc.altitude(), + "acc": loc.horizontalAccuracy(), + "address": reverse_geocode(lat, lon), + } + _log(f"Standort: {self._loc_data['address']} " + f"({lat:.6f}, {lon:.6f}), " + f"Genauigkeit ±{loc.horizontalAccuracy():.0f}m") + except Exception as e: + _log(f"Standort: {e}", "ERROR") + self._loc_data = {"error": str(e)} + + def _fetch_ips(self): + v4, v6 = get_ips() + self._ip_data = {"v4": v4, "v6": v6} + self._v4_str = v4["ip"] if v4 else None + self._v6_str = v6["ip"] if v6 else None + + if v4: + _log(f"IPv4: {v4['ip']} – {v4.get('org') or '—'}") + else: + # Gestaffelte Diagnose nur wenn IP-Abruf fehlschlug + _log("IPv4: nicht ermittelbar – starte Netzwerkdiagnose …", "WARN") + net_ok = diagnose_network() + if not net_ok: + self._ip_data["net_error"] = "Keine Internetverbindung" + _log("Kein Internet – IP-Lokalisierung nicht möglich", "ERROR") + else: + self._ip_data["net_error"] = "IP-Dienste nicht erreichbar" + _log("Internet OK, aber alle IP-Dienste nicht erreichbar", "WARN") + + if v6: + _log(f"IPv6: {v6['ip']}") + else: + _log("IPv6: nicht verfügbar (Provider unterstützt kein IPv6)") + + # ── Menü aktualisieren ───────────────────────────────────────────────────── + + def _refresh_menu(self): + loc = self._loc_data or {} + ips = self._ip_data or {} + v4 = ips.get("v4") or {} + v6 = ips.get("v6") or {} + + loc_error = "error" in (self._loc_data or {}) + ip_error = not (self._ip_data or {}).get("v4") and not (self._ip_data or {}).get("v6") + self.title = "🐢 ⚠️" if (loc_error or ip_error) else "🐢" + + if "error" in loc: + self._item_address.title = f" ⚠️ {loc['error']}" + self._item_coords.title = " Koordinaten: —" + self._item_altitude.title = " Höhe: —" + self._item_accuracy.title = " Genauigkeit: —" + else: + self._item_address.title = f" {loc.get('address', '—')}" + self._item_coords.title = (f" {loc['lat']:.6f}°, {loc['lon']:.6f}°" + if "lat" in loc else " —") + alt = loc.get('alt', 0) + alt_str = f"{alt:.0f} m ü. NN" if alt and abs(alt) > 0.5 else "nicht verfügbar (WLAN-Ortung)" + self._item_altitude.title = f" Höhe: {alt_str}" + self._item_accuracy.title = f" Genauigkeit: ± {loc.get('acc', 0):.0f} m" + + if v4.get("ip"): + self._item_v4.title = f" IPv4: {v4['ip']}" + loc_parts = [p for p in [v4.get("city"), v4.get("region"), v4.get("country")] if p] + self._item_v4_loc.title = f" Standort: {', '.join(loc_parts) if loc_parts else '—'}" + self._item_v4_org.title = f" Provider: {v4.get('org') or '—'}" + else: + net_err = ips.get("net_error", "") + if "Kein Internet" in net_err or "Keine Internet" in net_err: + self._item_v4.title = " ⚠️ Kein Internet" + self._item_v4_loc.title = " Prüfe deine Netzwerkverbindung" + self._item_v4_org.title = " —" + elif net_err: + self._item_v4.title = " ⚠️ IP-Dienste nicht erreichbar" + self._item_v4_loc.title = " Internet OK, Dienste blockiert?" + self._item_v4_org.title = " —" + else: + self._item_v4.title = " IPv4: nicht verfügbar" + self._item_v4_loc.title = " Standort: —" + self._item_v4_org.title = " Provider: —" + + if v6.get("ip"): + ip6 = v6["ip"] + self._item_v6.title = f" IPv6: {ip6 if len(ip6) <= 30 else ip6[:27] + '…'}" + else: + self._item_v6.title = " IPv6: nicht verfügbar" + + self._item_updated.title = f" Aktualisiert: {datetime.now().strftime('%d.%m.%Y %H:%M:%S')}" + + +# ── Start ────────────────────────────────────────────────────────────────────── + +if __name__ == "__main__": + import platform + import fcntl + + # ── Single-Instance-Lock: verhindert doppelte Prozesse ──────────────────── + LOCK_FILE = os.path.expanduser("~/Library/Application Support/GeoTurtl.lock") + os.makedirs(os.path.dirname(LOCK_FILE), exist_ok=True) + try: + _lock_fh = open(LOCK_FILE, "w") + fcntl.flock(_lock_fh, fcntl.LOCK_EX | fcntl.LOCK_NB) + except BlockingIOError: + print(f"[{datetime.now().strftime('%d.%m.%Y %H:%M:%S')}] [WARN] " + f"GeoTurtl läuft bereits – zweite Instanz wird beendet.") + sys.exit(0) + + # ── Dock-Icon unterdrücken ──────────────────────────────────────────────── + info = NSBundle.mainBundle().infoDictionary() + info["LSUIElement"] = "1" + + _log(f"GeoTurtl v{APP_VERSION} gestartet " + f"(PID {os.getpid()}, Python {platform.python_version()}, " + f"macOS {platform.mac_ver()[0]})") + app = GeoTurtlApp() + app.run() +PYEOF +chmod +x "$SCRIPT" +echo " ✓ GeoTurtl.py erzeugt" + +# ── Starter ─────────────────────────────────────────────────────────────────── +echo "→ Erzeuge Starter …" +cat > "$STARTER" << STARTEOF +#!/bin/zsh +LOGFILE="$LOGFILE" +PIDFILE="$PIDFILE" +mkdir -p "\$(dirname \$PIDFILE)" +if [ -f "\$PIDFILE" ]; then + OLD_PID=\$(cat "\$PIDFILE") + if kill -0 "\$OLD_PID" 2>/dev/null; then + echo "Beende laufende GeoTurtl-Instanz (PID \$OLD_PID) …" + kill "\$OLD_PID" && sleep 1 + fi + rm -f "\$PIDFILE" +fi +nohup $(which python3) "$SCRIPT" > "\$LOGFILE" 2>&1 & +echo \$! > "\$PIDFILE" +echo "GeoTurtl gestartet (PID \$!)" +echo "Log: \$LOGFILE" +STARTEOF +chmod +x "$STARTER" +echo " ✓ Starter erzeugt" + +# ── Stopper ─────────────────────────────────────────────────────────────────── +echo "→ Erzeuge Stopper …" +cat > "$STOPPER" << STOPEOF +#!/bin/zsh +PIDFILE="$PIDFILE" +if [ -f "\$PIDFILE" ]; then + PID=\$(cat "\$PIDFILE") + if kill -0 "\$PID" 2>/dev/null; then + kill "\$PID" && echo "GeoTurtl beendet (PID \$PID)." + else + echo "GeoTurtl lief nicht mehr." + fi + rm -f "\$PIDFILE" +else + echo "Kein PID-File — GeoTurtl läuft nicht." +fi +STOPEOF +chmod +x "$STOPPER" +echo " ✓ Stopper erzeugt" + +# ── launchd-Autostart ───────────────────────────────────────────────────────── +echo "→ Richte Autostart via launchd ein …" +mkdir -p "$PLIST_DIR" +PYTHON_BIN=$(which python3) +cat > "$PLIST" << PLISTEOF + + + + + Label + com.user.geoturtl + ProgramArguments + + $PYTHON_BIN + $SCRIPT + + RunAtLoad + + KeepAlive + + StandardOutPath + $LOGFILE + StandardErrorPath + $LOGFILE + + +PLISTEOF +launchctl unload "$PLIST" 2>/dev/null || true +launchctl load "$PLIST" +echo " ✓ Autostart eingerichtet" + +# ── Aliases ─────────────────────────────────────────────────────────────────── +for ENTRY in "geoturtl_start=$STARTER" "geoturtl_stop=$STOPPER"; do + ANAME="${ENTRY%%=*}" + AVAL="${ENTRY#*=}" + ALINE="alias $ANAME='$AVAL'" + if grep -q "alias $ANAME=" ~/.zshrc 2>/dev/null; then + echo "→ Alias '$ANAME' bereits vorhanden, wird übersprungen." + else + echo "$ALINE" >> ~/.zshrc + echo "→ Alias '$ANAME' eingetragen." + fi +done +source ~/.zshrc 2>/dev/null || true + +# ── Fertig ──────────────────────────────────────────────────────────────────── +echo "" +echo "╔══════════════════════════════════════════════════╗" +echo "║ ✓ GeoTurtl v$VERSION – Installation fertig! ║" +echo "║ ║" +echo "║ 🐢 Starten: geoturtl_start ║" +echo "║ ■ Stoppen: geoturtl_stop ║" +echo "║ 📄 Log: ~/Library/Logs/GeoTurtl.log ║" +echo "║ 📖 Doku: $TARGET/README.html ║" +echo "╚══════════════════════════════════════════════════╝" +echo "" +echo "HINWEIS: Beim ersten Start Ortungsdienste erlauben:" +echo " Systemeinstellungen → Datenschutz & Sicherheit" +echo " → Ortungsdienste → Terminal → Beim Verwenden" +echo "" +echo "→ Starte GeoTurtl jetzt im Hintergrund …" +"$STARTER" diff --git a/README.html b/README.html new file mode 100644 index 0000000..4eaa095 --- /dev/null +++ b/README.html @@ -0,0 +1,666 @@ + + + + + +🐢 GeoTurtl v0.9.7-beta – Dokumentation + + + + + +
+
+ + H. Linz / GeoTurtl + v0.9.7-beta +
+
+ +
+ + +
+ +

🐢 GeoTurtl v0.9.7-beta

+ +

+ GeoTurtl ist eine kleine, unauffällige App für deinen Mac. + Sie sitzt als Schildkröten-Symbol 🐢 in der Menüleiste und zeigt dir jederzeit + auf Knopfdruck, wo du gerade bist – und wie deine Verbindung ins Internet aussieht. +

+ +
+ 📋 Beta-Hinweis + Dies ist eine Beta-Version (v0.9.0). Sie ist funktionsfähig, kann aber noch + kleinere Fehler enthalten. Rückmeldungen sind herzlich willkommen! +
+ +
+ + +

Was ist GeoTurtl und was macht es?

+ +

GeoTurtl zeigt dir zwei Dinge auf einen Blick:

+ +

📍 Deinen Standort

+

+ GeoTurtl nutzt die eingebaute Ortungsfunktion deines Macs – dieselbe Technik, + die auch Apple Maps verwendet. Es zeigt dir: +

+ +

+ Mit einem Klick öffnest du deinen Standort direkt in + Google Maps, Apple Maps oder OpenStreetMap. +

+ +

🌐 Deine IP-Adressen

+

+ GeoTurtl ermittelt deine öffentliche IPv4- und (falls vorhanden) IPv6-Adresse – + also die Adresse, unter der du im Internet sichtbar bist. Zusätzlich siehst du + den ungefähren Standort dieser Adresse und deinen Internetanbieter. + Per Klick kannst du beide Adressen direkt in die Zwischenablage kopieren. +

+ +
+ ⏱ Auto-Update + GeoTurtl aktualisiert sich automatisch – du kannst selbst einstellen, + wie oft: alle 1, 5 oder 15 Minuten, oder manuell auf Knopfdruck. +
+ +
+ + +

Welche Installations- und Systemvoraussetzungen gibt es?

+ + + +

Die Installation erledigt alles Weitere automatisch für dich.

+ +
+ + +

Ist die Software sicher?

+ +

Ja. GeoTurtl ist quelloffen (MIT-Lizenz) – du kannst den kompletten + Programmcode jederzeit selbst einsehen. Das Programm:

+ + + +

+ Die Ortungsdienste werden nur auf ausdrückliche Anfrage genutzt – + macOS fragt beim ersten Start nach deiner Erlaubnis. +

+ +
+ + +

Wie installiere ich GeoTurtl?

+ +

+ Du benötigst nur die Datei GeoTurtl_install_v0.9.7-beta.sh + aus dem heruntergeladenen ZIP-Paket. Öffne das Terminal + (Programme → Dienstprogramme → Terminal) und führe diese zwei Befehle aus: +

+ +
+
+
1
+
+

Datei ausführbar machen

+
chmod +x ~/Downloads/GeoTurtl_v0.9.7-beta/GeoTurtl_install_v0.9.7-beta.sh
+
+
+
+
2
+
+

Installation starten

+
~/Downloads/GeoTurtl_v0.9.7-beta/GeoTurtl_install_v0.9.7-beta.sh
+
+
+
+
3
+
+

Ortungsdienste erlauben

+

Beim ersten Start erscheint eine macOS-Meldung. Klicke auf + „Beim Verwenden erlauben". Falls die Meldung ausbleibt:

+

Systemeinstellungen → Datenschutz & Sicherheit → Ortungsdienste → Terminal → „Beim Verwenden"

+
+
+
+ +
+ ℹ️ Was passiert bei der Installation? + Das Script installiert die benötigten Python-Pakete, legt den Programmordner an, + richtet den automatischen Start beim Mac-Login ein und startet GeoTurtl sofort. +
+ +
+ + +

Wie starte und stoppe ich GeoTurtl?

+ +

Nach der Installation erscheint das 🐢-Symbol automatisch in der Menüleiste. + Bei Bedarf kannst du GeoTurtl auch manuell über das Terminal steuern:

+ +

▶ Starten

+
geoturtl_start
+ +

■ Stoppen

+
geoturtl_stop
+ +
+ 🔄 Autostart + GeoTurtl startet nach der Installation automatisch bei jedem Mac-Login – + du musst nichts weiter tun. +
+ +
+ + +

Wo finde ich die Log-Dateien?

+ +

GeoTurtl schreibt alle wichtigen Ereignisse in eine Log-Datei:

+
~/Library/Logs/GeoTurtl.log
+ +

Log live verfolgen

+
tail -f ~/Library/Logs/GeoTurtl.log
+ +

Letzte 50 Zeilen anzeigen

+
tail -50 ~/Library/Logs/GeoTurtl.log
+ +

Log im Texteditor öffnen

+
open ~/Library/Logs/GeoTurtl.log
+ +
+ + +

Wie deinstalliere ich GeoTurtl restlos?

+ +

Alles mit einem einzigen Befehl entfernen – einfach ins Terminal kopieren:

+ +
geoturtl_stop 2>/dev/null; \
+launchctl unload ~/Library/LaunchAgents/com.user.geoturtl.plist 2>/dev/null; \
+rm -f ~/Library/LaunchAgents/com.user.geoturtl.plist; \
+rm -rf /Applications/Scripte/python/GeoTurtl; \
+rm -f ~/Library/Logs/GeoTurtl.log; \
+rm -f ~/Library/Application\ Support/GeoTurtl.pid; \
+sed -i '' '/geoturtl/d' ~/.zshrc; \
+source ~/.zshrc; \
+echo "✓ GeoTurtl vollständig entfernt."
+ +

Dieser Befehl:

+ + +
+ + +

Haftungsausschluss

+ +
+

⚠️ Wichtiger Hinweis – Bitte vor der Installation lesen

+ +

+ GeoTurtl wurde von H. Linz nach bestem Wissen und Gewissen + entwickelt und kostenlos zur Verfügung gestellt. Dennoch gilt: +

+ +
    +
  • + Die Software wird „so wie sie ist" (as is) bereitgestellt – + ohne Garantie auf Fehlerfreiheit, ununterbrochenen Betrieb oder + Eignung für einen bestimmten Zweck. +
  • +
  • + H. Linz übernimmt keine Haftung für direkte oder indirekte + Schäden, Datenverluste oder sonstige Beeinträchtigungen, die durch die + Installation, Nutzung oder Deinstallation dieser Software entstehen könnten. +
  • +
  • + Die Nutzung von Drittdiensten wie ipinfo.io und + ipify.org zur IP-Ermittlung unterliegt den jeweiligen + Nutzungsbedingungen dieser Anbieter und liegt außerhalb der Verantwortung + des Autors. +
  • +
  • + Die Ortungsfunktion verwendet ausschließlich die von Apple bereitgestellte + CoreLocation-API. Die Genauigkeit der Standortangaben + hängt von der Verfügbarkeit von WLAN-Signalen ab und kann variieren. +
  • +
  • + Jede Person installiert und nutzt diese Software auf eigene + Gefahr und eigene Verantwortung. +
  • +
  • + Durch die Installation und Nutzung von GeoTurtl erkennst du diese + Bedingungen an. +
  • +
+ +

+ Diese Software steht unter der MIT-Lizenz. + Der vollständige Lizenztext ist im Programmordner enthalten. +

+
+ +
+ + +

Fragen, Anregungen oder konstruktive Kritik?

+ +

+ Du schickst mir eine Nachricht an + linz.on.tour@gmail.com. + Ich beantworte sie dann, so schnell es mir möglich ist. +

+ +

H. Linz

+ +
+ + + + +
+ + + + + + + + diff --git a/README.md b/README.md index 00e22f5..d229c1a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,195 @@ -# GeoTurtle +--- +title: "GeoTurtl README" +version: "0.9.7-beta" +date: 2026-06-07 +author: "H. Linz" +contact: "linz.on.tour@gmail.com" +license: "MIT" +tags: [geoturtl, ip, geolocation, standort, macOS, menubar] +draft: false +--- +# 🐢 GeoTurtl v0.9.7-beta + +**GeoTurtl** ist eine kleine, unauffällige App für deinen Mac. Sie sitzt als +Schildkröten-Symbol 🐢 in der Menüleiste und zeigt dir jederzeit auf Knopfdruck, +wo du gerade bist – und wie deine Verbindung ins Internet aussieht. + +> **Beta-Hinweis:** Dies ist eine Beta-Version (v0.9.6). Sie ist funktionsfähig, +> kann aber noch kleinere Fehler enthalten. Rückmeldungen sind willkommen! + +--- + +## Inhaltsverzeichnis + +- [Was ist GeoTurtl und was macht es?](#was-ist-geoturtl-und-was-macht-es) +- [Welche Installations- und Systemvoraussetzungen gibt es?](#welche-installations--und-systemvoraussetzungen-gibt-es) +- [Ist die Software sicher?](#ist-die-software-sicher) +- [Wie installiere ich GeoTurtl?](#wie-installiere-ich-geoturtl) +- [Wie starte und stoppe ich GeoTurtl?](#wie-starte-und-stoppe-ich-geoturtl) +- [Wo finde ich die Log-Dateien und wie rufe ich sie auf?](#wo-finde-ich-die-log-dateien-und-wie-rufe-ich-sie-auf) +- [Wie deinstalliere ich GeoTurtl restlos?](#wie-deinstalliere-ich-geoturtl-restlos) +- [Haftungsausschluss](#haftungsausschluss) +- [Was mache ich, wenn ich Fragen, Anregungen oder konstruktive Kritik habe?](#was-mache-ich-wenn-ich-fragen-anregungen-oder-konstruktive-kritik-habe) + +--- + +## Was ist GeoTurtl und was macht es? + +GeoTurtl zeigt dir zwei Dinge auf einen Blick: + +**Deinen Standort** +GeoTurtl nutzt die eingebaute Ortungsfunktion deines Macs (dieselbe Technik, +die auch Apple Maps verwendet) und zeigt dir deine aktuelle Adresse, die genauen +Koordinaten (Breiten- und Längengrad), deine Höhe über dem Meeresspiegel sowie +die Genauigkeit der Ortung an. Mit einem Klick öffnest du deinen Standort direkt +in Google Maps, Apple Maps oder OpenStreetMap. + +**Deine IP-Adressen** +GeoTurtl ermittelt deine öffentliche IPv4- und (falls vorhanden) IPv6-Adresse – +also die Adresse, unter der du im Internet sichtbar bist. Dazu zeigt es dir +den ungefähren Standort dieser Adresse und deinen Internetanbieter an. +Per Klick kannst du beide Adressen direkt in die Zwischenablage kopieren. + +GeoTurtl aktualisiert diese Informationen automatisch – du kannst selbst +einstellen, wie oft (alle 1, 5 oder 15 Minuten, oder gar nicht). + +--- + +## Welche Installations- und Systemvoraussetzungen gibt es? + +- Einen **Mac mit macOS** (ab macOS Ventura, optimiert für macOS Tahoe) +- **Python 3** – auf modernen Macs in der Regel bereits vorinstalliert +- Eine aktive **Internetverbindung** für die Installation +- **Aktivierte Ortungsdienste** auf deinem Mac + +Die Installation erledigt alles Weitere automatisch für dich. + +--- + +## Ist die Software sicher? + +Ja. GeoTurtl ist quelloffen (MIT-Lizenz) – du kannst den kompletten +Programmcode jederzeit selbst einsehen. Das Programm: + +- sendet **keine** Daten an Dritte außer den öffentlichen Ortungs- und + IP-Diensten (ipinfo.io, ipify.org) +- speichert **keine** persönlichen Daten dauerhaft +- läuft ausschließlich **lokal** auf deinem Mac +- ist **werbefrei** und kostenlos + +Die Ortungsdienste werden nur auf ausdrückliche Anfrage genutzt – +macOS fragt beim ersten Start nach deiner Erlaubnis. + +--- + +## Wie installiere ich GeoTurtl? + +Öffne das Terminal (Programme → Dienstprogramme → Terminal) und führe +diese zwei Befehle aus: + +**Schritt 1** – Datei ausführbar machen: +``` +chmod +x ~/Downloads/GeoTurtl_v0.9.7-beta/GeoTurtl_install_v0.9.7-beta.sh +``` + +**Schritt 2** – Installation starten: +``` +~/Downloads/GeoTurtl_v0.9.7-beta/GeoTurtl_install_v0.9.7-beta.sh +``` + +**Schritt 3** – Ortungsdienste erlauben: +Beim ersten Start erscheint eine macOS-Meldung. Klicke auf +**„Beim Verwenden erlauben"**. Falls die Meldung ausbleibt: +> Systemeinstellungen → Datenschutz & Sicherheit → Ortungsdienste +> → Terminal → „Beim Verwenden" + +--- + +## Wie starte und stoppe ich GeoTurtl? + +**Starten** – im Terminal: +``` +geoturtl_start +``` + +**Stoppen** – im Terminal: +``` +geoturtl_stop +``` + +GeoTurtl startet nach der Installation **automatisch** bei jedem Mac-Login. + +--- + +## Wo finde ich die Log-Dateien und wie rufe ich sie auf? + +GeoTurtl schreibt alle wichtigen Ereignisse in eine Log-Datei: +``` +~/Library/Logs/GeoTurtl.log +``` + +**Log live verfolgen:** +``` +tail -f ~/Library/Logs/GeoTurtl.log +``` + +**Letzte 50 Zeilen anzeigen:** +``` +tail -50 ~/Library/Logs/GeoTurtl.log +``` + +**Log im Texteditor öffnen:** +``` +open ~/Library/Logs/GeoTurtl.log +``` + +--- + +## Wie deinstalliere ich GeoTurtl restlos? + +Alles auf einmal entfernen – diesen Befehl ins Terminal kopieren: + +``` +geoturtl_stop 2>/dev/null; \ +launchctl unload ~/Library/LaunchAgents/com.user.geoturtl.plist 2>/dev/null; \ +rm -f ~/Library/LaunchAgents/com.user.geoturtl.plist; \ +rm -rf /Applications/Scripte/python/GeoTurtl; \ +rm -f ~/Library/Logs/GeoTurtl.log; \ +rm -f ~/Library/Application\ Support/GeoTurtl.pid; \ +sed -i '' '/geoturtl/d' ~/.zshrc; \ +source ~/.zshrc; \ +echo "✓ GeoTurtl vollständig entfernt." +``` + +--- + +## Haftungsausschluss + +GeoTurtl wurde von **H. Linz** nach bestem Wissen und Gewissen entwickelt +und kostenlos zur Verfügung gestellt. Dennoch gilt: + +- Die Software wird **„so wie sie ist" (as is)** bereitgestellt – ohne Garantie + auf Fehlerfreiheit, ununterbrochenen Betrieb oder Eignung für einen bestimmten Zweck. +- **H. Linz übernimmt keine Haftung** für direkte oder indirekte Schäden, + Datenverluste oder sonstige Beeinträchtigungen durch Installation, Nutzung + oder Deinstallation dieser Software. +- Die Nutzung von Drittdiensten (ipinfo.io, ipify.org) unterliegt den jeweiligen + Nutzungsbedingungen dieser Anbieter und liegt außerhalb der Verantwortung des Autors. +- **Jede Person installiert und nutzt diese Software auf eigene Gefahr.** +- Durch die Installation erkennst du diese Bedingungen an. + +Diese Software steht unter der **MIT-Lizenz**. + +--- + +## Was mache ich, wenn ich Fragen, Anregungen oder konstruktive Kritik habe? + +Du schickst mir eine Nachricht an **linz.on.tour@gmail.com**. Ich beantworte +sie dann, so schnell es mir möglich ist. + +**H. Linz** + +--- + +*GeoTurtl v0.9.7-beta · MIT License · © 2026 H. Linz*