From ebb838b6b1a1e0f9956d0c080d8e0d0fc4e6fcc9 Mon Sep 17 00:00:00 2001 From: x3 Date: Mon, 21 Sep 2026 23:29:39 +0200 Subject: [PATCH] Editor: Passwort unter Benutzername, Live-Validierung, Fehlerbericht kopieren, Doku --- .../RemminaMac/Models/ProfileValidation.swift | 38 ++++++++++++++++++ .../Views/EmbeddedTerminalView.swift | 17 ++++++++ .../RemminaMac/Views/ProfileEditorView.swift | 40 +++++++++++++------ docs/ERRORS.md | 12 ++++++ docs/USAGE.md | 7 ++-- 5 files changed, 98 insertions(+), 16 deletions(-) create mode 100644 Sources/RemminaMac/Models/ProfileValidation.swift diff --git a/Sources/RemminaMac/Models/ProfileValidation.swift b/Sources/RemminaMac/Models/ProfileValidation.swift new file mode 100644 index 0000000..b7985ff --- /dev/null +++ b/Sources/RemminaMac/Models/ProfileValidation.swift @@ -0,0 +1,38 @@ +import Foundation + +/// Eingabe-Prüfung + Bereinigung für Profile (reine Funktionen, testbar). +/// Fängt die häufigsten „Verbindung falsch"-Ursachen ab: Leerzeichen, +/// user@host im Host-Feld, Protokoll-Präfix, leerer Benutzername. +enum ProfileValidation { + static func cleaned(_ profile: ConnectionProfile) -> ConnectionProfile { + var p = profile + p.name = p.name.trimmingCharacters(in: .whitespacesAndNewlines) + p.host = p.host.trimmingCharacters(in: .whitespacesAndNewlines) + p.username = p.username.trimmingCharacters(in: .whitespacesAndNewlines) + p.group = p.group.trimmingCharacters(in: .whitespacesAndNewlines) + p.resolution = p.resolution.trimmingCharacters(in: .whitespacesAndNewlines) + p.extraArgs = p.extraArgs.trimmingCharacters(in: .whitespacesAndNewlines) + return p + } + + static func warnings(for profile: ConnectionProfile) -> [String] { + var w: [String] = [] + let h = profile.host.trimmingCharacters(in: .whitespacesAndNewlines) + if h.contains("@") { + w.append("Host enthält „@“ — den Benutzernamen ins Feld darunter eintragen, hier nur Hostname/IP.") + } + if h.contains("://") { + w.append("Host enthält „://“ — nur Hostname/IP, kein Protokoll (das steht oben).") + } + if h.contains(where: { $0.isWhitespace }) { + w.append("Host enthält Leerzeichen.") + } + if profile.username.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + w.append("Benutzername fehlt.") + } + if !(1...65535).contains(profile.port) { + w.append("Port ungültig (1–65535).") + } + return w + } +} diff --git a/Sources/RemminaMac/Views/EmbeddedTerminalView.swift b/Sources/RemminaMac/Views/EmbeddedTerminalView.swift index c70dae3..c91207a 100644 --- a/Sources/RemminaMac/Views/EmbeddedTerminalView.swift +++ b/Sources/RemminaMac/Views/EmbeddedTerminalView.swift @@ -43,6 +43,9 @@ struct EmbeddedTerminalView: View { .lineLimit(3) } Spacer() + Button("Bericht kopieren") { copyReport() } + .buttonStyle(.link).font(.caption) + .help("Kopiert Profil (ohne Passwort), Status und letzte Ausgabe — hierher schicken zur Fehlersuche") Button("Diagnose") { diagnose() }.buttonStyle(.link).font(.caption) Button("Erneut") { connect() }.buttonStyle(.link).font(.caption) } @@ -129,6 +132,20 @@ struct EmbeddedTerminalView: View { input = "" } + /// Fehlerbericht ohne Secrets (Passwort steht weder im Profil noch im Befehl). + /// Hinweis: Wer das Passwort sichtbar eingetippt hat (nicht Auge-Modus), + /// löscht die Ausgabe vorher per „Leeren". + func copyReport() { + var r = "RemminaMac Fehlerbericht\n" + r += "Profil: \(profile.name) | \(profile.proto.rawValue) | \(profile.username)@\(profile.host):\(profile.port)\n" + r += "Auto-Login: \(term.autoLogin ? "ja" : "nein") | sshpass: \(SSHService.findSshpass() ?? "fehlt")\n" + r += "Status: \(statusText)\n" + let tail = term.lines.suffix(20).map { $0.map(\.text).joined() }.joined(separator: "\n") + r += "--- letzte Ausgabe ---\n\(tail)\n" + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(r, forType: .string) + } + var failed: Bool { if case .done(let code, _) = term.state { return (code ?? 1) != 0 diff --git a/Sources/RemminaMac/Views/ProfileEditorView.swift b/Sources/RemminaMac/Views/ProfileEditorView.swift index 8bec7f0..4b4486e 100644 --- a/Sources/RemminaMac/Views/ProfileEditorView.swift +++ b/Sources/RemminaMac/Views/ProfileEditorView.swift @@ -16,6 +16,8 @@ struct ProfileEditorView: View { self.onSave = onSave } + var liveWarnings: [String] { ProfileValidation.warnings(for: profile) } + var body: some View { VStack(spacing: 0) { Text(isNew ? "Neues Profil" : "Profil bearbeiten").font(.headline).padding() @@ -37,26 +39,37 @@ struct ProfileEditorView: View { } Section("Verbindung") { TextField("Host", text: $profile.host) + .autocorrectionDisabled() TextField("Port", value: $profile.port, format: .number) TextField("Benutzername", text: $profile.username) - TextField("Auflösung (RDP, z. B. 1920x1080 oder Automatisch)", text: $profile.resolution) - TextField("Extra-Argumente", text: $profile.extraArgs) - } - Section("Authentifizierung") { - Picker("SSH-Key", selection: $profile.sshKeyID) { - Text("Kein Key").tag(UUID?.none) - ForEach(store.sshKeys) { k in Text(k.name).tag(UUID?.some(k.id)) } - } + .autocorrectionDisabled() + // Passwort direkt unter dem Benutzernamen (Keychain-gespeichert) if storedPasswordPresent && password.isEmpty { HStack { Image(systemName: "checkmark.circle.fill").foregroundStyle(.green) - Text("Passwort in Keychain gespeichert") + Text("Passwort gespeichert") Spacer() Button("Löschen", role: .destructive) { deleteStoredPassword() } } } - SecureField(storedPasswordPresent ? "Neues Passwort …" : "Passwort (optional, Keychain)", text: $password) + SecureField(storedPasswordPresent ? "Neues Passwort …" : "Passwort", text: $password) Toggle("Passwort in Keychain speichern", isOn: $savePassword) + TextField("Auflösung (RDP, z. B. 1920x1080 oder Automatisch)", text: $profile.resolution) + TextField("Extra-Argumente", text: $profile.extraArgs) + if !liveWarnings.isEmpty { + ForEach(liveWarnings, id: \.self) { w in + HStack(spacing: 6) { + Image(systemName: "exclamationmark.triangle").foregroundStyle(.orange) + Text(w).font(.caption).foregroundStyle(.orange) + } + } + } + } + Section("SSH-Key (optional)") { + Picker("SSH-Key", selection: $profile.sshKeyID) { + Text("Kein Key (Passwort-Login)").tag(UUID?.none) + ForEach(store.sshKeys) { k in Text(k.name).tag(UUID?.some(k.id)) } + } Text(sshpassHint).font(.caption).foregroundStyle(.secondary) } } @@ -66,7 +79,7 @@ struct ProfileEditorView: View { Button("Abbrechen", role: .cancel) { dismiss() } Spacer() Button("Speichern") { - var toSave = profile + var toSave = ProfileValidation.cleaned(profile) if toSave.port == 0 { toSave.port = toSave.proto.defaultPort } if toSave.passwordAccount == nil { toSave.passwordAccount = "profile-\(toSave.id.uuidString)" } if savePassword, !password.isEmpty, let acct = toSave.passwordAccount { @@ -76,11 +89,12 @@ struct ProfileEditorView: View { dismiss() } .keyboardShortcut(.defaultAction) - .disabled(profile.host.isEmpty || profile.name.isEmpty) + .disabled(profile.name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + || profile.host.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) } .padding() } - .frame(width: 520, height: 640) + .frame(width: 520, height: 680) .onAppear { if let acct = profile.passwordAccount, let stored = KeychainHelper.load(account: acct), !stored.isEmpty { diff --git a/docs/ERRORS.md b/docs/ERRORS.md index c176c23..55648ef 100644 --- a/docs/ERRORS.md +++ b/docs/ERRORS.md @@ -18,6 +18,7 @@ Stand: 2026-09-21 · Version 0.1.0 · macOS arm64 · Swift 5.9+ · Xcode 15+ | 9 | 2026-09-21 | `swift build` | `Result`: String conformt nicht zu Error (Folgefehler: DispatchQueue-Overload) | blockierend | ✅ behoben — Completion als `(String?) -> Void` | | 10 | 2026-09-21 | Icon-Generator (`RenderIcon.swift`) | 3 Swift-Fehler: `guard let x = f() ?? g()` (nicht optional), `Int * CGFloat`, falscher Preview-Pfad | klein | ✅ behoben | | 11 | 2026-09-21 | Passwort-Feature | Wunsch: Passwort + Keys speichern UND nutzen (bisher nur gespeichert, nie benutzt) | Feature | ✅ umgesetzt — Auto-Login via sshpass, sonst interaktiv | +| 12 | 2026-09-21 | „Immer Verbindung falsch" | User-Report ohne Details (Ziel/Credentials unbekannt) | offen | ⬜ Teils: Editor-Validierung + kopierbarer Fehlerbericht; warte auf Bericht | --- @@ -164,3 +165,14 @@ Oder mit Token als Passwort: `git push https://:@git.sysdaemon.xyz **Lokaler Stand:** Commit `cd834dc` + `dist/RemminaMac-0.1.0-macos-arm64.zip` sind fertig und warteten nur auf den Push. **Nachtrag 2026-09-21:** ✅ behoben — mit korrigierten Credentials: `GET /api/v1/user → 200 (login x3)`, Repo `x3/remmina-mac` erstellt (201), `git push -u origin main` ok, Release `v0.1.0` (id 1) + 2 Assets (ZIP + SHA256) hochgeladen. **Status:** ✅ behoben. + +## #12 — „Immer Verbindung falsch" (User-Report, Details offen) +**Stand:** Gemeldet ohne Ziel-Host, Banner-Text oder Credentials — aus der Ferne nicht reproduzierbar. Systematische App-Fehler als Ursache weitgehend ausgeschlossen (PTY/ssh-Pfad, Fehlertext-Anzeige, TCP-Check und Keychain-Fluss sind per Harness verifiziert). +**Häufigste reale Ursachen (Checkliste):** +1. `user@host` im Host-Feld oder `ssh://`-Präfix → jetzt Live-Warnung im Editor +2. Leerzeichen/Vertipper in Host/User → wird jetzt getrimmt + gewarnt +3. Falsches Passwort / Key ohne Passphrase-Eintrag → Banner + Diagnose nutzen +4. Server blockt (Fail2ban, nur Key-Login erlaubt) → Server-Logs prüfen +**Umgesetzt:** `Models/ProfileValidation.swift` (trimmen + Warnungen, Harness 7/7), Passwort direkt unter Benutzername, „Bericht kopieren" im Fehler-Banner (ohne Secrets). +**Offen:** Bitte Banner-Text oder kopierten Bericht hierher schicken — dann steht die Ursache in einer Zeile. +**Status:** ⬜ wartet auf Fehlerbericht. diff --git a/docs/USAGE.md b/docs/USAGE.md index 213df77..5ab957f 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -7,10 +7,11 @@ - **Toolbar:** `+` Neu, Stift Bearbeiten, Kopie-Symbol Duplizieren, Schlüssel Keys, `▶` Verbinden, `⚡` Quick-Connect. ## Profil anlegen -1. `+` → Name, Gruppe, Protokoll (SSH/SFTP/RDP/VNC), Host, Port, User ausfüllen. -2. SSH-Key wählen (vorher unter Schlüssel-Icon importieren). -3. Optional Passwort → Haken „in Keychain speichern“. +1. `+` → Name, Gruppe, Protokoll (SSH/SFTP/RDP/VNC), Host, Port, Benutzername, **Passwort direkt darunter** ausfüllen (Keychain-Haken setzen zum Speichern). +2. Der Editor warnt live bei typischen Fehlern: `user@host` im Host-Feld, `ssh://`-Präfix, Leerzeichen, leerer Benutzername, ungültiger Port. Beim Speichern wird getrimmt. +3. SSH-Key wählen (optional, Bereich darunter — vorher unter Schlüssel-Icon importieren). 4. Speichern → liegt sofort in `profiles.json`. +5. Schlägt eine Verbindung fehl: rotes Banner → **Bericht kopieren** und zur Fehlersuche mitschicken (enthält kein Passwort). ## Verbinden - Profil wählen → `▶ Verbinden` oder Doppelklick.