diff --git a/internal/http/static/app.css b/internal/http/static/app.css
index a18c416..2012652 100644
--- a/internal/http/static/app.css
+++ b/internal/http/static/app.css
@@ -754,23 +754,24 @@ pre {
/* Detail pages (DESIGN-SYSTEM §6) */
.breadcrumb { display: flex; align-items: center; gap: 6px; font-size: 12.8px; color: var(--muted); margin-bottom: 8px; }
.page-head { margin-bottom: 12px; }
-.page-head h1 { font-size: 22px; font-weight: 800; margin: 0 0 4px; display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
+.page-head h1 { font-size: 22px; font-weight: 800; margin: 0 0 2px; display: flex; align-items: center; flex-wrap: wrap; gap: 10px; }
.page-head .meta { margin: 0; font-size: 12.8px; color: var(--muted); }
-.alert { display: flex; align-items: flex-start; gap: 10px; padding: 12px 14px; border-radius: 12px; border: 1px solid var(--line); margin-bottom: 12px; }
+.alert { display: flex; align-items: flex-start; gap: 10px; padding: 12px; border-radius: 12px; border: 1px solid var(--line); margin-bottom: 12px; font-size: 13px; }
.alert .icon { margin-top: 1px; }
-.alert-title { font-size: 14px; font-weight: 600; }
+.alert-title { font-size: 13px; font-weight: 700; }
.alert-sub { font-size: 12.8px; opacity: 0.9; overflow-wrap: anywhere; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.alert-text { min-width: 0; }
.alert-ok { background: var(--ok-bg); color: var(--ok-text); border-color: color-mix(in srgb, var(--ok) 25%, transparent); }
.alert-warn { background: var(--warn-bg); color: var(--warn-text); border-color: color-mix(in srgb, var(--warn) 25%, transparent); }
.alert-danger { background: var(--danger-bg); color: var(--danger-text); border-color: color-mix(in srgb, var(--danger) 25%, transparent); }
.alert-neutral { background: var(--sunk); color: var(--ink2); }
-.action-bar { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 16px; }
-.action-bar form { display: contents; }
-.action-bar .btn-primary { grid-column: 1 / -1; }
+.actgrid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--line); }
+.actgrid form { display: contents; }
+.actgrid .btn-primary { grid-column: 1 / -1; }
+.actgrid .btn .icon { width: 17px; height: 17px; }
@media (min-width: 700px) {
- .action-bar { display: flex; flex-wrap: wrap; }
- .action-bar .btn-primary { order: -1; }
+ .actgrid { display: flex; flex-wrap: wrap; }
+ .actgrid .btn-primary { order: -1; }
}
.seg { display: inline-flex; gap: 2px; padding: 3px; background: var(--sunk); border-radius: 12px; max-width: 100%; overflow-x: auto; }
.seg-btn { min-height: 32px; padding: 0 12px; border: 0; border-radius: 10px; background: none; box-shadow: none; color: var(--ink2); font-size: 12.8px; font-weight: 600; white-space: nowrap; }
@@ -794,11 +795,12 @@ div.bar-danger > span { background: var(--danger); }
.stats-body { margin-top: 12px; }
.muted-note { color: var(--muted); font-size: 12.8px; margin: 0 0 8px; }
.muted { color: var(--muted); }
-.detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px 16px; margin: 0; }
+.detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 14px; margin: 0; }
.detail-grid > div { min-width: 0; }
.detail-grid .wide { grid-column: 1 / -1; }
-.detail-grid dt { font-size: 11px; font-weight: 800; text-transform: uppercase; color: var(--muted); letter-spacing: 0.04em; margin-bottom: 2px; }
-.detail-grid dd { margin: 0; font-size: 14px; font-weight: 600; overflow-wrap: anywhere; }
+.detail-grid dt { font-size: 11px; font-weight: 800; text-transform: uppercase; color: var(--muted); letter-spacing: 0.06em; margin-bottom: 2px; }
+.detail-grid dd { margin: 0; font-size: 14px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.detail-grid dd.wrap { white-space: normal; overflow-wrap: anywhere; }
.detail-grid dd.mono, .mono { font-family: 'JetBrains Mono', ui-monospace, monospace; font-size: 13px; }
.wrap { overflow-wrap: anywhere; word-break: break-all; }
pre.code { margin: 0; padding: 12px; background: var(--sunk); border: 1px solid var(--line); border-radius: 12px; font-size: 12.5px; overflow-x: auto; white-space: pre-wrap; overflow-wrap: anywhere; color: var(--ink); }
@@ -810,15 +812,12 @@ pre.code { margin: 0; padding: 12px; background: var(--sunk); border: 1px solid
.file-list li { padding: 8px 0; border-bottom: 1px solid var(--line); }
.file-list li:last-child { border-bottom: 0; }
.log-cell { max-width: 420px; white-space: normal !important; overflow-wrap: anywhere; font-size: 12.5px; }
-.danger-zone { border-top: 2px solid var(--danger); }
-.danger-zone h3 { color: var(--danger-text); font-size: 12.8px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.04em; }
-.dz-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 0; border-top: 1px solid var(--line); }
-.dz-title { font-size: 14px; font-weight: 600; }
-.dz-desc { font-size: 12.8px; color: var(--muted); }
-@media (max-width: 699px) {
- .dz-row { flex-direction: column; align-items: stretch; }
- .detail-grid { grid-template-columns: 1fr; }
-}
+.danger-zone { box-shadow: none; }
+.danger-zone h3 { color: var(--danger); font-size: 12.8px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 2px; }
+.dz-sub { font-size: 12.8px; color: var(--muted); margin: 0; }
+.dz-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); gap: 8px; margin-top: 10px; }
+.dz-grid .btn { padding: 0 8px; }
+@media (min-width: 700px) { .dz-grid { display: flex; flex-wrap: wrap; } }
.t-ok { color: var(--ok); }
.t-info { color: var(--accent); }
.t-warn { color: var(--warn-text); }
diff --git a/internal/http/templates/apps_detail.html b/internal/http/templates/apps_detail.html
index 801cfc6..4f53ccc 100644
--- a/internal/http/templates/apps_detail.html
+++ b/internal/http/templates/apps_detail.html
@@ -16,29 +16,45 @@
{{.App.Name}} {{.App.Status}}{{if .Domains}}{{index .Domains 0}}{{if .ExtraDomainCount}} +{{.ExtraDomainCount}} more{{end}}:{{.App.Port}}{{else}}Internal service, port {{.App.Port}}{{end}}
- {{if eq .App.Status "running"}}{{template "ui_alert" (dict "Kind" "ok" "Icon" "check" "Title" (printf "Running on %s" .ServerName) "Sub" "")}}
+ {{if eq .App.Status "running"}}{{template "ui_alert" (dict "Kind" "ok" "Icon" "check" "Title" (printf "Running on %s" .ServerName) "Sub" (or (and .Deployments (printf "Last deploy %s · %s" ((index .Deployments 0).CreatedAt.Format "2006-01-02 15:04") (or (index .Deployments 0).Status "unknown"))) ""))}}
{{else if eq .App.Status "stopped"}}{{template "ui_alert" (dict "Kind" "neutral" "Icon" "alert" "Title" "Stopped" "Sub" "The app is not running. Start it to serve traffic.")}}
{{else if eq .App.Status "draft"}}{{template "ui_alert" (dict "Kind" "neutral" "Icon" "alert" "Title" "Draft" "Sub" (or (and (ne .App.ServerID 0) (printf "Draft on %s. Deploy when ready." .ServerName)) "Assign a server and deploy."))}}
{{else if or (eq .App.Status "deploying") (eq .App.Status "pending")}}{{template "ui_alert" (dict "Kind" "warn" "Icon" "refresh" "Title" "Deploying" "Sub" "This page refreshes every 2 seconds.")}}
{{else}}{{template "ui_alert" (dict "Kind" "danger" "Icon" "alert" "Title" (printf "Status: %s" .App.Status) "Sub" (or (and .Deployments (index .Deployments 0).Log) ""))}}{{end}}
-
+
+
+ - Server
- {{if eq .App.ServerID 0}}Not assigned Select one{{else}}{{.ServerName}}{{end}}
+ - Domain{{if gt (len .Domains) 1}}s{{end}}
- {{range $i, $d := .Domains}}{{if $i}}, {{end}}{{$d}}{{end}}{{if not .Domains}}-{{end}}
+ - Port
- {{.App.Port}}
+ - Internal
- {{.ServiceName}}:{{.App.Port}}
+ {{if .App.Command}}- Command
- {{.App.Command}}
{{end}}
+ {{if .App.Ports}}- Ports
- {{.App.Ports | nl2br}}
{{end}}
+ {{if .App.MemoryLimit}}- Memory limit
- {{.App.MemoryLimit}}
{{end}}
+ {{if .App.CPULimit}}- CPU limit
- {{.App.CPULimit}}
{{end}}
+ {{if .App.UlimitsNofile}}- Ulimits (nofile)
- {{.App.UlimitsNofile}}
{{end}}
+ {{if or .App.LogMaxSize .App.LogMaxFile}}- Log limits
- {{if .App.LogMaxSize}}max-size={{.App.LogMaxSize}}{{end}}{{if and .App.LogMaxSize .App.LogMaxFile}}, {{end}}{{if .App.LogMaxFile}}max-file={{.App.LogMaxFile}}{{end}}
{{end}}
+ {{if .App.AuthUser}}- Auth
- basic auth ({{.App.AuthUser}})
{{end}}
+ {{if .App.GitRepo}}- Git repo
- {{.App.GitRepo}}
- Branch
- {{.App.GitBranch}}
{{end}}
+
+
+
@@ -104,24 +120,6 @@
Resource usage
- Details
-
- - Status
- {{.App.Status}}
- - Server
- {{if eq .App.ServerID 0}}Not assigned Select one{{else}}{{.ServerName}}{{end}}
- - Domain{{if gt (len .Domains) 1}}s{{end}}
- {{range $i, $d := .Domains}}{{if $i}}, {{end}}{{$d}}{{end}}{{if not .Domains}}-{{end}}
- - Port
- {{.App.Port}}
- - Internal
- {{.ServiceName}}:{{.App.Port}}
- {{if .App.Command}}- Command
- {{.App.Command}}
{{end}}
- {{if .App.Ports}}- Ports
- {{.App.Ports | nl2br}}
{{end}}
- {{if .App.MemoryLimit}}- Memory limit
- {{.App.MemoryLimit}}
{{end}}
- {{if .App.CPULimit}}- CPU limit
- {{.App.CPULimit}}
{{end}}
- {{if .App.UlimitsNofile}}- Ulimits (nofile)
- {{.App.UlimitsNofile}}
{{end}}
- {{if or .App.LogMaxSize .App.LogMaxFile}}- Log limits
- {{if .App.LogMaxSize}}max-size={{.App.LogMaxSize}}{{end}}{{if and .App.LogMaxSize .App.LogMaxFile}}, {{end}}{{if .App.LogMaxFile}}max-file={{.App.LogMaxFile}}{{end}}
{{end}}
- {{if .App.AuthUser}}- Auth
- basic auth ({{.App.AuthUser}})
{{end}}
- {{if .App.GitRepo}}- Git repo
- {{.App.GitRepo}}
- Branch
- {{.App.GitBranch}}
{{end}}
-
-
Docker Compose
@@ -184,14 +182,13 @@
Logs
Danger zone
- {{if ne .App.Status "draft"}}
-
Redeploy
Rebuild and restart the app. It is briefly unavailable.
-
Rollback
Restore the last successful deployment.
- {{end}}
- {{if .Deployments}}
-
Clear deployment history
Remove all deployment records. This cannot be undone.
- {{end}}
-
{{if eq .App.Status "draft"}}Delete draft{{else}}Undeploy{{end}}
{{if eq .App.Status "draft"}}Permanently delete this draft.{{else}}Stop the app and remove it from {{.ServerName}}.{{end}}
+
Each action asks for confirmation first.
+
+ {{if ne .App.Status "draft"}}
+ {{end}}
+ {{if .Deployments}}{{end}}
+
+
diff --git a/internal/http/templates/servers_detail.html b/internal/http/templates/servers_detail.html
index b083b83..19e7cf6 100644
--- a/internal/http/templates/servers_detail.html
+++ b/internal/http/templates/servers_detail.html
@@ -20,9 +20,19 @@
{{.Server.Name}} {{.Server.Stat
{{else if or (eq .Server.Status "initializing") (eq .Server.Status "pending")}}{{template "ui_alert" (dict "Kind" "warn" "Icon" "refresh" "Title" (printf "Status: %s" .Server.Status) "Sub" "Initialization in progress. This page refreshes every 2 seconds.")}}
{{else}}{{template "ui_alert" (dict "Kind" "danger" "Icon" "alert" "Title" (printf "Status: %s" .Server.Status) "Sub" "Check the SSH key and connection, then run Init worker.")}}{{end}}
-
-
Edit
-
+
+
+ - Host
- {{.Server.Host}}
+ - Port
- {{.Server.Port}}
+ - User
- {{.Server.User}}
+ - SSH key
- {{.Server.SSHKey}}
+ - Host key
- {{with .Server.HostKeyFingerprint}}{{.}}{{else}}not pinned yet{{end}}
+ - Registry
- {{if .Server.RegistryUser}}{{.Server.RegistryHost}} as {{.Server.RegistryUser}}{{else}}—{{end}}
+
+
@@ -62,18 +72,6 @@
Resource usage
- Connection
-
- - Host
- {{.Server.Host}}
- - Port
- {{.Server.Port}}
- - User
- {{.Server.User}}
- - Status
- {{.Server.Status}}
- - SSH key
- {{.Server.SSHKey}}
- - Host key
- {{with .Server.HostKeyFingerprint}}{{.}}{{else}}not pinned yet{{end}}
- - Registry
- {{if .Server.RegistryUser}}{{.Server.RegistryHost}} as {{.Server.RegistryUser}}{{else}}—{{end}}
-
-
{{template "servers_resources_card" .}}
@@ -114,9 +112,12 @@
Apps {{len .Apps}}
Danger zone
-
{{if eq .Server.Status "online"}}Re-run init{{else}}Init worker{{end}}
Installs Docker and Caddy, re-applies the worker config.
-
Prune disk
Remove stopped containers, unused images and build cache.
-
Remove server
Remove from Dockify and delete its SSH key. Cannot be undone.
+
Each action asks for confirmation first.
+
+
+
+
+
diff --git a/internal/http/templates/ui_icon.html b/internal/http/templates/ui_icon.html
index bf966cb..581cbf5 100644
--- a/internal/http/templates/ui_icon.html
+++ b/internal/http/templates/ui_icon.html
@@ -1 +1 @@
-{{define "ui_icon"}}{{end}}
+{{define "ui_icon"}}{{end}}