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
1 change: 1 addition & 0 deletions internal/http/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (

func NewRouter(svc *server.Service, appSvc *app.Service, render RenderFunc, serverListAdapter app.ServerRepo, cfgUser, cfgPass, apiToken, sshKeyDir string, webhookSecretFn func() string, settingsHandler *settings.Handler, backupHandler *backup.Handler, version string, devMock, trustProxy bool) *chi.Mux {
assetVersion = version
noAuth = cfgPass == ""
r := chi.NewRouter()

r.Use(chimw.Logger)
Expand Down
188 changes: 64 additions & 124 deletions internal/http/static/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,6 @@ body:not(.login-page) { padding-bottom: calc(56px + env(safe-area-inset-bottom))
/* Grid System */
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 1em; }
form .grid { gap: 1.2em; margin-bottom: 0.9em; }
.grid-fill > .card { display: flex; flex-direction: column; }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 1em; }

/* Buttons */
Expand Down Expand Up @@ -528,18 +527,6 @@ progress.bar-danger::-moz-progress-bar { background: var(--danger); }
.rows td[data-label]::before { content: attr(data-label); display: block; font-size: 11px; font-weight: 800; text-transform: uppercase; color: var(--muted); }
.col-sm-hide { display: none !important; }
}

/* Tables */
.table-wrap {
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--surface);
box-shadow: var(--shadow);
margin-bottom: 1em;
}
table { width: 100%; border-collapse: collapse; text-align: left; }
thead th {
padding: 0.65em 0.85em;
Expand All @@ -557,39 +544,54 @@ tbody tr:hover td { background: var(--accent-soft); }
table a { font-weight: 500; color: var(--accent); }
table a:hover { color: var(--ink); text-decoration: underline; }

/* Form elements */
/* Form elements (DESIGN-SYSTEM §4, §5): 16px controls, 44px high, radius 12, border `control` */
input, textarea, select {
font-family: inherit;
font-size: 0.92em;
padding: 0.55em 0.8em;
font-size: 16px;
min-height: 44px;
padding: 0 12px;
border-radius: 12px;
border: 1px solid var(--line);
background: var(--bg);
border: 1px solid var(--control);
background: var(--surface);
color: var(--ink);
width: 100%;
outline: none;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
input:focus, textarea:focus, select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-soft);
}
input[type=radio], input[type=checkbox] { width: auto; cursor: pointer; }
input[type=radio] { margin: 0 0.6em 0 0; vertical-align: middle; }
input[type=checkbox] { margin: 0 0.5em 0 0; vertical-align: middle; }
textarea { resize: vertical; min-height: 110px; line-height: 1.5; }
label { font-size: 0.85em; font-weight: 500; color: var(--ink2); margin-bottom: 0.5em; display: block; }
fieldset { border: none; padding: 0; margin: 0 0 1.5em; }
legend {
font-size: 0.8em;
color: var(--ink2);
margin-bottom: 0.5em;
text-transform: uppercase;
letter-spacing: 0.04em;
font-weight: 600;
}
small { font-size: 0.82em; color: var(--muted); display: block; margin-top: 0.35em; }
.grid label { margin-bottom: 0; }
transition: border-color 0.15s ease;
}
textarea { padding: 10px 12px; resize: vertical; min-height: 96px; line-height: 1.5; }
textarea.mono, input.mono { font-family: 'JetBrains Mono', ui-monospace, monospace; font-size: 13px; }
input:focus-visible, textarea:focus-visible, select:focus-visible { outline: 3px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
input[type=radio], input[type=checkbox] { width: 20px; height: 20px; min-height: 0; padding: 0; accent-color: var(--accent-fill); cursor: pointer; flex-shrink: 0; }
input[type=file] { padding: 8px 12px; }
input[readonly] { background: var(--sunk); }
.field, label { display: flex; flex-direction: column; gap: 4px; margin-bottom: 12px; font-size: 15px; color: var(--ink); }
.field-label { font-size: 12.8px; font-weight: 600; color: var(--ink2); }
.field-hint, small { font-size: 12px; color: var(--muted); display: block; margin-top: 0; }
.field-error { font-size: 12.8px; color: var(--danger-text); }
.field-error[hidden] { display: none; }
.check { flex-direction: row; align-items: flex-start; gap: 8px; cursor: pointer; font-size: 15px; }
.select-wrap { position: relative; display: block; }
.select-wrap select { appearance: none; -webkit-appearance: none; padding-right: 40px; }
.select-wrap .icon { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); width: 18px; height: 18px; color: var(--muted); pointer-events: none; }
fieldset { border: none; padding: 0; margin: 0 0 16px; }
legend { font-size: 12.8px; color: var(--ink2); margin-bottom: 8px; font-weight: 600; }
details { margin-bottom: 12px; }
summary { cursor: pointer; font-size: 13px; font-weight: 600; color: var(--accent); padding: 8px 0; }
.card-sub { font-size: 12.8px; color: var(--muted); margin: -8px 0 12px; }
.form-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 8px; }
@media (min-width: 700px) { .form-actions { display: flex; } }
.row-grid { display: grid; gap: 8px; margin-bottom: 8px; align-items: start; }
.row-grid > * { min-width: 0; }
.row-domain { grid-template-columns: minmax(0, 1fr) 44px; }
.row-env { grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr) 40px 40px; }
.row-file { grid-template-columns: minmax(0, 1fr) auto 40px; }
.row-grid input, .row-grid textarea { margin: 0; }
.row-env textarea { min-height: 44px; }
.inline-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
.opt-group { display: flex; flex-direction: column; gap: 4px; margin-bottom: 12px; }
.opt-group legend { margin-bottom: 4px; }
.secret-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.secret-row input { flex: 1 1 200px; min-width: 0; font-family: 'JetBrains Mono', ui-monospace, monospace; font-size: 13px; }
.kpi-grid { grid-template-columns: repeat(2, 1fr); }
@media (min-width: 700px) { .kpi-grid { grid-template-columns: repeat(4, 1fr); } }

Expand Down Expand Up @@ -640,11 +642,6 @@ small { font-size: 0.82em; color: var(--muted); display: block; margin-top: 0.35
.breadcrumb { display: flex; align-items: center; gap: 0.4em; font-size: 0.88em; color: var(--ink2); margin-bottom: 1em; }
.breadcrumb a { color: var(--ink2); font-weight: 500; }
.breadcrumb a:hover { color: var(--accent); }
.breadcrumb span.sep { color: var(--muted); font-size: 0.8em; }

/* Button Group Layout */
.btn-group { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.6em; margin: 1.2em 0; }
.btn-group-left, .btn-group-right { display: flex; gap: 0.5em; align-items: center; flex-wrap: wrap; }

/* Code blocks & Log viewer */
pre, code { font-size: 0.88em; }
Expand All @@ -657,9 +654,6 @@ pre {
line-height: 1.5;
box-shadow: var(--shadow);
}

.log-container { margin: 1.2em 0; }
.log-toolbar { display: flex; gap: 0.4em; margin-bottom: 0.6em; }
.log-content {
background: var(--term-bg);
border: 1px solid var(--line);
Expand All @@ -683,7 +677,6 @@ pre {
gap: 0.6em;
}
.secret-row code { flex: 1; font-size: 0.85em; }
.secret-row .secret-val { color: var(--ink2); font-size: 0.85em; }
.secret-row input { width: auto; min-width: 160px; padding: 0.4em 0.6em; font-size: 0.85em; }

/* Empty state */
Expand Down Expand Up @@ -745,30 +738,16 @@ pre {
border-bottom: 1px solid var(--line);
background: var(--term-bar);
}
.console-dots { display: flex; gap: 0.35rem; }
.console-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
.console-dot-red { background: var(--danger); }
.console-dot-yellow { background: var(--warn); }
.console-dot-green { background: var(--ok); }
#terminal { padding: 0.4em; }

/* Gauges & Live Charts */
.stats-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.2em; }
.stats-header h3 { margin: 0; }

.stats-gauges { display: grid; grid-template-columns: repeat(5, 1fr); gap: 0.8em 1.5em; margin-bottom: 0.85em; }
.stats-gauges--resources { grid-template-columns: 0.7fr 1.65fr 0.65fr 1.1fr 1.1fr; }
.stat-gauge { min-width: 0; }
.stat-gauge-label { font-size: 0.7em; color: var(--ink2); font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; }
.stat-gauge-header { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 0.2em; }
.stat-gauge-row { display: flex; align-items: center; gap: 0.4em; }
.stat-gauge-bar { width: 100%; height: 7px; background: var(--line); border-radius: 4px; overflow: hidden; }
.stat-gauge-fill { height: 100%; border-radius: 4px; transition: width 0.5s ease; }
.stat-gauge-cpu { background: var(--ink2); }
.stat-gauge-mem { background: var(--ok); }
.stat-gauge-disk { background: var(--accent); }
.stat-gauge-value { font-size: 0.85em; font-weight: 600; font-family: 'JetBrains Mono', monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.stat-gauge-detail { font-size: 0.8em; color: var(--ink2); font-weight: 400; }
.stat-gauge-net { font-size: 0.85em; font-weight: 400; color: var(--ink); line-height: 1.4; word-break: break-word; font-family: 'JetBrains Mono', monospace; }
.stats-divider { border: none; border-top: 1px solid var(--line); margin: 1.2em 0 1em 0; }

Expand Down Expand Up @@ -800,70 +779,17 @@ pre {
.grid:not(.kpi-grid) { grid-template-columns: 1fr; }
.grid-2 { grid-template-columns: 1fr; }
.stats-gauges { grid-template-columns: 1fr 1fr; }
.btn-group { flex-direction: column; align-items: stretch; }
.btn-group-left, .btn-group-right { width: 100%; }
.btn-group-left > *, .btn-group-right > * { flex: 1 1 auto; }
}

/* Login page (body.login-page) */
.login-page {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 1em;
}
.login-card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: 12px;
padding: 2.2em 2em;
width: 360px;
max-width: 100%;
box-shadow: var(--shadow);
}
.brand-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.4rem; }
.brand-icon {
width: 30px;
height: 30px;
border-radius: 8px;
background: linear-gradient(135deg, var(--accent-fill), var(--chart));
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--on-accent);
font-size: 0.95em;
font-weight: 800;
box-shadow: var(--shadow);
}
.login-card h1 { font-size: 1.35em; font-weight: 700; margin: 0; letter-spacing: -0.02em; }
.login-card p { color: var(--ink2); margin: 0.2em 0 1.5em; font-size: 0.9em; }
.login-card .error {
background: var(--danger-bg);
color: var(--danger-text);
border: 1px solid color-mix(in srgb, var(--danger) 25%, transparent);
border-radius: 6px;
padding: 0.6em 0.8em;
font-size: 0.85em;
font-weight: 500;
margin-bottom: 1.2em;
}
.login-card label { font-size: 0.85em; font-weight: 500; color: var(--ink2); display: block; margin-bottom: 0.4em; }
.login-card input {
font-family: inherit;
font-size: 0.92em;
padding: 0.6em 0.85em;
border-radius: 6px;
border: 1px solid var(--control);
background: var(--bg);
color: var(--ink);
width: 100%;
outline: none;
margin-bottom: 1.2em;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.login-card input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.login-card .btn-primary { width: 100%; text-align: center; justify-content: center; }
.login-page { display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 16px; }
.login-card { background: var(--surface); border: 1px solid var(--line); border-radius: 16px; padding: 28px 24px; width: 360px; max-width: 100%; box-shadow: var(--shadow); }
.brand-header { display: flex; align-items: center; gap: 10px; margin-bottom: 4px; }
.brand-icon { width: 32px; height: 32px; border-radius: 8px; background: linear-gradient(135deg, var(--accent-fill), var(--chart)); display: inline-flex; align-items: center; justify-content: center; color: var(--on-accent); font-weight: 800; }
.login-card h1 { font-size: 20px; font-weight: 800; margin: 0; }
.login-card p { color: var(--ink2); margin: 0 0 20px; font-size: 14px; }
.login-card .alert { margin-bottom: 16px; }
.login-card .btn-primary { width: 100%; }
.login-page .theme-btn { position: fixed; top: 12px; right: 12px; }

/* Detail pages (DESIGN-SYSTEM §6) */
Expand Down Expand Up @@ -955,3 +881,17 @@ pre.code { margin: 0; padding: 12px; background: var(--sunk); border: 1px solid
.rows-apps td.c-name { grid-column: 1; }
.rows-apps td.c-act { grid-column: 1 / -1; }
}

/* Forms: segmented radios, page actions */
.seg-form { margin-bottom: 16px; }
.seg-radio { position: relative; display: block; margin: 0; flex-direction: row; }
.seg-radio input { position: absolute; opacity: 0; inset: 0; width: 100%; height: 100%; min-height: 0; cursor: pointer; }
.seg-radio span { display: flex; align-items: center; min-height: 32px; padding: 0 16px; border-radius: 10px; font-size: 12.8px; font-weight: 600; color: var(--ink2); }
.seg-radio input:checked + span { background: var(--surface); color: var(--ink); box-shadow: var(--shadow); }
.seg-radio input:focus-visible + span { outline: 3px solid var(--accent); outline-offset: 1px; }
@media (max-width: 699px) { .seg-radio span { min-height: 44px; } }
.group-title { font-size: 12.8px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.04em; color: var(--ink2); margin: 16px 0 4px; }
.page-actions { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin-bottom: 16px; }
/* iOS zooms inputs below 16px: keep mono fields at 16px on phones */
@media (max-width: 699px) { input.mono, textarea.mono, .secret-row input { font-size: 16px; } }
.row-domain > input:only-child { grid-column: 1 / -1; }
4 changes: 4 additions & 0 deletions internal/http/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,9 @@ func GetDevMock(r *http.Request) bool {
// assetVersion busts the browser cache of /static/app.css on upgrade; set by NewRouter.
var assetVersion string

// noAuth hides the Sign out button when no admin password is configured; set by NewRouter.
var noAuth bool

// navSection maps the request path to the active main-nav item.
func navSection(r *http.Request) string {
p := "/" + strings.TrimPrefix(strings.TrimPrefix(r.URL.Path, strings.TrimSuffix(web.GetBasePath(r), "/")), "/")
Expand Down Expand Up @@ -275,6 +278,7 @@ func Render(w http.ResponseWriter, r *http.Request, status int, name string, dat
m["DevMock"] = GetDevMock(r)
m["AssetVersion"] = assetVersion
m["Nav"] = navSection(r)
m["NoAuth"] = noAuth
}

var buf bytes.Buffer
Expand Down
29 changes: 14 additions & 15 deletions internal/http/templates/about.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,25 @@ <h3>What is Dockify?</h3>
<p>Built with Go, HTMX, SQLite, and a fully custom CSS interface — no JavaScript frameworks, no build steps.</p>
</div>

<div class="card" style="margin-top:1em">
<div class="card">
<h3>Update</h3>
<p style="color:var(--ink2);margin-top:0">
Current: <strong id="cur-ver">v{{.Version}}</strong><br>
Latest: <strong id="lat-ver">-</strong>
</p>
<div class="btn-group">
<button class="btn btn-secondary" onclick="checkUpdate()">Check for Updates</button>
<button class="btn btn-destructive-outline" id="update-btn" style="display:none" onclick="openSheet('update-confirm')">Update Now</button>
<dl class="detail-grid">
<div><dt>Current</dt><dd class="mono" id="cur-ver">v{{.Version}}</dd></div>
<div><dt>Latest</dt><dd class="mono" id="lat-ver">-</dd></div>
</dl>
<div class="inline-actions">
<button class="btn btn-secondary" onclick="checkUpdate()">Check for updates</button>
<button class="btn btn-destructive-outline" id="update-btn" hidden onclick="openSheet('update-confirm')">Update now</button>
</div>
<pre id="update-log" style="display:none;margin-top:0.5em;font-size:0.8em;max-height:200px;overflow:auto"></pre>
<pre class="code" id="update-log" hidden></pre>
</div>

<div class="card" style="margin-top:1em">
<div class="card">
<h3>Sponsor & Donation</h3>
<p>Dockify is open source and free to use. If you find it useful, consider supporting the project via GitHub Sponsors.</p>
<a href="https://github.com/sponsors/coderbuzz" class="btn btn-secondary" target="_blank" rel="noopener">Sponsor on GitHub</a>
</div>

<p style="text-align:center;color:var(--ink2);margin-top:2em;font-size:0.85em">Version {{.Version}}</p>
</div>
{{template "ui_confirm" (dict "ID" "update-confirm" "Title" "Update Dockify now?" "Body" "The server restarts to apply the update and is unavailable for a short time." "Label" "Update")}}
<script>
Expand All @@ -52,16 +51,16 @@ <h3>Sponsor & Donation</h3>
if (d.latest) {
document.getElementById('lat-ver').textContent = d.latest;
if (d.has_update) {
document.getElementById('update-btn').style.display = 'inline-block';
document.getElementById('update-btn').hidden = false;
document.getElementById('update-btn').textContent = 'Update to ' + d.latest;
} else {
document.getElementById('update-btn').style.display = 'none';
document.getElementById('update-btn').hidden = true;
}
} else {
document.getElementById('lat-ver').textContent = d.error || 'failed';
}
btn.disabled = false;
btn.textContent = 'Check for Updates';
btn.textContent = 'Check for updates';
});
}
function runUpdate() {
Expand All @@ -70,7 +69,7 @@ <h3>Sponsor & Donation</h3>
btn.disabled = true;
btn.textContent = 'Updating...';
var log = document.getElementById('update-log');
log.style.display = 'block';
log.hidden = false;
log.textContent = 'Starting update...\n';
var versionOnPage = '{{.Version}}';
fetch('api/settings/update/run', {method:'POST'}).then(function(r){return r.json()}).then(function(d){
Expand Down
2 changes: 1 addition & 1 deletion internal/http/templates/apps.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
{{if .Flash}}<div class="toast" role="status">{{.Flash}}</div>{{end}}
<hgroup>
<h1>Apps</h1>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1em">
<div class="page-actions">
<a href="apps/add" class="btn btn-brand">Deploy App</a>
<button type="button" class="btn btn-destructive-outline" onclick="openSheet('redeploy-all')">Redeploy All</button>
</div>
Expand Down
Loading
Loading