Author SHA1 Message Date
ksmithandClaude Sonnet 5 df99c78998 Bump version to v2026.9.15
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 06:22:53 -06:00
ksmithandClaude Sonnet 5 dffca3afef Add Import/Export for profile lists
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>
2026-09-15 06:22:26 -06:00
ksmithandClaude Sonnet 5 4f8fa3272b packaging: pin vendored FreeRDP's own version, stop git-tag bleed-through
third_party/FreeRDP is vendored in-tree rather than as a separate git
submodule, so its build-time git_get_exact_tag() call (in
cmake/GetProjectVersion.cmake) was resolving against OrbitHub's own git
tags instead of any real FreeRDP release tag. Its version-extraction regex
then greedily matched the last three dot-separated numbers of our tag
(e.g. v2026.9.14.2 -> "9.14.2"), mislabeling FreeRDP's own libraries in
packaged builds (libfreerdp9.so.9.14.2 instead of the real
libfreerdp3.so.3.23.1) -- self-consistent within a build, but misleading
and drifting release to release.

Adding .source_tag makes GetProjectVersion.cmake take its file-based
branch (which it already prefers over the git-tag branch) instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 22:00:51 -06:00
ksmithandClaude Sonnet 5 96c8403f3b Bump version to v2026.9.14.2
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 21:57:43 -06:00
ksmithandClaude Sonnet 5 3e621219f1 RDP: fix resize corruption by proactively resizing the local GDI buffer
update->DesktopResize (the only place gdi_resize() was called) only fires
during a full Deactivation-Reactivation sequence or a GFX ResetGraphics
PDU, neither of which our Display Control channel resize path (SendMonitorLayout,
MS-RDPEDISP) triggers. The client's own display buffer was left stuck at
its initial-connect size for the rest of the session, and FreeRDP's surface-bits
handling silently drops updates outside those stale bounds -- producing
the missing/misplaced taskbar and stale composited-looking content.

sendDisplayResize now calls gdi_resize() itself right after a successful
SendMonitorLayout, rather than waiting on a callback that structurally
never fires for this channel/codec configuration.

Fixes #18.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 21:57:08 -06:00
ksmith e0969041a7 packaging: pin Flathub manifest to v2026.9.14 About-dialog-fix commit 2026-09-14 21:32:56 -06:00
10 changed files with 234 additions and 5 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.21) cmake_minimum_required(VERSION 3.21)
project(OrbitHub VERSION 2026.9.14 LANGUAGES CXX) project(OrbitHub VERSION 2026.9.15 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
+2 -2
View File
@@ -16,14 +16,14 @@ OrbitHub is in active development.
- Milestones completed: M0-M5, and M7-M9 - Milestones completed: M0-M5, and M7-M9
- Current milestone: Milestone 10 (v1.0 Stabilization) - Current milestone: Milestone 10 (v1.0 Stabilization)
- Deferred milestone: Milestone 6 (VNC Fully Working) - Deferred milestone: Milestone 6 (VNC Fully Working)
- Latest checkpoint tag: `v2026.9.14` - Latest checkpoint tag: `v2026.9.15`
- VNC implementation milestone (M6) is currently deferred - VNC implementation milestone (M6) is currently deferred
Progress and milestone details: Progress and milestone details:
- [docs/PROGRESS.md](docs/PROGRESS.md) - [docs/PROGRESS.md](docs/PROGRESS.md)
Latest release (installers for Windows, Linux, and macOS): Latest release (installers for Windows, Linux, and macOS):
- [v2026.9.14](https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.14) - [v2026.9.15](https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.15)
User Guide: User Guide:
- [docs/USER_GUIDE.md](docs/USER_GUIDE.md) (also available as a PDF attached to each release, and in-app via `Help -> User Guide`) - [docs/USER_GUIDE.md](docs/USER_GUIDE.md) (also available as a PDF attached to each release, and in-app via `Help -> User Guide`)
+2
View File
@@ -201,6 +201,8 @@ Git:
- Release: [v2026.9.8.2](https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.8.2) — same-day patch fixing RDP TLS certificate verification (was fully disabled) and preparing Flatpak packaging for Flathub submission - Release: [v2026.9.8.2](https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.8.2) — same-day patch fixing RDP TLS certificate verification (was fully disabled) and preparing Flatpak packaging for Flathub submission
- Release: [v2026.9.8.3](https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.8.3) — same-day patch adding an in-app User Guide and standalone User Guide PDF - Release: [v2026.9.8.3](https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.8.3) — same-day patch adding an in-app User Guide and standalone User Guide PDF
- Release: [v2026.9.14](https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.14) — fixes distorted RDP text on HiDPI monitors and reduces RDP resize-related display glitches - Release: [v2026.9.14](https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.14) — fixes distorted RDP text on HiDPI monitors and reduces RDP resize-related display glitches
- Release: [v2026.9.14.2](https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.14.2) — same-day patch fixing RDP display corruption (missing/misplaced taskbar) after resizing the session window (the client never resized its own display buffer for channel-driven RDP resizes)
- Release: [v2026.9.15](https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.15) — adds Import/Export for profile lists (File menu)
## Milestone 10 - v1.0 Stabilization ## Milestone 10 - v1.0 Stabilization
@@ -19,5 +19,5 @@ modules:
sources: sources:
- type: git - type: git
url: https://git.darksingularity.org/DarkSingularity/orbithub.git url: https://git.darksingularity.org/DarkSingularity/orbithub.git
tag: v2026.9.14 tag: v2026.9.15
commit: e90e9b5abfad585ce2ce6dc54453f110c6ed217f commit: dffca3afef80b5a3ca4832e0b7f748775cb90328
@@ -34,6 +34,18 @@
</screenshot> </screenshot>
</screenshots> </screenshots>
<releases> <releases>
<release version="2026.9.15" date="2026-09-15">
<url>https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.15</url>
<description>
<p>Adds Import/Export for profile lists (File menu).</p>
</description>
</release>
<release version="2026.9.14.2" date="2026-09-14">
<url>https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.14.2</url>
<description>
<p>Fixes RDP display corruption (missing/misplaced taskbar, stale composited content) after resizing the session window.</p>
</description>
</release>
<release version="2026.9.14" date="2026-09-14"> <release version="2026.9.14" date="2026-09-14">
<url>https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.14</url> <url>https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.14</url>
<description> <description>
+177
View File
@@ -7,8 +7,13 @@
#include <QAction> #include <QAction>
#include <QAbstractItemView> #include <QAbstractItemView>
#include <QComboBox> #include <QComboBox>
#include <QFile>
#include <QFileDialog>
#include <QHeaderView> #include <QHeaderView>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QLabel> #include <QLabel>
#include <QLineEdit> #include <QLineEdit>
#include <QInputDialog> #include <QInputDialog>
@@ -85,6 +90,53 @@ bool profileHasTag(const Profile& profile, const QString& requestedTag)
return false; return false;
} }
constexpr int kProfileExportFormatVersion = 1;
QJsonObject profileToJson(const Profile& profile)
{
QJsonObject object;
object.insert(QStringLiteral("name"), profile.name);
object.insert(QStringLiteral("host"), profile.host);
object.insert(QStringLiteral("port"), profile.port);
object.insert(QStringLiteral("username"), profile.username);
object.insert(QStringLiteral("domain"), profile.domain);
object.insert(QStringLiteral("folderPath"), profile.folderPath);
object.insert(QStringLiteral("protocol"), profile.protocol);
object.insert(QStringLiteral("authMode"), profile.authMode);
object.insert(QStringLiteral("privateKeyPath"), profile.privateKeyPath);
object.insert(QStringLiteral("knownHostsPolicy"), profile.knownHostsPolicy);
object.insert(QStringLiteral("rdpSecurityMode"), profile.rdpSecurityMode);
object.insert(QStringLiteral("rdpPerformanceProfile"), profile.rdpPerformanceProfile);
object.insert(QStringLiteral("tags"), profile.tags);
return object;
}
// Deliberately excludes id (import always creates new rows -- an imported
// profile's id has no meaning in the destination database) and any
// credential material (none is ever persisted on Profile in the first
// place, see ProfileRepository).
Profile profileFromJson(const QJsonObject& object)
{
Profile profile;
profile.name = object.value(QStringLiteral("name")).toString();
profile.host = object.value(QStringLiteral("host")).toString();
profile.port = object.value(QStringLiteral("port")).toInt(22);
profile.username = object.value(QStringLiteral("username")).toString();
profile.domain = object.value(QStringLiteral("domain")).toString();
profile.folderPath = object.value(QStringLiteral("folderPath")).toString();
profile.protocol = object.value(QStringLiteral("protocol")).toString(QStringLiteral("SSH"));
profile.authMode = object.value(QStringLiteral("authMode")).toString(QStringLiteral("Password"));
profile.privateKeyPath = object.value(QStringLiteral("privateKeyPath")).toString();
profile.knownHostsPolicy =
object.value(QStringLiteral("knownHostsPolicy")).toString(QStringLiteral("Ask"));
profile.rdpSecurityMode =
object.value(QStringLiteral("rdpSecurityMode")).toString(QStringLiteral("Negotiate"));
profile.rdpPerformanceProfile =
object.value(QStringLiteral("rdpPerformanceProfile")).toString(QStringLiteral("Balanced"));
profile.tags = object.value(QStringLiteral("tags")).toString();
return profile;
}
} }
ProfilesWindow::ProfilesWindow(QWidget* parent) ProfilesWindow::ProfilesWindow(QWidget* parent)
@@ -930,3 +982,128 @@ void ProfilesWindow::createFolderInCurrentContext()
const QString folderPath = folderPathForItem(m_profilesTree->currentItem()); const QString folderPath = folderPathForItem(m_profilesTree->currentItem());
createFolderInContext(folderPath); createFolderInContext(folderPath);
} }
void ProfilesWindow::exportProfiles()
{
const QString fileName = QFileDialog::getSaveFileName(
this, QStringLiteral("Export Profiles"), QStringLiteral("orbithub-profiles.json"),
QStringLiteral("JSON Files (*.json)"));
if (fileName.isEmpty()) {
return;
}
QJsonArray folders;
for (const QString& folderPath : m_repository->listFolders()) {
folders.append(folderPath);
}
QJsonArray profiles;
for (const Profile& profile : m_repository->listProfiles()) {
profiles.append(profileToJson(profile));
}
QJsonObject root;
root.insert(QStringLiteral("orbithubProfileExport"), kProfileExportFormatVersion);
root.insert(QStringLiteral("folders"), folders);
root.insert(QStringLiteral("profiles"), profiles);
QFile file(fileName);
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
QMessageBox::warning(this,
QStringLiteral("Export Profiles"),
QStringLiteral("Failed to write %1: %2")
.arg(fileName, file.errorString()));
return;
}
file.write(QJsonDocument(root).toJson(QJsonDocument::Indented));
file.close();
QMessageBox::information(this,
QStringLiteral("Export Profiles"),
QStringLiteral("Exported %1 profile(s) to %2.")
.arg(profiles.size())
.arg(fileName));
}
void ProfilesWindow::importProfiles()
{
const QString fileName = QFileDialog::getOpenFileName(
this, QStringLiteral("Import Profiles"), QString(), QStringLiteral("JSON Files (*.json)"));
if (fileName.isEmpty()) {
return;
}
QFile file(fileName);
if (!file.open(QIODevice::ReadOnly)) {
QMessageBox::warning(this,
QStringLiteral("Import Profiles"),
QStringLiteral("Failed to read %1: %2")
.arg(fileName, file.errorString()));
return;
}
QJsonParseError parseError{};
const QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &parseError);
file.close();
if (parseError.error != QJsonParseError::NoError || !doc.isObject()) {
QMessageBox::warning(this,
QStringLiteral("Import Profiles"),
QStringLiteral("%1 is not a valid OrbitHub profile export: %2")
.arg(fileName, parseError.errorString()));
return;
}
const QJsonObject root = doc.object();
if (!root.value(QStringLiteral("profiles")).isArray()) {
QMessageBox::warning(this,
QStringLiteral("Import Profiles"),
QStringLiteral("%1 does not contain a profile list.").arg(fileName));
return;
}
for (const QJsonValue& folderValue : root.value(QStringLiteral("folders")).toArray()) {
const QString folderPath = folderValue.toString();
if (!folderPath.isEmpty()) {
m_repository->createFolder(folderPath);
}
}
const QJsonArray profilesArray = root.value(QStringLiteral("profiles")).toArray();
int imported = 0;
QStringList failures;
for (const QJsonValue& profileValue : profilesArray) {
if (!profileValue.isObject()) {
continue;
}
const Profile profile = profileFromJson(profileValue.toObject());
if (profile.name.isEmpty() || profile.host.isEmpty()) {
failures.push_back(QStringLiteral("(unnamed profile): missing name or host"));
continue;
}
// No need to createFolder(profile.folderPath) here: the tree view
// already synthesizes folder nodes from a profile's own folderPath
// (see ProfilesWindow::loadProfiles). Explicit profile_folders rows
// are only for folders with no profiles in them, and those are
// already recreated above from the export's top-level "folders"
// list -- doing it again per-profile would just add spurious
// entries not present in the original export.
if (m_repository->createProfile(profile).has_value()) {
++imported;
} else {
failures.push_back(QStringLiteral("%1: %2").arg(profile.name, m_repository->lastError()));
}
}
loadProfiles();
QString summary = QStringLiteral("Imported %1 of %2 profile(s) from %3.")
.arg(imported)
.arg(profilesArray.size())
.arg(fileName);
if (!failures.isEmpty()) {
summary += QStringLiteral("\n\nFailed:\n%1").arg(failures.join(QChar::fromLatin1('\n')));
QMessageBox::warning(this, QStringLiteral("Import Profiles"), summary);
} else {
QMessageBox::information(this, QStringLiteral("Import Profiles"), summary);
}
}
+2
View File
@@ -32,6 +32,8 @@ public:
void createProfileInCurrentContext(); void createProfileInCurrentContext();
void createFolderInCurrentContext(); void createFolderInCurrentContext();
void exportProfiles();
void importProfiles();
signals: signals:
void connectRequested(const Profile& profile); void connectRequested(const Profile& profile);
+24
View File
@@ -1933,6 +1933,30 @@ bool RdpSessionBackend::sendDisplayResize(rdp_freerdp* instance, int width, int
m_lastResizeHeight = height; m_lastResizeHeight = height;
m_lastResizeScale = static_cast<int>(scaleValue); m_lastResizeScale = static_cast<int>(scaleValue);
// The Display Control channel (MS-RDPEDISP) has no server
// acknowledgment PDU, and real hosts apply the new resolution
// without a Deactivation-Reactivation sequence — so
// update->DesktopResize (which only fires for that sequence, or
// for the GFX/Progressive pipeline we don't use) never runs for
// a channel-driven resize. Without a matching gdi_resize() call,
// gdi->width/height stay at the old size, and FreeRDP's own
// surface-bits handling (intersect_rect in gdi.c) then silently
// *drops* any update reaching outside those stale bounds —
// which is what produced the missing/misplaced taskbar and
// stale composited-looking content: this call is the fix, not
// just a best-effort nudge.
if (instance->context->gdi != nullptr
&& gdi_resize(instance->context->gdi, static_cast<UINT32>(width),
static_cast<UINT32>(height))) {
emit remoteDesktopSizeChanged(width, height);
} else if (instance->context->gdi != nullptr) {
emit eventLogged(QStringLiteral(
"RDP warning: local resize to %1x%2 failed; display may show stale content "
"until the next full repaint.")
.arg(width)
.arg(height));
}
// Best-effort nudge: some hosts don't fully repaint their own // Best-effort nudge: some hosts don't fully repaint their own
// desktop after a resolution change (observed: taskbar missing // desktop after a resolution change (observed: taskbar missing
// until something else forces a redraw). Explicitly asking for // until something else forces a redraw). Explicitly asking for
+11
View File
@@ -170,6 +170,9 @@ SessionWindow::SessionWindow(QWidget* parent)
QAction* newProfileAction = fileMenu->addAction(QStringLiteral("New Profile")); QAction* newProfileAction = fileMenu->addAction(QStringLiteral("New Profile"));
QAction* newFolderAction = fileMenu->addAction(QStringLiteral("New Folder")); QAction* newFolderAction = fileMenu->addAction(QStringLiteral("New Folder"));
fileMenu->addSeparator(); fileMenu->addSeparator();
QAction* importProfilesAction = fileMenu->addAction(QStringLiteral("Import Profiles..."));
QAction* exportProfilesAction = fileMenu->addAction(QStringLiteral("Export Profiles..."));
fileMenu->addSeparator();
QAction* quitAction = fileMenu->addAction(QStringLiteral("Quit")); QAction* quitAction = fileMenu->addAction(QStringLiteral("Quit"));
connect(newProfileAction, connect(newProfileAction,
@@ -180,6 +183,14 @@ SessionWindow::SessionWindow(QWidget* parent)
&QAction::triggered, &QAction::triggered,
this, this,
[this]() { m_profilesWidget->createFolderInCurrentContext(); }); [this]() { m_profilesWidget->createFolderInCurrentContext(); });
connect(importProfilesAction,
&QAction::triggered,
this,
[this]() { m_profilesWidget->importProfiles(); });
connect(exportProfilesAction,
&QAction::triggered,
this,
[this]() { m_profilesWidget->exportProfiles(); });
connect(quitAction, &QAction::triggered, this, []() { qApp->quit(); }); connect(quitAction, &QAction::triggered, this, []() { qApp->quit(); });
QMenu* helpMenu = menuBar()->addMenu(QStringLiteral("Help")); QMenu* helpMenu = menuBar()->addMenu(QStringLiteral("Help"));
+1
View File
@@ -0,0 +1 @@
3.23.1-dev0