Fix FTP settings UI: add selects, persist connection timeout

- Replace number inputs with select dropdowns for retry attempts,
  retry delay, and connection timeout to avoid auto-save race conditions
- Move connection timeout inside the FTP retry toggle section
- Add ftp_timeout to backend settings schema and integer parsing list
  so the value actually persists (was silently dropped before)

Closes #275
This commit is contained in:
maziggy
2026-02-06 09:20:33 +01:00
parent b4285913a9
commit 905e1762de
7 changed files with 81 additions and 82 deletions
+1
View File
@@ -89,6 +89,7 @@ async def get_settings(
"ams_history_retention_days",
"ftp_retry_count",
"ftp_retry_delay",
"ftp_timeout",
"mqtt_port",
]:
settings_dict[setting.key] = int(setting.value)
+2
View File
@@ -78,6 +78,7 @@ class AppSettings(BaseModel):
ftp_retry_enabled: bool = Field(default=True, description="Enable automatic retry for FTP operations")
ftp_retry_count: int = Field(default=3, description="Number of retry attempts for FTP operations (1-10)")
ftp_retry_delay: int = Field(default=2, description="Seconds to wait between FTP retry attempts (1-30)")
ftp_timeout: int = Field(default=30, description="FTP connection timeout in seconds (10-120)")
# MQTT Relay settings for publishing events to external broker
mqtt_enabled: bool = Field(default=False, description="Enable MQTT event publishing to external broker")
@@ -158,6 +159,7 @@ class AppSettingsUpdate(BaseModel):
ftp_retry_enabled: bool | None = None
ftp_retry_count: int | None = None
ftp_retry_delay: int | None = None
ftp_timeout: int | None = None
mqtt_enabled: bool | None = None
mqtt_broker: str | None = None
mqtt_port: int | None = None
+38 -42
View File
@@ -1890,63 +1890,59 @@ export function SettingsPage() {
<label className="block text-sm text-bambu-gray mb-1">
Retry attempts
</label>
<div className="flex items-center gap-2">
<input
type="number"
min="1"
max="10"
<div className="relative w-44">
<select
value={localSettings.ftp_retry_count ?? 3}
onChange={(e) => updateSetting('ftp_retry_count', Math.min(10, Math.max(1, parseInt(e.target.value) || 3)))}
className="w-24 px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
/>
<span className="text-bambu-gray">times</span>
onChange={(e) => updateSetting('ftp_retry_count', parseInt(e.target.value))}
className="w-full px-3 py-2 pr-10 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none appearance-none cursor-pointer"
>
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(n => (
<option key={n} value={n}>{n} {n === 1 ? 'time' : 'times'}</option>
))}
</select>
<ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray pointer-events-none" />
</div>
<p className="text-xs text-bambu-gray mt-1">
Number of retry attempts before giving up (1-10)
</p>
</div>
<div>
<label className="block text-sm text-bambu-gray mb-1">
Retry delay
</label>
<div className="flex items-center gap-2">
<input
type="number"
min="1"
max="30"
<div className="relative w-44">
<select
value={localSettings.ftp_retry_delay ?? 2}
onChange={(e) => updateSetting('ftp_retry_delay', Math.min(30, Math.max(1, parseInt(e.target.value) || 2)))}
className="w-24 px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
/>
<span className="text-bambu-gray">seconds</span>
onChange={(e) => updateSetting('ftp_retry_delay', parseInt(e.target.value))}
className="w-full px-3 py-2 pr-10 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none appearance-none cursor-pointer"
>
{[1, 2, 3, 5, 10, 15, 20, 30].map(n => (
<option key={n} value={n}>{n} {n === 1 ? 'second' : 'seconds'}</option>
))}
</select>
<ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray pointer-events-none" />
</div>
</div>
<div>
<label className="block text-sm text-bambu-gray mb-1">
Connection timeout
</label>
<div className="relative w-44">
<select
value={localSettings.ftp_timeout ?? 30}
onChange={(e) => updateSetting('ftp_timeout', parseInt(e.target.value))}
className="w-full px-3 py-2 pr-10 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none appearance-none cursor-pointer"
>
{[10, 15, 20, 30, 45, 60, 90, 120].map(n => (
<option key={n} value={n}>{n} seconds</option>
))}
</select>
<ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray pointer-events-none" />
</div>
<p className="text-xs text-bambu-gray mt-1">
Wait time between retries (1-30)
Increase for printers with weak WiFi
</p>
</div>
</div>
)}
<div className="pt-2 border-t border-bambu-dark-tertiary">
<label className="block text-sm text-bambu-gray mb-1">
Connection timeout
</label>
<div className="flex items-center gap-2">
<input
type="number"
min="10"
max="120"
value={localSettings.ftp_timeout ?? 30}
onChange={(e) => updateSetting('ftp_timeout', Math.min(120, Math.max(10, parseInt(e.target.value) || 30)))}
className="w-24 px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
/>
<span className="text-bambu-gray">seconds</span>
</div>
<p className="text-xs text-bambu-gray mt-1">
Socket timeout for slow connections. Increase for A1/A1 Mini printers with weak WiFi (10-120)
</p>
</div>
</CardContent>
</Card>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -23,8 +23,8 @@
<!-- Splash screens for iOS -->
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
<script type="module" crossorigin src="/assets/index-CwIN-fI6.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CosC5iN4.css">
<script type="module" crossorigin src="/assets/index-DJ147FSl.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-togsBDt6.css">
</head>
<body>
<div id="root"></div>