Parses mRemoteNG's confCons.xml export format directly -- schema verified
against mRemoteNG's own XmlConnectionsDeserializer.cs source and a real
exported sample, not guessed at. Maps nested <Node Type="Container">
folders and <Node Type="Connection"> entries onto OrbitHub profiles:
RDP stays RDP, SSH1/SSH2 collapse to OrbitHub's single SSH protocol,
anything else (VNC, Telnet, HTTP, PowerShell, ...) is skipped and listed
in the import summary rather than silently dropped.
Passwords are never read, not even for the common case where they're
technically readable without a master password (mRemoteNG only encrypts
the Password attribute itself, everything else -- Hostname, Username,
Domain, Protocol -- is plaintext). A FullFileEncryption="true" export
encrypts the whole node tree instead and genuinely can't be read without
the user's master password; that case is detected and refused with a
clear message rather than failing confusingly.
The parser (src/mremoteng_importer.h/.cpp) is a pure function decoupled
from any file/UI I/O, matching this session's established pattern of
keeping business logic separately testable from the Qt Widgets shell that
calls it (ProfilesWindow::importFromMRemoteNG() is the thin wrapper:
QFileDialog, call the parser, write results via ProfileRepository, show a
summary). 11 test cases against realistic sample XML.
Hit a real moc gotcha along the way: a literal "//" inside a raw string
literal (the xmlns URL) makes moc's lexer think a line comment started
there, silently desyncing its parse so it never finds the QObject-derived
test class at all (no error, just a missing vtable at link time). Fixed
by moving the XML fixtures into a plain non-QObject header moc never
scans, split across two adjacent literals as a second safeguard.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ProfileRepository::deleteFolder() removes a folder without ever deleting
the profiles or subfolders inside it -- everything directly under the
deleted folder shifts up to take its place (its parent, or the top level
if it had none), exactly as if that one path segment were removed from
each affected path. This is a labels-only operation (a "folder" is just a
grouping string on each profile, not a container that owns them), so
that's the least-surprising behavior versus silently bulk-deleting saved
connections.
Right-clicking a folder in the tree now offers "Delete Folder"; if it
isn't empty, a confirmation states exactly how many profiles/subfolders
will move and to where.
Covered by 7 new unit tests, which caught the same class of bug fixed in
3fab2f9: the new code's own folder-path remap also bound an unguarded
null QString (a folder moving to root) against the `folder_path NOT
NULL` column.
Fixes#20.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
File -> Export Profiles... writes all profiles (plus explicit, including
empty, folders) to a JSON file. File -> Import Profiles... reads one back,
recreates folders, and inserts profiles as new rows so IDs never collide
with the destination database. No credentials are ever persisted on a
Profile in the first place (only privateKeyPath, a filesystem path), so
nothing sensitive is exposed by an exported file.
Verified with a standalone headless round-trip test against isolated
app-data databases (SSH + RDP profiles, nested folders, all fields);
caught and fixed a bug where import was redundantly creating an explicit
folder row per profile that didn't exist in the original export.
Fixes#17.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ProfilesWindow and SessionWindow were two independent top-level windows,
coordinated through a QPointer and manual show/create-or-reuse logic, with
duplicated Help menus and a path where Quit didn't actually quit if a
SessionWindow happened to be open. ProfilesWindow is now an embedded QWidget
shown as a permanent, unclosable "Profiles" tab inside SessionWindow's tab
widget; SessionWindow is the app's sole top-level window. Double-clicking a
profile opens a session tab in the same window instead of finding-or-creating
a second one, and closing all session tabs falls back to the Profiles tab
rather than closing the app.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>