diff --git a/backend/static/css/app.css b/backend/static/css/app.css index 3d11cc6..f433f56 100644 --- a/backend/static/css/app.css +++ b/backend/static/css/app.css @@ -251,6 +251,9 @@ h1 { } .sidebar-link { + display: flex; + align-items: center; + gap: 0.55rem; padding: 0.62rem 0.75rem; border: 1px solid transparent; border-radius: var(--radius); @@ -1570,3 +1573,248 @@ pre code { text-overflow: ellipsis; white-space: nowrap; } + +/* ── Storage card UI ─────────────────────────────────────────────────────── */ + +.storage-stack { + display: grid; + gap: 0.85rem; +} + +.storage-card { + border: 1px solid var(--border); + border-radius: var(--radius); + background: color-mix(in srgb, var(--card) 94%, transparent); + overflow: hidden; +} + +.storage-card.is-local { + border-left: 3px solid rgba(125, 211, 252, 0.45); +} + +.storage-card.is-editing { + border-color: rgba(125, 211, 252, 0.35); + box-shadow: 0 0 0 1px rgba(125, 211, 252, 0.12); +} + +.storage-card-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 1rem 1.1rem; + flex-wrap: wrap; +} + +.storage-card-identity { + display: flex; + align-items: center; + gap: 0.85rem; + min-width: 0; +} + +.storage-card-icon { + display: grid; + place-items: center; + flex-shrink: 0; + width: 2.4rem; + height: 2.4rem; + border: 1px solid var(--border); + border-radius: calc(var(--radius) - 0.125rem); + background: var(--muted); + color: var(--muted-foreground); +} + +.storage-card-icon svg { + width: 1.2rem; + height: 1.2rem; +} + +.storage-card-name { + display: block; + font-size: 0.95rem; + font-weight: 650; + color: var(--foreground); +} + +.storage-card-meta { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0.4rem; + margin-top: 0.3rem; +} + +.storage-card-usage { + color: var(--muted-foreground); + font-size: 0.78rem; +} + +.storage-card-actions { + display: flex; + align-items: center; + gap: 0.4rem; + flex-shrink: 0; +} + +/* View-mode summary */ +.storage-card-summary { + display: flex; + flex-wrap: wrap; + gap: 0 1.75rem; + padding: 0.65rem 1.1rem 0.9rem; + border-top: 1px solid var(--border); +} + +.storage-detail { + display: flex; + flex-direction: column; + gap: 0.15rem; + min-width: 8rem; +} + +.storage-detail > span:first-child, +.storage-detail > code:first-child { + color: var(--muted-foreground); + font-size: 0.72rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.storage-detail > span:last-child, +.storage-detail > code:last-child { + font-size: 0.82rem; + color: var(--foreground); + word-break: break-all; +} + +.storage-detail-test > span:last-child { + font-size: 0.8rem; +} + +.storage-detail-test.is-ok > span:last-child { color: #86efac; } +.storage-detail-test.is-err > span:last-child { color: #fca5a5; } + +/* Edit-mode body */ +.storage-card:not(.is-editing) .storage-card-body { display: none; } +.storage-card.is-editing .storage-card-summary { display: none; } +.storage-card.is-editing .storage-edit-trigger { display: none; } + +.storage-card-body { + border-top: 1px solid var(--border); + padding: 1rem 1.1rem; +} + +.storage-card-fields { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.75rem; + align-items: end; +} + +.storage-card-fields label { + display: grid; + gap: 0.28rem; + color: var(--muted-foreground); + font-size: 0.8rem; +} + +.storage-card-fields label span { + font-size: 0.72rem; + color: var(--muted-foreground); +} + +.storage-card-fields textarea { + min-height: 5rem; + resize: vertical; +} + +.storage-card-fields .checkbox-field { + align-self: center; +} + +.storage-card-edit-bar { + grid-column: 1 / -1; + display: flex; + gap: 0.5rem; + margin-top: 0.25rem; + padding-top: 0.65rem; + border-top: 1px solid var(--border); +} + +@media (max-width: 640px) { + .storage-card-fields { + grid-template-columns: 1fr; + } +} + +/* Add storage section */ +.storage-add-section { + display: grid; + gap: 0.75rem; +} + +.storage-add-controls { + display: flex; + align-items: center; + gap: 0.65rem; +} + +.storage-type-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr)); + gap: 0.6rem; +} + +.storage-type-option { + display: grid; + grid-template-rows: auto auto auto; + gap: 0.3rem; + padding: 0.9rem 1rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--card); + color: var(--foreground); + font: inherit; + text-align: left; + cursor: pointer; + transition: border-color 120ms ease, background 120ms ease; +} + +.storage-type-option:hover { + border-color: rgba(125, 211, 252, 0.35); + background: color-mix(in srgb, var(--card) 80%, rgba(14, 116, 144, 0.3)); +} + +.storage-type-option svg { + width: 1.5rem; + height: 1.5rem; + color: var(--muted-foreground); + margin-bottom: 0.2rem; +} + +.storage-type-option strong { + font-size: 0.88rem; + font-weight: 650; +} + +.storage-type-option span { + font-size: 0.78rem; + color: var(--muted-foreground); + line-height: 1.4; +} + +.storage-new-card { + border: 1px dashed rgba(125, 211, 252, 0.4); + border-radius: var(--radius); + background: color-mix(in srgb, var(--card) 90%, rgba(14, 116, 144, 0.15)); +} + +.storage-new-card .storage-card-header { + border-bottom: 1px solid var(--border); +} + +.storage-new-card .storage-card-body { + border-top: none; +} diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 1d0cc3b..24a94db 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -122,39 +122,128 @@ }); } + function syncStorageProvider(select) { + const formScope = select.closest("form"); + if (!formScope) { + return; + } + const provider = select.value; + const isContabo = provider === "contabo"; + formScope.querySelectorAll("[data-provider-fields]").forEach((group) => { + const providers = (group.getAttribute("data-provider-fields") || "").split(/\s+/); + const active = providers.includes(provider); + group.hidden = !active; + group.querySelectorAll("input, select, textarea").forEach((input) => { + input.disabled = !active; + }); + }); + const tls = formScope.querySelector('input[name="use_ssl"]'); + const pathStyle = formScope.querySelector('input[name="path_style"]'); + if (tls) { + tls.checked = isContabo || tls.checked; + tls.disabled = isContabo; + } + if (pathStyle) { + pathStyle.checked = isContabo || pathStyle.checked; + pathStyle.disabled = isContabo; + } + } + if (storageProviderSelects.length > 0) { storageProviderSelects.forEach((select) => { - const formScope = select.closest("form"); - const syncStorageProvider = () => { - if (!formScope) { - return; - } - const provider = select.value; - const isContabo = provider === "contabo"; - formScope.querySelectorAll("[data-provider-fields]").forEach((group) => { - const providers = (group.getAttribute("data-provider-fields") || "").split(/\s+/); - const active = providers.includes(provider); - group.hidden = !active; - group.querySelectorAll("input, select, textarea").forEach((input) => { - input.disabled = !active; - }); - }); - const tls = formScope.querySelector('input[name="use_ssl"]'); - const pathStyle = formScope.querySelector('input[name="path_style"]'); - if (tls) { - tls.checked = isContabo || tls.checked; - tls.disabled = isContabo; - } - if (pathStyle) { - pathStyle.checked = isContabo || pathStyle.checked; - pathStyle.disabled = isContabo; - } - }; - select.addEventListener("change", syncStorageProvider); - syncStorageProvider(); + select.addEventListener("change", () => syncStorageProvider(select)); + syncStorageProvider(select); }); } + /* Storage card edit / cancel toggles */ + document.querySelectorAll(".storage-edit-trigger").forEach((btn) => { + btn.addEventListener("click", () => { + const card = btn.closest(".storage-card"); + if (!card) return; + card.classList.add("is-editing"); + const providerSelect = card.querySelector("[data-storage-provider]"); + if (providerSelect) { + syncStorageProvider(providerSelect); + } + }); + }); + + document.querySelectorAll(".storage-cancel-trigger").forEach((btn) => { + btn.addEventListener("click", () => { + const card = btn.closest(".storage-card"); + if (!card) return; + const form = card.querySelector("form"); + if (form) form.reset(); + card.classList.remove("is-editing"); + }); + }); + + /* Add storage: type picker */ + const storageAddTrigger = document.querySelector(".storage-add-trigger"); + const storageTypePicker = document.querySelector(".storage-type-picker"); + const storageNewCard = document.querySelector(".storage-new-card"); + + const providerLabels = { + s3: "S3 bucket", + contabo: "Contabo Object Storage", + sftp: "SFTP", + smb: "Samba", + webdav: "WebDAV", + }; + + const providerIconSVGs = { + s3: storageNewCard && storageNewCard.querySelector(".storage-new-icon") ? storageNewCard.querySelector(".storage-new-icon").innerHTML : "", + contabo: "", + sftp: "", + smb: "", + webdav: "", + }; + + if (storageAddTrigger && storageTypePicker) { + storageAddTrigger.addEventListener("click", () => { + storageTypePicker.hidden = !storageTypePicker.hidden; + if (storageNewCard && !storageTypePicker.hidden) { + storageNewCard.hidden = true; + } + }); + + storageTypePicker.querySelectorAll(".storage-type-option").forEach((opt) => { + opt.addEventListener("click", () => { + const provider = opt.dataset.provider; + if (!storageNewCard) return; + + const providerSelect = storageNewCard.querySelector("[data-storage-provider]"); + if (providerSelect) { + providerSelect.value = provider; + syncStorageProvider(providerSelect); + } + + const typeBadge = storageNewCard.querySelector(".storage-new-type-badge"); + if (typeBadge) typeBadge.textContent = providerLabels[provider] || provider; + + const iconEl = storageNewCard.querySelector(".storage-new-icon"); + const optIcon = opt.querySelector("svg"); + if (iconEl && optIcon) { + iconEl.innerHTML = optIcon.outerHTML; + } + + storageTypePicker.hidden = true; + storageNewCard.hidden = false; + }); + }); + } + + if (storageNewCard) { + const cancelBtn = storageNewCard.querySelector(".storage-new-cancel"); + if (cancelBtn) { + cancelBtn.addEventListener("click", () => { + storageNewCard.hidden = true; + if (storageTypePicker) storageTypePicker.hidden = true; + }); + } + } + if (!form || !dropZone || !fileInput) { return; } diff --git a/backend/templates/pages/account.html b/backend/templates/pages/account.html index a6005c5..768aafe 100644 --- a/backend/templates/pages/account.html +++ b/backend/templates/pages/account.html @@ -4,14 +4,14 @@
diff --git a/backend/templates/pages/admin.html b/backend/templates/pages/admin.html index 8e129ec..9f0dc4f 100644 --- a/backend/templates/pages/admin.html +++ b/backend/templates/pages/admin.html @@ -4,20 +4,20 @@
diff --git a/backend/templates/pages/admin_settings.html b/backend/templates/pages/admin_settings.html index b0a8bc9..7aa924f 100644 --- a/backend/templates/pages/admin_settings.html +++ b/backend/templates/pages/admin_settings.html @@ -4,20 +4,20 @@
diff --git a/backend/templates/pages/admin_storage.html b/backend/templates/pages/admin_storage.html index 04b9ad0..292fd2c 100644 --- a/backend/templates/pages/admin_storage.html +++ b/backend/templates/pages/admin_storage.html @@ -4,18 +4,20 @@
@@ -24,126 +26,222 @@

Operator console

{{.Data.PageTitle}}

+

Local storage is always active. Remote backends are proxied through Warpbox.

{{if .Data.Error}}

{{.Data.Error}}

{{end}} -
-
-
-
-

Storage backends

-

Local storage is always available. Remote backends stay private behind Warpbox routes.

-
-
-
- - - - {{range .Data.Storage}} - - - - - - - - - {{end}} - -
NameTypeStatusUsageDetailsActions
{{.Config.Name}}{{if eq .Config.Provider "contabo"}}Contabo Object Storage{{else if eq .Config.Type "sftp"}}SFTP{{else if eq .Config.Type "smb"}}Samba{{else if eq .Config.Type "webdav"}}WebDAV{{else if eq .Config.Type "s3"}}S3 bucket{{else}}{{.Config.Type}}{{end}}{{if .Config.Enabled}}Enabled{{else}}Disabled{{end}}{{.UsageLabel}}{{if eq .Config.Type "local"}}{{.Config.LocalPath}}{{else if eq .Config.Type "sftp"}}{{.Config.Username}}@{{.Config.Host}}:{{.Config.RemotePath}}{{else if eq .Config.Type "smb"}}{{.Config.Domain}}\{{.Config.Username}}@{{.Config.Host}}/{{.Config.Share}}:{{.Config.RemotePath}}{{else if eq .Config.Type "webdav"}}{{.Config.Endpoint}}/{{.Config.RemotePath}}{{else}}{{.Config.Bucket}} @ {{.Config.Endpoint}}{{end}} -
- - -
- {{if ne .Config.ID "local"}} -
- Edit -
- - - - - - - - - - - - - - - - - - - - - -
-
-
- - -
-
- - -
- {{end}} -
-
-
-
+
-
-
-
-
-

Add storage

-

Choose a provider kind first. SFTP is useful for a server or NAS you control.

+ {{range .Data.Storage}} +
+ +
+
+
+ {{if eq .Config.Type "local"}}{{template "icon-hard-drive" $}} + {{else if eq .Config.Type "sftp"}}{{template "icon-database" $}} + {{else if eq .Config.Type "smb"}}{{template "icon-folder" $}} + {{else if eq .Config.Type "webdav"}}{{template "icon-cloud-sync" $}} + {{else}}{{template "icon-cloud-upload" $}}{{end}} +
+
+ {{.Config.Name}} +
+ {{if eq .Config.Provider "contabo"}}Contabo{{else if eq .Config.Type "sftp"}}SFTP{{else if eq .Config.Type "smb"}}Samba{{else if eq .Config.Type "webdav"}}WebDAV{{else if eq .Config.Type "s3"}}S3{{else if eq .Config.Type "local"}}Local files{{else}}{{.Config.Type}}{{end}} + {{if eq .Config.ID "local"}}Required + {{else if .Config.Enabled}}Enabled + {{else}}Disabled{{end}} + {{if .UsageLabel}}{{.UsageLabel}}{{end}} +
+
+
+ +
+
+ + +
+ {{if ne .Config.ID "local"}} + + {{if .Config.Enabled}} +
+ + +
+ {{end}} +
+ + +
+ {{end}}
-
- -
- - - - - - - - - - - - - - - - - - - + + {{/* View-mode summary */}} +
+ {{if eq .Config.Type "local"}} +
Path{{.Config.LocalPath}}
+ {{else if or (eq .Config.Type "s3") (eq .Config.Provider "contabo")}} + {{if .Config.Endpoint}}
Endpoint{{.Config.Endpoint}}
{{end}} + {{if .Config.Bucket}}
Bucket{{.Config.Bucket}}
{{end}} + {{if .Config.Region}}
Region{{.Config.Region}}
{{end}} + {{if .Config.AccessKey}}
Access key{{.Config.AccessKey}}
{{end}} + {{else if eq .Config.Type "sftp"}} + {{if .Config.Host}}
Host{{.Config.Host}}{{if .Config.Port}}:{{.Config.Port}}{{end}}
{{end}} + {{if .Config.Username}}
Username{{.Config.Username}}
{{end}} + {{if .Config.RemotePath}}
Remote path{{.Config.RemotePath}}
{{end}} + {{else if eq .Config.Type "smb"}} + {{if .Config.Host}}
Host{{if .Config.Domain}}{{.Config.Domain}}\{{end}}{{.Config.Username}}@{{.Config.Host}}/{{.Config.Share}}
{{end}} + {{if .Config.RemotePath}}
Remote path{{.Config.RemotePath}}
{{end}} + {{else if eq .Config.Type "webdav"}} + {{if .Config.Endpoint}}
URL{{.Config.Endpoint}}
{{end}} + {{if .Config.Username}}
Username{{.Config.Username}}
{{end}} + {{if .Config.RemotePath}}
Remote path{{.Config.RemotePath}}
{{end}} + {{end}} + {{if not (.Config.LastTestedAt.IsZero)}} +
+ Last test + {{.Config.LastTestedAt.Format "Jan 2, 15:04"}} · {{if .Config.LastTestSuccess}}Passed{{else}}{{if .Config.LastTestError}}{{.Config.LastTestError}}{{else}}Failed{{end}}{{end}}
- - + {{end}} +
+ + {{/* Edit-mode form — hidden via CSS until .is-editing */}} + {{if ne .Config.ID "local"}} +
+
+ + + + + + + + + + + + + + + + + + + + +
+ + +
+
+
+ {{end}} +
+ {{end}} + + {{/* Add storage section */}} +
+
+ +
+ + + + +
+
diff --git a/backend/templates/pages/admin_user_edit.html b/backend/templates/pages/admin_user_edit.html index dd058ea..2755ffb 100644 --- a/backend/templates/pages/admin_user_edit.html +++ b/backend/templates/pages/admin_user_edit.html @@ -4,18 +4,18 @@
diff --git a/backend/templates/pages/admin_users.html b/backend/templates/pages/admin_users.html index 4272a35..1d6c76a 100644 --- a/backend/templates/pages/admin_users.html +++ b/backend/templates/pages/admin_users.html @@ -4,20 +4,20 @@
diff --git a/backend/templates/pages/dashboard.html b/backend/templates/pages/dashboard.html index 2ba39d6..1873f16 100644 --- a/backend/templates/pages/dashboard.html +++ b/backend/templates/pages/dashboard.html @@ -4,14 +4,14 @@
diff --git a/backend/templates/partials/icons.html b/backend/templates/partials/icons.html new file mode 100644 index 0000000..c6983d5 --- /dev/null +++ b/backend/templates/partials/icons.html @@ -0,0 +1,21 @@ +{{define "icon-dashboard"}}{{end}} + +{{define "icon-folder"}}{{end}} + +{{define "icon-user-circle"}}{{end}} + +{{define "icon-settings"}}{{end}} + +{{define "icon-database"}}{{end}} + +{{define "icon-home-simple"}}{{end}} + +{{define "icon-log-out"}}{{end}} + +{{define "icon-hard-drive"}}{{end}} + +{{define "icon-cloud-upload"}}{{end}} + +{{define "icon-cloud-sync"}}{{end}} + +{{define "icon-plus-circle"}}{{end}} diff --git a/backend/warpbox b/backend/warpbox index d289430..9eb995a 100755 Binary files a/backend/warpbox and b/backend/warpbox differ