diff --git a/services/web-ui/public/settings.html b/services/web-ui/public/settings.html index c2c031f..95bfb56 100644 --- a/services/web-ui/public/settings.html +++ b/services/web-ui/public/settings.html @@ -39,9 +39,10 @@ flex-shrink: 0; } - .settings-section-icon.gpu { background: oklch(20% 0.08 266 / 0.5); color: oklch(70% 0.18 266); } - .settings-section-icon.sdi { background: oklch(20% 0.08 25 / 0.5); color: oklch(72% 0.18 40); } - .settings-section-icon.ampp { background: oklch(20% 0.08 148 / 0.5); color: oklch(68% 0.18 148); } + .settings-section-icon.s3 { background: oklch(20% 0.08 195 / 0.5); color: oklch(68% 0.18 195); } + .settings-section-icon.gpu { background: oklch(20% 0.08 266 / 0.5); color: oklch(70% 0.18 266); } + .settings-section-icon.sdi { background: oklch(20% 0.08 25 / 0.5); color: oklch(72% 0.18 40); } + .settings-section-icon.ampp { background: oklch(20% 0.08 148 / 0.5); color: oklch(68% 0.18 148); } .settings-section-title { font-size: 14px; @@ -164,6 +165,16 @@ gap: 10px; margin-top: 4px; } + + /* Secret key row with show/hide toggle */ + .input-with-action { + display: flex; + gap: 8px; + } + + .input-with-action input { + flex: 1; + } @@ -252,6 +263,74 @@
+ +
+
+
+ + + + + +
+
+
S3 / Object Storage
+
S3-compatible bucket for media asset storage (Garage, MinIO, AWS S3, etc.)
+
+
+
+ +
+ Initial setup required: + Configure your S3-compatible object store below. Changes take effect immediately without restarting the API. + Use Test Connection to verify credentials before saving. +
+ +
+
+ + +
Full URL including protocol and port. Leave blank to use AWS S3.
+
+
+ + +
Required for AWS; use any value for self-hosted.
+
+
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+ +
+ +
+ + + Saved +
+ +
+ +
+
+
@@ -440,6 +519,66 @@ function esc(s) { return String(s || '').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } +// ── S3 / Object Storage ──────────────────────────────────────────────────── +async function loadS3() { + try { + const d = await api('/settings/s3'); + document.getElementById('s3Endpoint').value = d.s3_endpoint || ''; + document.getElementById('s3Bucket').value = d.s3_bucket || ''; + document.getElementById('s3AccessKey').value = d.s3_access_key || ''; + document.getElementById('s3Region').value = d.s3_region || ''; + if (d.s3_secret_key_exists) { + document.getElementById('s3SecretHint').style.display = ''; + } + } catch (_) {} +} + +async function saveS3() { + try { + const body = { + s3_endpoint: document.getElementById('s3Endpoint').value.trim(), + s3_bucket: document.getElementById('s3Bucket').value.trim(), + s3_access_key: document.getElementById('s3AccessKey').value.trim(), + s3_region: document.getElementById('s3Region').value.trim(), + }; + const secret = document.getElementById('s3SecretKey').value.trim(); + if (secret) body.s3_secret_key = secret; + await api('/settings/s3', { method: 'PUT', body: JSON.stringify(body) }); + document.getElementById('s3SecretKey').value = ''; + document.getElementById('s3SecretHint').style.display = ''; + flashFeedback('s3Feedback'); + hideTestResult('s3TestResult'); + } catch (e) { toast('Save failed: ' + e.message, 'error'); } +} + +async function testS3() { + const el = document.getElementById('s3TestResult'); + try { + const body = { + s3_endpoint: document.getElementById('s3Endpoint').value.trim(), + s3_bucket: document.getElementById('s3Bucket').value.trim(), + s3_access_key: document.getElementById('s3AccessKey').value.trim(), + s3_region: document.getElementById('s3Region').value.trim(), + }; + const secret = document.getElementById('s3SecretKey').value.trim(); + if (secret) body.s3_secret_key = secret; + const d = await api('/settings/s3/test', { method: 'POST', body: JSON.stringify(body) }); + showTestResult(el, true, d.message || 'Connection successful'); + } catch (e) { showTestResult(el, false, e.message); } +} + +function toggleS3SecretVisibility() { + const inp = document.getElementById('s3SecretKey'); + const btn = document.getElementById('s3SecretToggle'); + if (inp.type === 'password') { + inp.type = 'text'; + btn.textContent = 'Hide'; + } else { + inp.type = 'password'; + btn.textContent = 'Show'; + } +} + // ── Hardware inventory ───────────────────────────────────────────────────── async function loadHardware() { const wrap = document.getElementById('hwTableWrap'); @@ -607,7 +746,7 @@ function toast(msg, type = 'info') { } // ── Init ─────────────────────────────────────────────────────────────────── -Promise.all([loadHardware(), loadTranscoding(), loadCaptureService(), loadAmpp()]); +Promise.all([loadS3(), loadHardware(), loadTranscoding(), loadCaptureService(), loadAmpp()]);