feat: add S3 / Object Storage settings section
This commit is contained in:
parent
7032cee6b3
commit
11e1de1cf8
1 changed files with 143 additions and 4 deletions
|
|
@ -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 & 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,'&').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()]);
|
||||
</script>
|
||||
<script src="js/auth-guard.js"></script>
|
||||
</body>
|
||||
|
|
|
|||
Loading…
Reference in a new issue