Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 19 additions & 20 deletions internal/http/static/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand All @@ -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); }
Expand All @@ -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); }
Expand Down
67 changes: 32 additions & 35 deletions internal/http/templates/apps_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,45 @@ <h1>{{.App.Name}} <span class="badge badge-{{.App.Status}}">{{.App.Status}}</spa
<p class="meta">{{if .Domains}}{{index .Domains 0}}{{if .ExtraDomainCount}} <span class="badge">+{{.ExtraDomainCount}} more</span>{{end}}:{{.App.Port}}{{else}}Internal service, port {{.App.Port}}{{end}}</p>
</div>

{{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}}

<div class="action-bar">
<div class="card detail-card">
<dl class="detail-grid">
<div><dt>Server</dt><dd>{{if eq .App.ServerID 0}}Not assigned <a href="apps/{{.App.ID}}/edit">Select one</a>{{else}}{{.ServerName}}{{end}}</dd></div>
<div><dt>Domain{{if gt (len .Domains) 1}}s{{end}}</dt><dd class="mono">{{range $i, $d := .Domains}}{{if $i}}, {{end}}{{$d}}{{end}}{{if not .Domains}}-{{end}}</dd></div>
<div><dt>Port</dt><dd class="mono">{{.App.Port}}</dd></div>
<div><dt>Internal</dt><dd class="mono">{{.ServiceName}}:{{.App.Port}}</dd></div>
{{if .App.Command}}<div><dt>Command</dt><dd class="mono">{{.App.Command}}</dd></div>{{end}}
{{if .App.Ports}}<div><dt>Ports</dt><dd class="mono">{{.App.Ports | nl2br}}</dd></div>{{end}}
{{if .App.MemoryLimit}}<div><dt>Memory limit</dt><dd>{{.App.MemoryLimit}}</dd></div>{{end}}
{{if .App.CPULimit}}<div><dt>CPU limit</dt><dd>{{.App.CPULimit}}</dd></div>{{end}}
{{if .App.UlimitsNofile}}<div><dt>Ulimits (nofile)</dt><dd>{{.App.UlimitsNofile}}</dd></div>{{end}}
{{if or .App.LogMaxSize .App.LogMaxFile}}<div><dt>Log limits</dt><dd>{{if .App.LogMaxSize}}max-size={{.App.LogMaxSize}}{{end}}{{if and .App.LogMaxSize .App.LogMaxFile}}, {{end}}{{if .App.LogMaxFile}}max-file={{.App.LogMaxFile}}{{end}}</dd></div>{{end}}
{{if .App.AuthUser}}<div><dt>Auth</dt><dd>basic auth ({{.App.AuthUser}})</dd></div>{{end}}
{{if .App.GitRepo}}<div><dt>Git repo</dt><dd class="mono">{{.App.GitRepo}}</dd></div><div><dt>Branch</dt><dd class="mono">{{.App.GitBranch}}</dd></div>{{end}}
</dl>
<div class="actgrid">
{{if eq .App.Status "draft"}}
<form method="POST" action="apps/{{.App.ID}}/deploy">
<button type="submit" class="btn btn-primary"{{if eq .App.ServerID 0}} disabled title="Assign a server in Edit first"{{end}}>Deploy</button>
<button type="submit" class="btn btn-primary"{{if eq .App.ServerID 0}} disabled title="Assign a server in Edit first"{{end}}>{{template "ui_icon" "rocket"}}Deploy</button>
</form>
<a href="apps/{{.App.ID}}/edit" class="btn btn-secondary">Edit</a>
<a href="apps/{{.App.ID}}/edit" class="btn btn-secondary">{{template "ui_icon" "edit"}}Edit</a>
{{else}}
{{if eq .App.Status "stopped"}}
<form method="POST" action="apps/{{.App.ID}}/start"><button type="submit" class="btn btn-primary">Start</button></form>
<a href="apps/{{.App.ID}}/edit" class="btn btn-secondary">Edit</a>
<form method="POST" action="apps/{{.App.ID}}/start"><button type="submit" class="btn btn-primary">{{template "ui_icon" "play"}}Start</button></form>
<a href="apps/{{.App.ID}}/edit" class="btn btn-secondary">{{template "ui_icon" "edit"}}Edit</a>
{{else}}
<a href="apps/{{.App.ID}}/edit" class="btn btn-primary">Edit</a>
{{if eq .App.Status "running"}}<form method="POST" action="apps/{{.App.ID}}/stop"><button type="submit" class="btn btn-secondary">Stop</button></form>{{end}}
<a href="apps/{{.App.ID}}/edit" class="btn btn-primary">{{template "ui_icon" "edit"}}Edit</a>
{{if eq .App.Status "running"}}<form method="POST" action="apps/{{.App.ID}}/stop"><button type="submit" class="btn btn-secondary">{{template "ui_icon" "square"}}Stop</button></form>{{end}}
{{end}}
<button class="btn btn-ghost" id="app-console-toggle" data-app-id="{{.App.ID}}" onclick="toggleAppConsole()">Console</button>
<button class="btn btn-ghost" id="app-console-toggle" data-app-id="{{.App.ID}}" onclick="toggleAppConsole()">{{template "ui_icon" "terminal"}}Console</button>
{{end}}
</div>
</div>

<div class="console-section">
<div id="app-console-container" class="console-container" hidden>
Expand Down Expand Up @@ -104,24 +120,6 @@ <h3>Resource usage <span id="app-live-indicator" class="badge badge-online live-
</script>
{{end}}

<div class="card">
<h3>Details</h3>
<dl class="detail-grid">
<div><dt>Status</dt><dd><span class="badge badge-{{.App.Status}}">{{.App.Status}}</span></dd></div>
<div><dt>Server</dt><dd>{{if eq .App.ServerID 0}}Not assigned <a href="apps/{{.App.ID}}/edit">Select one</a>{{else}}{{.ServerName}}{{end}}</dd></div>
<div><dt>Domain{{if gt (len .Domains) 1}}s{{end}}</dt><dd class="mono">{{range $i, $d := .Domains}}{{if $i}}, {{end}}{{$d}}{{end}}{{if not .Domains}}-{{end}}</dd></div>
<div><dt>Port</dt><dd class="mono">{{.App.Port}}</dd></div>
<div><dt>Internal</dt><dd class="mono">{{.ServiceName}}:{{.App.Port}}</dd></div>
{{if .App.Command}}<div><dt>Command</dt><dd class="mono">{{.App.Command}}</dd></div>{{end}}
{{if .App.Ports}}<div><dt>Ports</dt><dd class="mono">{{.App.Ports | nl2br}}</dd></div>{{end}}
{{if .App.MemoryLimit}}<div><dt>Memory limit</dt><dd>{{.App.MemoryLimit}}</dd></div>{{end}}
{{if .App.CPULimit}}<div><dt>CPU limit</dt><dd>{{.App.CPULimit}}</dd></div>{{end}}
{{if .App.UlimitsNofile}}<div><dt>Ulimits (nofile)</dt><dd>{{.App.UlimitsNofile}}</dd></div>{{end}}
{{if or .App.LogMaxSize .App.LogMaxFile}}<div><dt>Log limits</dt><dd>{{if .App.LogMaxSize}}max-size={{.App.LogMaxSize}}{{end}}{{if and .App.LogMaxSize .App.LogMaxFile}}, {{end}}{{if .App.LogMaxFile}}max-file={{.App.LogMaxFile}}{{end}}</dd></div>{{end}}
{{if .App.AuthUser}}<div><dt>Auth</dt><dd>basic auth ({{.App.AuthUser}})</dd></div>{{end}}
{{if .App.GitRepo}}<div><dt>Git repo</dt><dd class="mono">{{.App.GitRepo}}</dd></div><div><dt>Branch</dt><dd class="mono">{{.App.GitBranch}}</dd></div>{{end}}
</dl>
</div>

<div class="card">
<h3>Docker Compose</h3>
Expand Down Expand Up @@ -184,14 +182,13 @@ <h3>Logs</h3>

<div class="card danger-zone">
<h3>Danger zone</h3>
{{if ne .App.Status "draft"}}
<div class="dz-row"><div><div class="dz-title">Redeploy</div><div class="dz-desc">Rebuild and restart the app. It is briefly unavailable.</div></div><button type="button" class="btn btn-destructive-outline btn-sm" onclick="openSheet('app-redeploy')">Redeploy</button></div>
<div class="dz-row"><div><div class="dz-title">Rollback</div><div class="dz-desc">Restore the last successful deployment.</div></div><button type="button" class="btn btn-destructive-outline btn-sm" onclick="openSheet('app-rollback')">Rollback</button></div>
{{end}}
{{if .Deployments}}
<div class="dz-row"><div><div class="dz-title">Clear deployment history</div><div class="dz-desc">Remove all deployment records. This cannot be undone.</div></div><button type="button" class="btn btn-destructive-outline btn-sm" onclick="openSheet('app-clear-history')">Clear history</button></div>
{{end}}
<div class="dz-row"><div><div class="dz-title">{{if eq .App.Status "draft"}}Delete draft{{else}}Undeploy{{end}}</div><div class="dz-desc">{{if eq .App.Status "draft"}}Permanently delete this draft.{{else}}Stop the app and remove it from {{.ServerName}}.{{end}}</div></div><button type="button" class="btn btn-destructive-outline btn-sm" onclick="openSheet('app-undeploy')">{{if eq .App.Status "draft"}}Delete draft{{else}}Undeploy{{end}}</button></div>
<p class="dz-sub">Each action asks for confirmation first.</p>
<div class="dz-grid">
{{if ne .App.Status "draft"}}<button type="button" class="btn btn-destructive-outline" onclick="openSheet('app-redeploy')">Redeploy</button>
<button type="button" class="btn btn-destructive-outline" onclick="openSheet('app-rollback')">Rollback</button>{{end}}
{{if .Deployments}}<button type="button" class="btn btn-destructive-outline" onclick="openSheet('app-clear-history')">Clear history</button>{{end}}
<button type="button" class="btn btn-destructive-outline" onclick="openSheet('app-undeploy')">{{if eq .App.Status "draft"}}Delete draft{{else}}Undeploy{{end}}</button>
</div>
</div>
</div>

Expand Down
37 changes: 19 additions & 18 deletions internal/http/templates/servers_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,19 @@ <h1>{{.Server.Name}} <span class="badge badge-{{.Server.Status}}">{{.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}}

<div class="action-bar">
<a href="servers/{{.Server.ID}}/edit" class="btn btn-primary">Edit</a>
<button class="btn btn-ghost" id="console-toggle" data-server-id="{{.Server.ID}}" onclick="toggleConsole()">Console</button>
<div class="card detail-card">
<dl class="detail-grid">
<div><dt>Host</dt><dd class="mono">{{.Server.Host}}</dd></div>
<div><dt>Port</dt><dd class="mono">{{.Server.Port}}</dd></div>
<div><dt>User</dt><dd class="mono">{{.Server.User}}</dd></div>
<div class="wide"><dt>SSH key</dt><dd class="mono wrap">{{.Server.SSHKey}}</dd></div>
<div class="wide"><dt>Host key</dt><dd class="mono wrap">{{with .Server.HostKeyFingerprint}}{{.}}{{else}}not pinned yet{{end}}</dd></div>
<div class="wide"><dt>Registry</dt><dd>{{if .Server.RegistryUser}}{{.Server.RegistryHost}} as {{.Server.RegistryUser}}{{else}}&mdash;{{end}}</dd></div>
</dl>
<div class="actgrid">
<a href="servers/{{.Server.ID}}/edit" class="btn btn-primary">{{template "ui_icon" "edit"}}Edit</a>
<button class="btn btn-ghost" id="console-toggle" data-server-id="{{.Server.ID}}" onclick="toggleConsole()">{{template "ui_icon" "terminal"}}Console</button>
</div>
</div>

<div class="console-section">
Expand Down Expand Up @@ -62,18 +72,6 @@ <h3>Resource usage <span id="live-indicator" class="badge badge-online live-badg
</div>
{{end}}

<div class="card">
<h3>Connection</h3>
<dl class="detail-grid">
<div><dt>Host</dt><dd class="mono">{{.Server.Host}}</dd></div>
<div><dt>Port</dt><dd class="mono">{{.Server.Port}}</dd></div>
<div><dt>User</dt><dd class="mono">{{.Server.User}}</dd></div>
<div><dt>Status</dt><dd><span class="badge badge-{{.Server.Status}}">{{.Server.Status}}</span></dd></div>
<div class="wide"><dt>SSH key</dt><dd class="mono wrap">{{.Server.SSHKey}}</dd></div>
<div class="wide"><dt>Host key</dt><dd class="mono wrap">{{with .Server.HostKeyFingerprint}}{{.}}{{else}}not pinned yet{{end}}</dd></div>
<div class="wide"><dt>Registry</dt><dd>{{if .Server.RegistryUser}}{{.Server.RegistryHost}} as {{.Server.RegistryUser}}{{else}}&mdash;{{end}}</dd></div>
</dl>
</div>

{{template "servers_resources_card" .}}

Expand Down Expand Up @@ -114,9 +112,12 @@ <h3>Apps <span class="count">{{len .Apps}}</span></h3>

<div class="card danger-zone">
<h3>Danger zone</h3>
<div class="dz-row"><div><div class="dz-title">{{if eq .Server.Status "online"}}Re-run init{{else}}Init worker{{end}}</div><div class="dz-desc">Installs Docker and Caddy, re-applies the worker config.</div></div><button type="button" class="btn btn-destructive-outline btn-sm" onclick="openSheet('server-init')">{{if eq .Server.Status "online"}}Re-run init{{else}}Init worker{{end}}</button></div>
<div class="dz-row"><div><div class="dz-title">Prune disk</div><div class="dz-desc">Remove stopped containers, unused images and build cache.</div></div><button type="button" class="btn btn-destructive-outline btn-sm" onclick="openSheet('prune-modal')">Prune disk</button></div>
<div class="dz-row"><div><div class="dz-title">Remove server</div><div class="dz-desc">Remove from Dockify and delete its SSH key. Cannot be undone.</div></div><button type="button" class="btn btn-destructive-outline btn-sm" onclick="openSheet('server-remove')">Remove server</button></div>
<p class="dz-sub">Each action asks for confirmation first.</p>
<div class="dz-grid">
<button type="button" class="btn btn-destructive-outline" onclick="openSheet('server-init')">{{if eq .Server.Status "online"}}Re-run init{{else}}Init worker{{end}}</button>
<button type="button" class="btn btn-destructive-outline" onclick="openSheet('prune-modal')">Prune disk</button>
<button type="button" class="btn btn-destructive-outline" onclick="openSheet('server-remove')">Remove server</button>
</div>
</div>

</div>
Expand Down
Loading
Loading