feat: add S3 / Object Storage settings section

This commit is contained in:
Zac Gaetano 2026-05-20 15:55:34 -04:00
parent 7032cee6b3
commit 11e1de1cf8

View file

@ -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;
}
</style>
</head>
<body>
@ -252,6 +263,74 @@
<div class="page-content">
<div class="settings-layout">
<!-- ── S3 / Object Storage ───────────────────────────────────── -->
<div class="settings-section">
<div class="settings-section-header">
<div class="settings-section-icon s3">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" width="16" height="16">
<ellipse cx="8" cy="4" rx="6" ry="2.5"/>
<path d="M2 4v4c0 1.4 2.7 2.5 6 2.5S14 9.4 14 8V4"/>
<path d="M2 8v4c0 1.4 2.7 2.5 6 2.5S14 13.4 14 12V8"/>
</svg>
</div>
<div>
<div class="settings-section-title">S3 / Object Storage</div>
<div class="settings-section-desc">S3-compatible bucket for media asset storage (Garage, MinIO, AWS S3, etc.)</div>
</div>
</div>
<div class="settings-section-body">
<div style="font-size:13px;color:var(--text-secondary);line-height:1.6;padding:12px 14px;background:oklch(11% 0.018 250 / 0.5);border:1px solid var(--border);border-radius:8px;">
<strong style="color:var(--text-primary);">Initial setup required:</strong>
Configure your S3-compatible object store below. Changes take effect immediately without restarting the API.
Use <strong>Test Connection</strong> to verify credentials before saving.
</div>
<div class="form-row">
<div class="form-group" style="flex:2;">
<label class="form-label" for="s3Endpoint">Endpoint URL</label>
<input type="url" id="s3Endpoint" placeholder="http://192.168.1.10:9000" autocomplete="off">
<div class="form-hint">Full URL including protocol and port. Leave blank to use AWS S3.</div>
</div>
<div class="form-group" style="flex:1;">
<label class="form-label" for="s3Region">Region</label>
<input type="text" id="s3Region" placeholder="us-east-1" autocomplete="off">
<div class="form-hint">Required for AWS; use any value for self-hosted.</div>
</div>
</div>
<div class="form-group">
<label class="form-label" for="s3Bucket">Bucket name</label>
<input type="text" id="s3Bucket" placeholder="mam" autocomplete="off">
</div>
<div class="form-group">
<label class="form-label" for="s3AccessKey">Access key ID</label>
<input type="text" id="s3AccessKey" placeholder="Access key" autocomplete="off" spellcheck="false">
</div>
<div class="form-group">
<label class="form-label" for="s3SecretKey">Secret access key</label>
<div class="input-with-action">
<input type="password" id="s3SecretKey" placeholder="Leave blank to keep current secret" autocomplete="off" spellcheck="false">
<button class="btn btn-ghost btn-sm" id="s3SecretToggle" onclick="toggleS3SecretVisibility()" style="flex-shrink:0;">Show</button>
</div>
<div class="form-hint" id="s3SecretHint" style="display:none;">
A secret key is currently saved. Enter a new value to replace it.
</div>
</div>
<div class="form-row-actions">
<button class="btn btn-primary btn-sm" onclick="saveS3()">Save &amp; Apply</button>
<button class="btn btn-ghost btn-sm" onclick="testS3()">Test connection</button>
<span class="save-feedback" id="s3Feedback">Saved</span>
</div>
<div class="test-result" id="s3TestResult"></div>
</div>
</div>
<!-- ── GPU / Transcoding ─────────────────────────────────────── -->
<div class="settings-section">
<div class="settings-section-header">
@ -440,6 +519,66 @@ function esc(s) {
return String(s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ── 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()]);
</script>
<script src="js/auth-guard.js"></script>
</body>