Compare commits

...

8 Commits

Author SHA1 Message Date
Ian Chua
b37e009c24 fix: propagate force push down put_setting 2026-05-29 18:52:00 +08:00
SoftFever
835d8fb3bb Merge branch 'main' into feature/409-resolutions 2026-05-29 17:26:15 +08:00
Ian Chua
5301e2963e remove formatting churn 2026-05-29 14:41:29 +08:00
Ian Chua
4457d6d061 Merge branch 'feature/409-resolutions' of https://github.com/OrcaSlicer/OrcaSlicer into feature/409-resolutions 2026-05-29 12:08:11 +08:00
Ian Chua
d82cb9d338 fix: pass force push flag to start_sync_user_preset 2026-05-29 12:07:35 +08:00
SoftFever
c33f0b5513 Merge branch 'main' into feature/409-resolutions 2026-05-28 23:15:05 +08:00
Ian Chua
9451827e33 fix: silently log other http errors 2026-05-28 18:09:25 +08:00
Ian Chua
2fe0509359 fix: 409 conflicts resolution in notifications 2026-05-28 17:59:44 +08:00
11 changed files with 221 additions and 54 deletions

View File

@@ -12,6 +12,7 @@
#include <boost/chrono/duration.hpp>
#include <boost/log/detail/native_typeof.hpp>
#include <libslic3r/Config.hpp>
#include <mutex>
#include <wx/event.h>
// Localization headers: include libslic3r version first so everything in this file
@@ -4812,6 +4813,8 @@ void GUI_App::handle_http_error(unsigned int status, std::string body, const std
wxQueueEvent(this, evt);
}
static std::mutex conflict_ids_mutex;
void GUI_App::on_http_error(wxCommandEvent &evt)
{
int status = evt.GetInt();
@@ -4887,32 +4890,65 @@ void GUI_App::on_http_error(wxCommandEvent &evt)
return;
}
static bool m_is_error_shown = false;
if (status == 409 && provider == ORCA_CLOUD_PROVIDER) {
BOOST_LOG_TRIVIAL(info) << "Http error 409.";
// Parse the conflict body to extract the error code and server profile id
int conflict_code = 0;
json conflict_body;
try {
conflict_body = json::parse(body_str);
if (conflict_body.contains("code"))
conflict_code = conflict_body["code"].get<int>();
} catch (...) {
BOOST_LOG_TRIVIAL(warning) << "Failed to parse 409 conflict body.";
}
std::string conflict_setting_id;
if (conflict_body.contains("server_profile") && conflict_body["server_profile"].contains("id"))
conflict_setting_id = conflict_body["server_profile"]["id"].get<std::string>();
auto* plater = wxGetApp().plater();
if (plater != nullptr && wxGetApp().imgui()->display_initialized()) {
std::string text;
if (conflict_code == -1) {
text = _u8L("Cloud sync conflict: this preset has a newer version in OrcaCloud.\n"
"Pull downloads the cloud copy. Force push overwrites it with your local preset.");
} else {
text = _u8L("Cloud sync conflict: a preset with this name already exists in OrcaCloud.\n"
"Pull downloads the cloud copy. Force push overwrites it with your local preset.");
}
plater->get_notification_manager()->push_orca_sync_conflict_notification(
text,
[this](wxEvtHandler*) {
std::thread([this]() {
if (is_closing() || !m_agent || !preset_bundle)
return;
BOOST_LOG_TRIVIAL(info) << "Pulling Orca Cloud settings to resolve sync conflict.";
restart_sync_user_preset();
}).detach();
return true;
},
[this, conflict_setting_id](wxEvtHandler*) {
MessageDialog
dlg(mainframe,
_L("Force push will overwrite the cloud copy with your local preset changes.\nDo you want to continue?"),
_L("Resolve cloud sync conflict"), wxCENTER | wxYES_NO | wxNO_DEFAULT | wxICON_WARNING);
if (dlg.ShowModal() != wxID_YES)
return false;
if (!conflict_setting_id.empty()) {
std::unique_lock lock(conflict_ids_mutex);
m_pending_conflict_setting_ids.push_back(conflict_setting_id);
}
return true;
});
}
return;
}
// Show general error notification for Orca Cloud API failures (not Bambu)
if (provider == ORCA_CLOUD_PROVIDER && status >= 400 && code != HttpErrorVersionLimited) {
wxString msg;
if (!error.empty()) {
msg = wxString::Format(_L("Failed to connect to OrcaCloud.\nPlease check your network connectivity\n(HTTP %u): %s"), status, wxString::FromUTF8(error));
} else {
msg = wxString::Format(_L("Failed to connect to OrcaCloud.\nPlease check your network connectivity\n(HTTP %u)"), status);
}
if (app_config->get_bool("developer_mode")) {
// Use notification manager if ImGui is ready; fall back to wxMessageBox on Linux
// where ImGui may not be initialized until the user switches to the Prepare tab.
if (wxGetApp().plater() != nullptr && wxGetApp().imgui()->display_initialized()) {
wxGetApp()
.plater()
->get_notification_manager()
->push_notification(NotificationType::PlaterError, NotificationManager::NotificationLevel::WarningNotificationLevel,
msg.ToUTF8().data());
}
}
if (!m_is_error_shown) {
m_is_error_shown = true;
wxMessageBox(msg, _L("Cloud Error"), wxOK | wxICON_ERROR, wxGetApp().mainframe);
}
BOOST_LOG_TRIVIAL(warning) << "API call to OrcaCloud failed with status=" << status;
}
}
@@ -6181,13 +6217,14 @@ void GUI_App::load_pending_vendors()
need_add_filaments.clear();
}
void GUI_App::sync_preset(Preset* preset)
void GUI_App::sync_preset(Preset* preset, bool force)
{
int result = -1;
unsigned int http_code = 200;
std::string updated_info;
long long update_time = 0;
// only sync user's preset
if (!m_agent) return;
if (!preset->is_user()) return;
auto setting_id = preset->setting_id;
@@ -6259,9 +6296,9 @@ void GUI_App::sync_preset(Preset* preset)
result = 0;
}
else {
result = m_agent->put_setting(setting_id, preset->name, &values_map, &http_code);
result = m_agent->put_setting(setting_id, preset->name, &values_map, &http_code, ORCA_CLOUD_PROVIDER, force);
if (http_code >= 400) {
result = 0;
result = 0;
updated_info = "hold";
BOOST_LOG_TRIVIAL(error) << "[sync_preset] put setting_id = " << setting_id << " failed, http_code = " << http_code;
} else {
@@ -6733,9 +6770,24 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg)
int total_count = 0;
sync_count = preset_bundle->prints.get_user_presets(preset_bundle, presets_to_sync);
auto sync_with_lock = [this](Preset& preset) {
bool force = false;
{
std::scoped_lock lock(conflict_ids_mutex);
auto it = std::find_if(m_pending_conflict_setting_ids.begin(), m_pending_conflict_setting_ids.end(),
[&preset](const std::string& id) { return id == preset.setting_id; });
if (it != m_pending_conflict_setting_ids.end()) {
force = true;
m_pending_conflict_setting_ids.erase(it);
}
}
sync_preset(&preset, force);
};
if (sync_count > 0) {
for (Preset& preset : presets_to_sync) {
sync_preset(&preset);
sync_with_lock(preset);
boost::this_thread::sleep_for(boost::chrono::milliseconds(100));
}
}
@@ -6744,7 +6796,7 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg)
sync_count = preset_bundle->filaments.get_user_presets(preset_bundle, presets_to_sync);
if (sync_count > 0) {
for (Preset& preset : presets_to_sync) {
sync_preset(&preset);
sync_with_lock(preset);
boost::this_thread::sleep_for(boost::chrono::milliseconds(100));
}
}
@@ -6753,7 +6805,7 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg)
sync_count = preset_bundle->printers.get_user_presets(preset_bundle, presets_to_sync);
if (sync_count > 0) {
for (Preset& preset : presets_to_sync) {
sync_preset(&preset);
sync_with_lock(preset);
boost::this_thread::sleep_for(boost::chrono::milliseconds(100));
}
}

View File

@@ -297,6 +297,7 @@ private:
NetworkAgent* m_agent { nullptr };
std::map<std::string, std::string> need_delete_presets; // store setting ids of preset
std::vector<bool> m_create_preset_blocked { false, false, false, false, false, false }; // excceed limit
std::vector<std::string> m_pending_conflict_setting_ids; // setting_id from the most recent 409 conflict
bool m_networking_compatible { false };
bool m_networking_need_update { false };
bool m_networking_cancel_update { false };
@@ -529,7 +530,7 @@ public:
void add_pending_vendor_preset(const std::pair<std::string, std::map<std::string, std::string>>& preset_data);
void load_pending_vendors();
void sync_preset(Preset* preset);
void sync_preset(Preset* preset, bool force = false);
void start_sync_user_preset(bool with_progress_dlg = false);
void stop_sync_user_preset();
void restart_sync_user_preset();

View File

@@ -2382,6 +2382,79 @@ void NotificationManager::SharedProfilesNotification::render_hypertext(ImGuiWrap
(int)(HyperColor.w * 255.f * (m_state == EState::FadingOut ? m_current_fade_opacity : 1.f))));
}
void NotificationManager::OrcaSyncConflictNotification::init()
{
PopNotification::init();
// Reserve a dedicated action row for the two conflict-resolution links.
m_lines_count = m_lines_count + 1;
}
void NotificationManager::OrcaSyncConflictNotification::render_text(ImGuiWrapper& imgui,
const float win_size_x, const float win_size_y,
const float win_pos_x, const float win_pos_y)
{
float x_offset = m_left_indentation;
float shift_y = m_line_height;
float starting_y = m_line_height / 2;
int last_end = 0;
std::string line;
for (size_t i = 0; i < m_endlines.size(); i++) {
if (m_text1.size() >= m_endlines[i]) {
line = m_text1.substr(last_end, m_endlines[i] - last_end);
last_end = m_endlines[i];
if (m_text1.size() > m_endlines[i])
last_end += (m_text1[m_endlines[i]] == '\n' || m_text1[m_endlines[i]] == ' ' ? 1 : 0);
ImGui::SetCursorPosX(x_offset);
ImGui::SetCursorPosY(starting_y + i * shift_y);
imgui.text(line.c_str());
}
}
const float action_y = starting_y + m_endlines.size() * shift_y;
const std::string pull_text = _u8L("Pull");
render_action_link(imgui, x_offset, action_y, pull_text, "##orca_sync_pull", m_pull_callback);
if (m_force_push_callback) {
const std::string force_push_text = _u8L("Force push");
const float force_x = x_offset + ImGui::CalcTextSize((pull_text + " ").c_str()).x;
render_action_link(imgui, force_x, action_y, force_push_text, "##orca_sync_force_push", m_force_push_callback);
}
}
void NotificationManager::OrcaSyncConflictNotification::render_action_link(ImGuiWrapper& imgui, float text_x, float text_y, const std::string& text,
const char* id, const std::function<bool(wxEvtHandler*)>& callback)
{
ImVec2 part_size = ImGui::CalcTextSize(text.c_str());
ImGui::SetCursorPosX(text_x - 4);
ImGui::SetCursorPosY(text_y - 5);
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(.0f, .0f, .0f, .0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(.0f, .0f, .0f, .0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(.0f, .0f, .0f, .0f));
if (imgui.button(id, part_size.x + 6, part_size.y + 10)) {
if (callback && callback(m_evt_handler))
close();
}
ImGui::PopStyleColor(3);
ImVec4 color = m_HyperTextColor;
if (ImGui::IsItemHovered(ImGuiHoveredFlags_RectOnly))
color = m_HyperTextColorHover;
push_style_color(ImGuiCol_Text, color, m_state == EState::FadingOut, m_current_fade_opacity);
ImGui::SetCursorPosX(text_x);
ImGui::SetCursorPosY(text_y);
imgui.text(text.c_str());
ImGui::PopStyleColor();
ImVec2 lineEnd = ImGui::GetItemRectMax();
lineEnd.y -= 2;
ImVec2 lineStart = lineEnd;
lineStart.x = ImGui::GetItemRectMin().x;
ImGui::GetWindowDrawList()->AddLine(lineStart, lineEnd,
IM_COL32((int)(color.x * 255), (int)(color.y * 255), (int)(color.z * 255),
(int)(color.w * 255.f * (m_state == EState::FadingOut ? m_current_fade_opacity : 1.f))));
}
void NotificationManager::push_shared_profiles_notification(const std::string& explore_url)
{
close_notification_of_type(NotificationType::OrcaSharedProfilesAvailable);
@@ -2391,6 +2464,16 @@ void NotificationManager::push_shared_profiles_notification(const std::string& e
push_notification_data(std::make_unique<NotificationManager::SharedProfilesNotification>(data, m_id_provider, m_evt_handler, explore_url), 0);
}
void NotificationManager::push_orca_sync_conflict_notification(const std::string& text,
std::function<bool(wxEvtHandler*)> pull_callback,
std::function<bool(wxEvtHandler*)> force_push_callback)
{
close_notification_of_type(NotificationType::OrcaSyncConflict);
NotificationData data{ NotificationType::OrcaSyncConflict, NotificationLevel::WarningNotificationLevel, 0, text };
push_notification_data(std::make_unique<NotificationManager::OrcaSyncConflictNotification>(
data, m_id_provider, m_evt_handler, std::move(pull_callback), std::move(force_push_callback)), 0);
}
void NotificationManager::push_download_URL_progress_notification(size_t id, const std::string& text, std::function<bool(DownloaderUserAction, int)> user_action_callback)
{
// If already exists

View File

@@ -15,6 +15,8 @@
#include <wx/time.h>
#include <string>
#include <functional>
#include <utility>
#include <vector>
#include <deque>
#include <unordered_set>
@@ -162,6 +164,8 @@ enum class NotificationType
BBLMixUsePLAAndPETG,
BBLNozzleFilamentIncompatible,
OrcaSharedProfilesAvailable,
OrcaCloudAPIError,
OrcaSyncConflict,
NotificationTypeCount
};
@@ -274,6 +278,9 @@ public:
// Shared profiles available for selected printer
void push_shared_profiles_notification(const std::string& explore_url);
void push_orca_sync_conflict_notification(const std::string& text,
std::function<bool(wxEvtHandler*)> pull_callback,
std::function<bool(wxEvtHandler*)> force_push_callback);
// Download URL progress notif
void push_download_URL_progress_notification(size_t id, const std::string& text, std::function<bool(DownloaderUserAction, int)> user_action_callback);
@@ -887,6 +894,30 @@ private:
std::string m_explore_url;
bool m_dont_show_clicked{ false };
};
class OrcaSyncConflictNotification : public PopNotification
{
public:
OrcaSyncConflictNotification(const NotificationData& n, NotificationIDProvider& id_provider, wxEvtHandler* evt_handler,
std::function<bool(wxEvtHandler*)> pull_callback,
std::function<bool(wxEvtHandler*)> force_push_callback)
: PopNotification(n, id_provider, evt_handler)
, m_pull_callback(std::move(pull_callback))
, m_force_push_callback(std::move(force_push_callback))
{
m_multiline = true;
}
protected:
void init() override;
void render_text(ImGuiWrapper& imgui,
const float win_size_x, const float win_size_y,
const float win_pos_x, const float win_pos_y) override;
void render_action_link(ImGuiWrapper& imgui, float text_x, float text_y, const std::string& text,
const char* id, const std::function<bool(wxEvtHandler*)>& callback);
std::function<bool(wxEvtHandler*)> m_pull_callback;
std::function<bool(wxEvtHandler*)> m_force_push_callback;
};
class SlicingProgressNotification;
// in HintNotification.hpp

View File

@@ -433,7 +433,7 @@ std::string BBLCloudServiceAgent::request_setting_id(std::string name, std::map<
return "";
}
int BBLCloudServiceAgent::put_setting(std::string setting_id, std::string name, std::map<std::string, std::string>* values_map, unsigned int* http_code)
int BBLCloudServiceAgent::put_setting(std::string setting_id, std::string name, std::map<std::string, std::string>* values_map, unsigned int* http_code, bool force)
{
auto& plugin = BBLNetworkPlugin::instance();
auto agent = plugin.get_agent();

View File

@@ -70,7 +70,7 @@ public:
// Settings Synchronization
int get_user_presets(std::map<std::string, std::map<std::string, std::string>>* user_presets) override;
std::string request_setting_id(std::string name, std::map<std::string, std::string>* values_map, unsigned int* http_code) override;
int put_setting(std::string setting_id, std::string name, std::map<std::string, std::string>* values_map, unsigned int* http_code) override;
int put_setting(std::string setting_id, std::string name, std::map<std::string, std::string>* values_map, unsigned int* http_code, bool force = false) override;
int get_setting_list(std::string bundle_version, ProgressFn pro_fn = nullptr, WasCancelledFn cancel_fn = nullptr) override;
int get_setting_list2(std::string bundle_version, CheckFn chk_fn, ProgressFn pro_fn = nullptr, WasCancelledFn cancel_fn = nullptr) override;
int delete_setting(std::string setting_id) override;

View File

@@ -247,7 +247,7 @@ public:
/**
* Update or create a preset with a known setting_id.
*/
virtual int put_setting(std::string setting_id, std::string name, std::map<std::string, std::string>* values_map, unsigned int* http_code) = 0;
virtual int put_setting(std::string setting_id, std::string name, std::map<std::string, std::string>* values_map, unsigned int* http_code, bool force = false) = 0;
/**
* Trigger bulk download of user presets.

View File

@@ -383,11 +383,12 @@ int NetworkAgent::put_setting(std::string setting_id,
std::string name,
std::map<std::string, std::string>* values_map,
unsigned int* http_code,
const std::string& provider)
const std::string& provider,
bool force)
{
const auto cloud_agent = get_cloud_agent(provider);
if (cloud_agent)
return cloud_agent->put_setting(std::move(setting_id), std::move(name), values_map, http_code);
return cloud_agent->put_setting(std::move(setting_id), std::move(name), values_map, http_code, force);
return -1;
}

View File

@@ -93,7 +93,7 @@ public:
// NOTE: this should always call only OrcaCloud
int get_user_presets(std::map<std::string, std::map<std::string, std::string>>* user_presets, const std::string& provider = ORCA_CLOUD_PROVIDER);
std::string request_setting_id(std::string name, std::map<std::string, std::string>* values_map, unsigned int* http_code, const std::string& provider = ORCA_CLOUD_PROVIDER);
int put_setting(std::string setting_id, std::string name, std::map<std::string, std::string>* values_map, unsigned int* http_code, const std::string& provider = ORCA_CLOUD_PROVIDER);
int put_setting(std::string setting_id, std::string name, std::map<std::string, std::string>* values_map, unsigned int* http_code, const std::string& provider = ORCA_CLOUD_PROVIDER, bool force = false);
int get_setting_list(std::string bundle_version, ProgressFn pro_fn = nullptr, WasCancelledFn cancel_fn = nullptr, const std::string& provider = ORCA_CLOUD_PROVIDER);
int get_setting_list2(std::string bundle_version, CheckFn chk_fn, ProgressFn pro_fn = nullptr, WasCancelledFn cancel_fn = nullptr, const std::string& provider = ORCA_CLOUD_PROVIDER);
int delete_setting(std::string setting_id, const std::string& provider = ORCA_CLOUD_PROVIDER);

View File

@@ -56,6 +56,7 @@ constexpr const char* ORCA_DEFAULT_PUB_KEY = "sb_publishable_lvVe_whOi80SU9BPSxM
constexpr const char* ORCA_HEALTH_PATH = "/api/v1/health";
constexpr const char* ORCA_SYNC_PULL_PATH = "/api/v1/sync/pull";
constexpr const char* ORCA_SYNC_PUSH_PATH = "/api/v1/sync/push";
constexpr const char* ORCA_SYNC_FORCE_PUSH_PATH = "/api/v1/sync/force-push";
constexpr const char* ORCA_SYNC_DELETE_PATH = "/api/v1/sync/delete";
constexpr const char* ORCA_PROFILES_PATH = "/api/v1/sync/profiles";
constexpr const char* ORCA_SUBSCRIPTIONS_PATH = "/api/v1/subscriptions";
@@ -965,7 +966,7 @@ std::string OrcaCloudServiceAgent::request_setting_id(std::string name, std::map
return "";
}
int OrcaCloudServiceAgent::put_setting(std::string setting_id, std::string name, std::map<std::string, std::string>* values_map, unsigned int* http_code)
int OrcaCloudServiceAgent::put_setting(std::string setting_id, std::string name, std::map<std::string, std::string>* values_map, unsigned int* http_code, bool force)
{
// Extract original_updated_time for Optimistic Concurrency Control
// If present, server will verify version before update. If absent, treated as insert.
@@ -989,7 +990,7 @@ int OrcaCloudServiceAgent::put_setting(std::string setting_id, std::string name,
}
}
auto result = sync_push(setting_id, name, content, original_updated_time);
auto result = sync_push(setting_id, name, content, original_updated_time, force);
if (http_code) *http_code = result.http_code;
if (result.success) {
@@ -1208,11 +1209,11 @@ int OrcaCloudServiceAgent::sync_pull(
}
}
SyncPushResult OrcaCloudServiceAgent::sync_push(
const std::string& profile_id,
const std::string& name,
const nlohmann::json& content,
const std::string& original_updated_time)
SyncPushResult OrcaCloudServiceAgent::sync_push(const std::string& profile_id,
const std::string& name,
const nlohmann::json& content,
const std::string& original_updated_time,
bool force)
{
SyncPushResult result;
result.success = false;
@@ -1243,7 +1244,7 @@ SyncPushResult OrcaCloudServiceAgent::sync_push(
std::string response;
unsigned int http_code = 0;
int http_result = http_post(ORCA_SYNC_PUSH_PATH, body_str, &response, &http_code);
int http_result = http_post(force ? ORCA_SYNC_FORCE_PUSH_PATH : ORCA_SYNC_PUSH_PATH, body_str, &response, &http_code);
result.http_code = http_code;
@@ -1888,7 +1889,7 @@ int OrcaCloudServiceAgent::http_post(const std::string& path, const std::string&
.on_error([&](std::string resp_body, std::string error, unsigned resp_status) {
result.success = false;
result.status = resp_status == 0 ? 404 : resp_status;
result.body = body;
result.body = resp_body;
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: HTTP error - " << error;
})
.timeout_max(30)

View File

@@ -176,7 +176,12 @@ public:
// ========================================================================
int get_user_presets(std::map<std::string, std::map<std::string, std::string>>* user_presets) override;
std::string request_setting_id(std::string name, std::map<std::string, std::string>* values_map, unsigned int* http_code) override;
int put_setting(std::string setting_id, std::string name, std::map<std::string, std::string>* values_map, unsigned int* http_code) override;
int put_setting(std::string setting_id, std::string name, std::map<std::string, std::string>* values_map, unsigned int* http_code, bool force = false) override;
SyncPushResult sync_push(const std::string& profile_id,
const std::string& name,
const nlohmann::json& content,
const std::string& original_updated_time = "",
bool force = false);
int get_setting_list(std::string bundle_version, ProgressFn pro_fn = nullptr, WasCancelledFn cancel_fn = nullptr) override;
int get_setting_list2(std::string bundle_version, CheckFn chk_fn, ProgressFn pro_fn = nullptr, WasCancelledFn cancel_fn = nullptr) override;
int delete_setting(std::string setting_id) override;
@@ -294,13 +299,6 @@ private:
std::function<void(int http_code, const std::string& error)> on_error
);
SyncPushResult sync_push(
const std::string& profile_id,
const std::string& name,
const nlohmann::json& content,
const std::string& original_updated_time = ""
);
// HTTP request helpers
int http_get(const std::string& path, std::string* response_body, unsigned int* http_code);
int http_post(const std::string& path, const std::string& body, std::string* response_body, unsigned int* http_code);