diff --git a/simpletuner/simpletuner_sdk/server/services/tab_service.py b/simpletuner/simpletuner_sdk/server/services/tab_service.py
index 779739286..2e7de3776 100644
--- a/simpletuner/simpletuner_sdk/server/services/tab_service.py
+++ b/simpletuner/simpletuner_sdk/server/services/tab_service.py
@@ -508,7 +508,12 @@ def _git_mirror_enabled(self) -> bool:
def _cloud_tab_enabled(self) -> bool:
try:
defaults = WebUIStateStore().load_defaults()
- return bool(getattr(defaults, "cloud_tab_enabled", True))
+ value = getattr(defaults, "cloud_tab_enabled", True)
+ if value is None:
+ return True
+ if isinstance(value, str):
+ return value.strip().lower() not in {"0", "false", "no", "off"}
+ return bool(value)
except Exception as exc:
logger.debug("Failed to evaluate cloud tab enabled flag: %s", exc, exc_info=True)
return True
diff --git a/simpletuner/simpletuner_sdk/server/services/webui_state.py b/simpletuner/simpletuner_sdk/server/services/webui_state.py
index 857cc3429..fe5704cf8 100644
--- a/simpletuner/simpletuner_sdk/server/services/webui_state.py
+++ b/simpletuner/simpletuner_sdk/server/services/webui_state.py
@@ -331,12 +331,13 @@ def load_defaults(self) -> WebUIDefaults:
payload = self._read_json("defaults")
if not payload:
return WebUIDefaults()
+ base_defaults = WebUIDefaults()
data: Dict[str, Any] = {}
- for key in WebUIDefaults().__dict__.keys():
+ for key, default_value in base_defaults.__dict__.items():
if key == "accelerate_overrides":
data[key] = _normalise_accelerate_overrides(payload.get(key))
else:
- data[key] = payload.get(key)
+ data[key] = payload.get(key, default_value)
defaults = WebUIDefaults(**data)
# Normalise theme selection
@@ -429,6 +430,17 @@ def load_defaults(self) -> WebUIDefaults:
defaults.cloud_dataloader_hint_dismissed = bool(payload.get("cloud_dataloader_hint_dismissed", False))
defaults.cloud_git_hint_dismissed = bool(payload.get("cloud_git_hint_dismissed", False))
+ # Normalise cloud tab enabled (default True)
+ cloud_tab_value = payload.get("cloud_tab_enabled")
+ if cloud_tab_value is None:
+ defaults.cloud_tab_enabled = True
+ elif isinstance(cloud_tab_value, bool):
+ defaults.cloud_tab_enabled = cloud_tab_value
+ elif isinstance(cloud_tab_value, str):
+ defaults.cloud_tab_enabled = cloud_tab_value.strip().lower() not in {"0", "false", "no", "off"}
+ else:
+ defaults.cloud_tab_enabled = bool(cloud_tab_value)
+
# Normalise cloud data consent
consent_value = payload.get("cloud_data_consent")
if isinstance(consent_value, str) and consent_value in {"ask", "allow", "deny"}:
diff --git a/simpletuner/static/js/modules/cloud/index.js b/simpletuner/static/js/modules/cloud/index.js
index 8c42cc256..63e0e1cbe 100644
--- a/simpletuner/static/js/modules/cloud/index.js
+++ b/simpletuner/static/js/modules/cloud/index.js
@@ -413,7 +413,8 @@ if (!window.cloudDashboardComponent) {
this.setupStatus.outputConfigured =
this.publishingStatus.push_to_hub ||
this.publishingStatus.s3_configured ||
- (this.webhookUrl && this.webhookUrl.trim().length > 0);
+ (this.savedWebhookUrl && this.savedWebhookUrl.trim().length > 0) ||
+ this.publishingStatus.local_upload_available;
},
// Note: hasDatasets getter moved to final return object
@@ -677,7 +678,8 @@ if (!window.cloudDashboardComponent) {
if (!this.publishingStatus) return false;
return this.publishingStatus.push_to_hub ||
this.publishingStatus.s3_configured ||
- (this.webhookUrl && this.webhookUrl.trim().length > 0);
+ (this.savedWebhookUrl && this.savedWebhookUrl.trim().length > 0) ||
+ this.publishingStatus.local_upload_available;
},
get allSetupComplete() {
return this.hasDatasets && this.hasActiveConfig && this.hasOutputDestination;
diff --git a/simpletuner/static/js/modules/cloud/metrics.js b/simpletuner/static/js/modules/cloud/metrics.js
index d3c1204df..a04aa1748 100644
--- a/simpletuner/static/js/modules/cloud/metrics.js
+++ b/simpletuner/static/js/modules/cloud/metrics.js
@@ -59,19 +59,42 @@ window.cloudMetricsMethods = {
async saveWebhookConfig() {
this.configSaving = true;
+ const webhookUrl = typeof this.webhookUrl === 'string' ? this.webhookUrl.trim() : '';
try {
const response = await fetch('/api/cloud/providers/replicate/config', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ webhook_url: this.webhookUrl || null }),
+ body: JSON.stringify({ webhook_url: webhookUrl }),
});
- if (response.ok && window.showToast) {
+
+ let data = {};
+ try {
+ data = await response.json();
+ } catch (_) {}
+
+ if (!response.ok) {
+ throw new Error(data.detail || 'Failed to save webhook config');
+ }
+
+ const savedUrl = (data.config && data.config.webhook_url) || data.webhook_url || webhookUrl || '';
+ this.savedWebhookUrl = savedUrl;
+ this.webhookUrl = savedUrl;
+ if (this.publishingStatus) {
+ this.publishingStatus.local_upload_available = savedUrl.length > 0;
+ if (!savedUrl) {
+ this.publishingStatus.local_upload_dir = null;
+ }
+ }
+
+ if (window.showToast) {
window.showToast('Webhook configuration saved', 'success');
}
+ return true;
} catch (error) {
if (window.showToast) {
- window.showToast('Failed to save webhook config', 'error');
+ window.showToast(error.message || 'Failed to save webhook config', 'error');
}
+ return false;
} finally {
this.configSaving = false;
}
diff --git a/simpletuner/static/js/modules/cloud/providers.js b/simpletuner/static/js/modules/cloud/providers.js
index 46f93508c..9f2e75f4d 100644
--- a/simpletuner/static/js/modules/cloud/providers.js
+++ b/simpletuner/static/js/modules/cloud/providers.js
@@ -27,8 +27,9 @@ window.cloudProviderMethods = {
const response = await fetch(`/api/cloud/providers/${this.activeProvider}/config`);
if (response.ok) {
const data = await response.json();
- this.providerConfig = data || {};
- this.webhookUrl = data.webhook_url || '';
+ this.providerConfig = data.config || data || {};
+ this.savedWebhookUrl = this.providerConfig.webhook_url || '';
+ this.webhookUrl = this.savedWebhookUrl;
this.loadCostLimitStatus();
}
} catch (error) {
diff --git a/simpletuner/static/js/modules/cloud/state/publishing-state.js b/simpletuner/static/js/modules/cloud/state/publishing-state.js
index 448d610c6..aa87ba7f9 100644
--- a/simpletuner/static/js/modules/cloud/state/publishing-state.js
+++ b/simpletuner/static/js/modules/cloud/state/publishing-state.js
@@ -10,6 +10,7 @@ window.cloudPublishingStateFactory = function(initial) {
availableConfigs: [],
selectedConfigName: null,
webhookUrl: initialData.webhook_url || '',
+ savedWebhookUrl: initialData.webhook_url || '',
webhookTesting: false,
webhookTestMode: null,
webhookTestResult: null,
diff --git a/simpletuner/templates/cloud_tab.html b/simpletuner/templates/cloud_tab.html
index e262b3936..c342c356a 100644
--- a/simpletuner/templates/cloud_tab.html
+++ b/simpletuner/templates/cloud_tab.html
@@ -139,7 +139,7 @@
Local Outputs
+ x-text="savedWebhookUrl.replace(/\/$/, '') + '/api/cloud/storage'">
diff --git a/simpletuner/templates/partials/cloud_dataloader_hint.html b/simpletuner/templates/partials/cloud_dataloader_hint.html
index e901c41a3..f80782f31 100644
--- a/simpletuner/templates/partials/cloud_dataloader_hint.html
+++ b/simpletuner/templates/partials/cloud_dataloader_hint.html
@@ -141,7 +141,7 @@
S3/Cloud Storage
-
+
Webhook to local machine
diff --git a/simpletuner/templates/partials/cloud_onboarding_flow.html b/simpletuner/templates/partials/cloud_onboarding_flow.html
index b445444e0..cfad3b9b2 100644
--- a/simpletuner/templates/partials/cloud_onboarding_flow.html
+++ b/simpletuner/templates/partials/cloud_onboarding_flow.html
@@ -133,10 +133,10 @@ How You Get Your Model Back
Replicate uploads the model back here. Requires exposing your server via ngrok or cloudflare tunnel.
-
+
Webhook configured
-
+