diff --git a/internal/http/static/app.css b/internal/http/static/app.css index 50b389e..7217699 100644 --- a/internal/http/static/app.css +++ b/internal/http/static/app.css @@ -204,29 +204,8 @@ color-scheme: dark; } -/* Layer-1 aliases: legacy names mapped onto the layer-2 tokens above. Removed step by step in S2..S6. */ +/* xterm console chrome stays dark in both themes (matches the xterm theme in the templates) */ :root { - --bg-elevated: var(--surface); - --bg-card: var(--surface); - --border: var(--line); - --border-hover: var(--line-strong); - --border-focus: var(--accent); - --text: var(--ink); - --text-muted: var(--ink2); - --text-dim: var(--muted); - --accent-light: var(--accent-soft); - --green: var(--ok); - --green-bg: var(--ok-bg); - --red: var(--danger); - --yellow: var(--warn); - --orange: var(--ink2); - --blue: var(--accent); - --link: var(--accent); - --link-hover: color-mix(in srgb, var(--accent) 80%, var(--ink)); - --shadow-sm: var(--shadow); - --shadow-md: var(--shadow); - --shadow-lg: var(--shadow); - /* xterm console chrome stays dark in both themes (matches the xterm theme in the templates) */ --term-bg: #090d16; --term-bar: #0f172a; --term-fg: #cbd5e1; @@ -236,7 +215,7 @@ body { margin: 0; background: var(--bg); - color: var(--text); + color: var(--ink); font-family: 'Plus Jakarta Sans', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 15px; line-height: 1.5; @@ -248,15 +227,15 @@ code, pre, .mono, .badge, .secret-row input, .secret-row code, td.mono, th.mono font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', 'Cascadia Code', monospace; } -a { color: var(--link); text-decoration: none; transition: color 0.15s ease; } -a:hover { color: var(--link-hover); } +a { color: var(--accent); text-decoration: none; transition: color 0.15s ease; } +a:hover { color: var(--ink); } -h1, h2, h3, h4 { font-weight: 600; margin: 0 0 0.4em; color: var(--text); letter-spacing: -0.015em; } +h1, h2, h3, h4 { font-weight: 600; margin: 0 0 0.4em; color: var(--ink); letter-spacing: -0.015em; } h1 { font-size: 1.5em; font-weight: 700; } -h2 { font-size: 1.15em; color: var(--text); font-weight: 600; margin-top: 1.8em; margin-bottom: 0.6em; } +h2 { font-size: 1.15em; color: var(--ink); font-weight: 600; margin-top: 1.8em; margin-bottom: 0.6em; } hgroup { margin-bottom: 1.2em; } hgroup h1 { margin-bottom: 0.15em; } -hgroup p { color: var(--text-muted); margin: 0; font-size: 0.95em; } +hgroup p { color: var(--ink2); margin: 0; font-size: 0.95em; } /* Shell: sticky header (56px) + main nav (bottom tab bar < 900px, tab row under the header >= 900px) */ .top-nav { @@ -335,16 +314,16 @@ body:not(.login-page) { padding-bottom: calc(56px + env(safe-area-inset-bottom)) /* Cards & Elevation */ .card { - background: var(--bg-card); - border: 1px solid var(--border); + background: var(--surface); + border: 1px solid var(--line); border-radius: 16px; padding: 1.25em 1.4em; margin-bottom: 1em; - box-shadow: var(--shadow-sm); + box-shadow: var(--shadow); transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease; } -.card:hover { border-color: var(--border-hover); box-shadow: var(--shadow-md); } -.card h3 { font-size: 0.85em; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.8em; font-weight: 600; } +.card:hover { border-color: var(--line-strong); box-shadow: var(--shadow); } +.card h3 { font-size: 0.85em; color: var(--ink2); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.8em; font-weight: 600; } .card p { font-size: 0.9em; margin: 0.3em 0; } /* Grid System */ @@ -363,17 +342,17 @@ form .grid { gap: 1.2em; margin-bottom: 0.9em; } font-size: 0.9em; font-family: inherit; border-radius: 6px; - border: 1px solid var(--border); - background: var(--bg-elevated); - color: var(--text); + border: 1px solid var(--line); + background: var(--surface); + color: var(--ink); cursor: pointer; transition: all 0.15s ease; line-height: 1.45; text-decoration: none; font-weight: 500; - box-shadow: var(--shadow-sm); + box-shadow: var(--shadow); } -[role=button]:hover, button:hover { border-color: var(--border-hover); background: var(--bg-card); color: var(--text); } +[role=button]:hover, button:hover { border-color: var(--line-strong); background: var(--surface); color: var(--ink); } .btn { display: inline-flex; align-items: center; @@ -555,28 +534,28 @@ progress.bar-danger::-moz-progress-bar { background: var(--danger); } width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; - border: 1px solid var(--border); + border: 1px solid var(--line); border-radius: 8px; - background: var(--bg-card); - box-shadow: var(--shadow-sm); + 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; - border-bottom: 1px solid var(--border); - background: var(--bg-elevated); - color: var(--text-muted); + border-bottom: 1px solid var(--line); + background: var(--surface); + color: var(--ink2); font-weight: 600; font-size: 0.78em; text-transform: uppercase; letter-spacing: 0.05em; } -tbody td { padding: 0.65em 0.85em; border-bottom: 1px solid var(--border); vertical-align: middle; font-size: 0.9em; } +tbody td { padding: 0.65em 0.85em; border-bottom: 1px solid var(--line); vertical-align: middle; font-size: 0.9em; } tbody tr:last-child td { border-bottom: none; } -tbody tr:hover td { background: var(--accent-light); } -table a { font-weight: 500; color: var(--link); } -table a:hover { color: var(--link-hover); text-decoration: underline; } +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 */ input, textarea, select { @@ -584,32 +563,32 @@ input, textarea, select { font-size: 0.92em; padding: 0.55em 0.8em; border-radius: 12px; - border: 1px solid var(--border); + border: 1px solid var(--line); background: var(--bg); - color: var(--text); + 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(--border-focus); - box-shadow: 0 0 0 3px var(--accent-light); + 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(--text-muted); margin-bottom: 0.5em; display: block; } +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(--text-muted); + color: var(--ink2); margin-bottom: 0.5em; text-transform: uppercase; letter-spacing: 0.04em; font-weight: 600; } -small { font-size: 0.82em; color: var(--text-dim); display: block; margin-top: 0.35em; } +small { font-size: 0.82em; color: var(--muted); display: block; margin-top: 0.35em; } .grid label { margin-bottom: 0; } .kpi-grid { grid-template-columns: repeat(2, 1fr); } @media (min-width: 700px) { .kpi-grid { grid-template-columns: repeat(4, 1fr); } } @@ -658,10 +637,10 @@ small { font-size: 0.82em; color: var(--text-dim); display: block; margin-top: 0 .stat small { font-size: 12.8px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; } /* Breadcrumbs */ -.breadcrumb { display: flex; align-items: center; gap: 0.4em; font-size: 0.88em; color: var(--text-muted); margin-bottom: 1em; } -.breadcrumb a { color: var(--text-muted); font-weight: 500; } +.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(--text-dim); font-size: 0.8em; } +.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; } @@ -670,20 +649,20 @@ small { font-size: 0.82em; color: var(--text-dim); display: block; margin-top: 0 /* Code blocks & Log viewer */ pre, code { font-size: 0.88em; } pre { - background: var(--bg-elevated); - border: 1px solid var(--border); + background: var(--surface); + border: 1px solid var(--line); padding: 0.85em 1em; border-radius: 8px; overflow-x: auto; line-height: 1.5; - box-shadow: var(--shadow-sm); + 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(--border); + border: 1px solid var(--line); padding: 0.8em 1em; border-radius: 8px; max-height: 420px; @@ -700,15 +679,15 @@ pre { display: flex; align-items: center; padding: 0.5em 0; - border-bottom: 1px solid var(--border); + border-bottom: 1px solid var(--line); gap: 0.6em; } .secret-row code { flex: 1; font-size: 0.85em; } -.secret-row .secret-val { color: var(--text-muted); 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 */ -.empty-state { text-align: center; padding: 3.5em 1.5em; color: var(--text-muted); background: var(--bg-card); border: 1px dashed var(--border); border-radius: 10px; margin: 1em 0; } +.empty-state { text-align: center; padding: 3.5em 1.5em; color: var(--ink2); background: var(--surface); border: 1px dashed var(--line); border-radius: 10px; margin: 1em 0; } .empty-state p { margin-bottom: 1em; font-size: 0.95em; } /* Dev Mock Bar & Flash Banner */ @@ -752,18 +731,18 @@ pre { .console-section { margin-top: 1.2em; } .console-section #console-toggle { margin-bottom: 0.6em; } .console-container { - border: 1px solid var(--border); + border: 1px solid var(--line); border-radius: 8px; overflow: hidden; background: var(--term-bg); - box-shadow: var(--shadow-lg); + box-shadow: var(--shadow); } .console-toolbar { display: flex; align-items: center; gap: 0.6em; padding: 0.5em 0.8em; - border-bottom: 1px solid var(--border); + border-bottom: 1px solid var(--line); background: var(--term-bar); } .console-dots { display: flex; gap: 0.35rem; } @@ -776,34 +755,31 @@ pre { /* Gauges & Live Charts */ .stats-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.2em; } .stats-header h3 { margin: 0; } -.chart-range-btns { display: flex; gap: 0.3em; flex-wrap: wrap; } -.chart-range-btns .btn { font-size: 0.78em; padding: 0.25em 0.65em; } -.chart-range-btns .btn-active { background: var(--accent-fill); border-color: var(--accent-fill); color: var(--on-accent); } .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(--text-muted); font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; } +.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(--border); border-radius: 4px; overflow: hidden; } +.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(--orange); } -.stat-gauge-mem { background: var(--green); } -.stat-gauge-disk { background: var(--blue); } +.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(--text-muted); font-weight: 400; } -.stat-gauge-net { font-size: 0.85em; font-weight: 400; color: var(--text); line-height: 1.4; word-break: break-word; font-family: 'JetBrains Mono', monospace; } -.stats-divider { border: none; border-top: 1px solid var(--border); margin: 1.2em 0 1em 0; } +.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; } .chart-section { margin-top: 1em; } -.chart-title { display: flex; align-items: center; justify-content: space-between; font-size: 0.75em; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.04em; font-weight: 600; margin-bottom: 0.4em; } +.chart-title { display: flex; align-items: center; justify-content: space-between; font-size: 0.75em; color: var(--ink2); text-transform: uppercase; letter-spacing: 0.04em; font-weight: 600; margin-bottom: 0.4em; } .chart-title-val { font-size: 1.15em; font-weight: 700; text-transform: none; letter-spacing: normal; font-family: 'JetBrains Mono', monospace; } -.chart-title-val--cpu { color: var(--orange); } -.chart-title-val--mem { color: var(--green); } -.chart-title-val--net { color: var(--blue); } -.chart-title-val--disk { color: var(--blue); } -.chart-svg { width: 100%; height: 100px; display: block; background: var(--bg-elevated); border-radius: 6px; border: 1px solid var(--border); } +.chart-title-val--cpu { color: var(--ink2); } +.chart-title-val--mem { color: var(--ok); } +.chart-title-val--net { color: var(--accent); } +.chart-title-val--disk { color: var(--accent); } +.chart-svg { width: 100%; height: 100px; display: block; background: var(--surface); border-radius: 6px; border: 1px solid var(--line); } .live-badge { display: inline-flex; align-items: center; @@ -812,8 +788,8 @@ pre { padding: 0.15em 0.5em; font-size: 0.72em; font-weight: 600; - color: var(--green); - background: var(--green-bg); + color: var(--ok); + background: var(--ok-bg); border-radius: 9999px; border: 1px solid color-mix(in srgb, var(--ok) 20%, transparent); } @@ -861,7 +837,7 @@ pre { 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(--text-muted); margin: 0.2em 0 1.5em; font-size: 0.9em; } +.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); @@ -872,7 +848,7 @@ pre { font-weight: 500; margin-bottom: 1.2em; } -.login-card label { font-size: 0.85em; font-weight: 500; color: var(--text-muted); display: block; margin-bottom: 0.4em; } +.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; @@ -880,7 +856,7 @@ pre { border-radius: 6px; border: 1px solid var(--control); background: var(--bg); - color: var(--text); + color: var(--ink); width: 100%; outline: none; margin-bottom: 1.2em; @@ -889,3 +865,93 @@ pre { .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 .theme-btn { position: fixed; top: 12px; right: 12px; } + +/* Detail pages (DESIGN-SYSTEM §6) */ +.breadcrumb { font-size: 12.8px; color: var(--muted); margin-bottom: 8px; display: flex; gap: 6px; } +.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 .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 .icon { margin-top: 1px; } +.alert-title { font-size: 14px; font-weight: 600; } +.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; } +@media (min-width: 700px) { + .action-bar { display: flex; flex-wrap: wrap; } + .action-bar .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; } +.seg-btn:focus-visible { outline: 3px solid var(--accent); outline-offset: 1px; } +@media (max-width: 699px) { .seg-btn { min-height: 44px; } } +.seg-btn[aria-pressed=true] { background: var(--surface); color: var(--ink); box-shadow: var(--shadow); } +.stats-header { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; } +.stats-header h3 { margin: 0; display: flex; align-items: center; gap: 8px; } +.card h3 { font-size: 15px; font-weight: 700; text-transform: none; letter-spacing: 0; color: var(--ink); margin: 0 0 12px; } +.card h3 .count { background: var(--sunk); color: var(--ink2); border-radius: 9999px; padding: 0 8px; font-size: 12px; font-weight: 700; } +.gauges { display: grid; grid-template-columns: 1fr; gap: 12px; } +@media (min-width: 700px) { .gauges { grid-template-columns: repeat(3, 1fr); } } +.gauge-head { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; margin-bottom: 4px; } +.gauge-label { font-size: 11px; font-weight: 800; text-transform: uppercase; color: var(--muted); letter-spacing: 0.04em; } +.gauge-val { font-size: 13px; font-weight: 600; font-family: 'JetBrains Mono', monospace; } +.gauge-detail { font-size: 12px; color: var(--muted); font-weight: 400; } +div.bar { height: 6px; background: var(--chart-soft); border-radius: 3px; overflow: hidden; } +div.bar > span { display: block; height: 100%; width: var(--w, 0); background: var(--chart); border-radius: 3px; transition: width 0.5s ease; } +div.bar-warn > span { background: var(--warn); } +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 > 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 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); } +.code-block { position: relative; margin-bottom: 8px; } +.code-block pre.code { padding-right: 84px; } +.code-copy { position: absolute; top: 6px; right: 6px; gap: 4px; } +.code-copy .icon { width: 14px; height: 14px; } +.file-list { list-style: none; margin: 0; padding: 0; } +.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; } +} +.t-ok { color: var(--ok); } +.t-info { color: var(--accent); } +.t-warn { color: var(--warn-text); } +.t-danger { color: var(--danger-text); } +/* rows: left-aligned headers, numeric columns right at >= 700px */ +.rows th { text-align: left; } +.rows .wrap { white-space: normal; } +@media (min-width: 700px) { + .rows th.num .th-sort { justify-content: flex-end; } + .rows td.num { text-align: right; } + .rows td.c-act { text-align: right; width: 1%; white-space: nowrap; } +} +@media (max-width: 699px) { + .rows-kv tbody tr { grid-template-columns: minmax(0, 1fr); } + .rows-dep tbody tr { grid-template-columns: minmax(0, 1fr) auto; } + .rows-dep td.c-wide { grid-column: 1 / -1; } + .rows-dep td:nth-child(3) { grid-column: 1 / -1; } + .rows-apps tbody tr { grid-template-columns: minmax(0, 1fr) auto; } + .rows-apps td.c-name { grid-column: 1; } + .rows-apps td.c-act { grid-column: 1 / -1; } +} diff --git a/internal/http/static/app.js b/internal/http/static/app.js index 4ea64cb..be378e4 100644 --- a/internal/http/static/app.js +++ b/internal/http/static/app.js @@ -26,3 +26,35 @@ document.addEventListener('click', function (e) { if (d.classList && d.classList.contains('sheet') && d === sheetDown && outsideSheet(d, e)) d.close(); sheetDown = null; }); + +// Segmented controls: keep aria-pressed in sync (the header sits outside the htmx-swapped body). +document.addEventListener('click', function (e) { + var b = e.target.closest && e.target.closest('.seg-btn'); + if (!b) return; + [].forEach.call(b.parentNode.children, function (s) { s.setAttribute('aria-pressed', String(s === b)); }); +}); +// Copy button: data-copy="#selector" copies that element's text (execCommand fallback for plain http). +document.addEventListener('click', function (e) { + var b = e.target.closest && e.target.closest('[data-copy]'); + if (!b) return; + var src = document.querySelector(b.getAttribute('data-copy')); + if (!src) return; + var text = src.textContent.trim(); + function done(ok) { + var old = b.innerHTML; + b.textContent = ok ? 'Copied' : 'Copy failed'; + setTimeout(function () { b.innerHTML = old; }, 1500); + } + function fallback() { + var r = document.createRange(); + r.selectNodeContents(src); + var sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(r); + var ok = false; + try { ok = document.execCommand('copy'); } catch (err) {} + done(ok); + } + if (navigator.clipboard && window.isSecureContext) navigator.clipboard.writeText(text).then(function () { done(true); }, fallback); + else fallback(); +}); diff --git a/internal/http/templates.go b/internal/http/templates.go index 4345626..e487a4a 100644 --- a/internal/http/templates.go +++ b/internal/http/templates.go @@ -213,6 +213,7 @@ var funcMap = template.FuncMap{ "div": div, "mul": mul, "clamp100": clamp100, + "split": strings.Split, "list": func(v ...string) []string { return v }, "dict": func(kv ...interface{}) map[string]interface{} { m := make(map[string]interface{}, len(kv)/2) diff --git a/internal/http/templates/about.html b/internal/http/templates/about.html index 0f4872f..bd8a17f 100644 --- a/internal/http/templates/about.html +++ b/internal/http/templates/about.html @@ -21,7 +21,7 @@

What is Dockify?

Update

-

+

Current: v{{.Version}}
Latest: -

@@ -38,7 +38,7 @@

Sponsor & Donation

Sponsor on GitHub
-

Version {{.Version}}

+

Version {{.Version}}

{{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")}} {{end}} -
-
-

Details

-
- - - - - - {{if .App.Command}}{{end}} - {{if .App.Ports}}{{end}} - {{if .App.MemoryLimit}}{{end}} - {{if .App.CPULimit}}{{end}} - {{if .App.UlimitsNofile}}{{end}} - {{if or .App.LogMaxSize .App.LogMaxFile}}{{end}} - {{if .App.AuthUser}}{{end}} - {{if .App.GitRepo}} - {{end}} -
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}} (plain HTTP)
Command{{.App.Command}}
Ports{{.App.Ports | nl2br}}
Memory Limit{{.App.MemoryLimit}}
CPU Limit{{.App.CPULimit}}
Ulimits (nofile){{.App.UlimitsNofile}}
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}}
Authbasic auth ({{.App.AuthUser}})
Git Repo{{.App.GitRepo}}
Branch{{.App.GitBranch}}
-
-
-

Docker Compose

-
{{.App.Compose}}
-
+
+

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

Environment Variables

-

- Use ${KEY} in your compose to reference these values. -

- {{if .Secrets}} -
- - - - - {{range .Secrets}} - - - - - {{end}} -
KeyValue
{{.Key}} - {{if .IsSecret}}••••••••{{else}}{{.Value}}{{end}} -
- {{else}} -

No environment variables configured.

- {{end}} -

Variables can be added or modified in Edit mode.

-
+
+

Docker Compose

+
{{.App.Compose}}
+
-
-

Config Files

-

- Files written to the app directory before deploy. Mount them via ./filename:/container/path in compose. -

-
- {{if .Files}} - {{range .Files}} -
- {{.Path}} -
- {{end}} - {{else}} -

No config files.

+
+

Environment variables

+

Use ${KEY} in your compose to reference these values. Add or change them in Edit.

+ {{if .Secrets}} + + + + {{range .Secrets}} + {{end}} - -

Config files can be added or modified in Edit mode.

- - -
-
- {{if eq .App.Status "draft"}} -
- - - Edit - {{else}} - {{if eq .App.Status "running"}} -
- - - {{end}} - {{if eq .App.Status "stopped"}} -
- - - {{end}} - {{if eq .App.Status "running"}} - Edit - {{else}} - Edit - {{end}} - - - - {{end}} -
-
- -
+
+
KeyValue
{{.Key}}{{if .IsSecret}}••••••••{{else}}{{.Value}}{{end}}
+ {{else}} +

No environment variables configured.

+ {{end}}
-
- +
+

Config files

+

Written to the app directory before deploy. Mount them via ./filename:/container/path in compose. Add or change them in Edit.

+ {{if .Files}} +
    {{range .Files}}
  • {{.Path}}
  • {{end}}
+ {{else}} +

No config files.

+ {{end}}
{{if .Deployments}} -
-
-

Deployment History

- -
-
- - - +
+

Deployment history

+
TimeStatusCommitLog
+ {{range .Deployments}} - + - - + + {{end}} -
TimeStatusCommitLog
{{or .Status "unknown"}}{{if .CommitSHA}}{{.CommitSHA}}{{else}}-{{end}}{{.Log}}{{if .CommitSHA}}{{.CommitSHA}}{{else}}-{{end}}{{.Log}}
+
{{end}} {{if or (eq .App.Status "running") (eq .App.Status "stopped")}} -
+

Logs

-
-
- - - -
-
Click to load logs…
-
+ {{template "ui_segmented" (dict "Label" "Log lines" "Base" (printf "api/apps/%d/logs?tail=" .App.ID) "Target" "#log-output" "Swap" "innerHTML" "Active" -1 "Items" (list "50:Tail 50" "200:Tail 200" "500:Tail 500"))}} +
Click to load logs…
{{end}} + +
+

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}}
+
{{end}} \ No newline at end of file diff --git a/internal/http/templates/servers_add.html b/internal/http/templates/servers_add.html index 276e81e..6efea39 100644 --- a/internal/http/templates/servers_add.html +++ b/internal/http/templates/servers_add.html @@ -10,7 +10,7 @@

Add Server

{{if .Error}} -
{{.Error}}
+
{{.Error}}
{{end}}
@@ -28,7 +28,7 @@

Add Server

-

Registry Credential (optional)

+

Registry Credential (optional)

diff --git a/internal/http/templates/servers_detail.html b/internal/http/templates/servers_detail.html index b9df026..ae5a63b 100644 --- a/internal/http/templates/servers_detail.html +++ b/internal/http/templates/servers_detail.html @@ -10,96 +10,116 @@ {{.Server.Name}}
-
+

{{.Server.Name}} {{.Server.Status}}

-

{{.Server.Host}}:{{.Server.Port}} as {{.Server.User}}

-
+

{{.Server.Host}}:{{.Server.Port}} as {{.Server.User}}

+
+ + {{if eq .Server.Status "online"}}{{template "ui_alert" (dict "Kind" "ok" "Icon" "check" "Title" "Online" "Sub" (or (and (not .Server.ResourcesUpdatedAt.IsZero) (printf "Updated %s" (relativeTime .Server.ResourcesUpdatedAt))) ""))}} + {{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 + +
+ +
+ +
{{if eq .Server.Status "online"}} -
+
-

Resource Usage ● LIVE

-
- - - - - -
+

Resource usage Live

+ {{template "ui_segmented" (dict "Label" "Time range" "Base" (printf "servers/%d/stats?range=" .Server.ID) "Target" "#server-stats-body" "Swap" "outerHTML" "Active" 0 "Items" (list "realtime:Realtime" "1h:1h" "6h:6h" "24h:24h" "7d:7d"))}}
-
-
-
-
CPU
-
{{if .Server.CPUUsage}}{{printf "%.1f" .Server.CPUUsage}}{{else}}0.0{{end}}%
-
-
-
-
+
+
+
CPU{{if .Server.CPUUsage}}{{printf "%.1f" .Server.CPUUsage}}{{else}}0.0{{end}}%
+
-
-
-
RAM
-
{{if .Server.RAMUsage}}{{printf "%.1f" .Server.RAMUsage}}{{else}}0.0{{end}}% {{if and .Server.RAMMB .Server.RAMUsage}}{{printf "%.0f" (usedAmount .Server.RAMMB .Server.RAMUsage)}} MB{{else}}-{{end}}
-
-
-
-
+
+
RAM{{if .Server.RAMUsage}}{{printf "%.1f" .Server.RAMUsage}}{{else}}0.0{{end}}% {{if and .Server.RAMMB .Server.RAMUsage}}{{printf "%.0f" (usedAmount .Server.RAMMB .Server.RAMUsage)}} MB{{else}}-{{end}}
+
-
-
-
Disk
-
{{if .Server.DiskUsage}}{{printf "%.1f" .Server.DiskUsage}}{{else}}0.0{{end}}% {{if and .Server.DiskGB .Server.DiskUsage}}{{printf "%.0f" (usedAmount .Server.DiskGB .Server.DiskUsage)}} GB{{else}}-{{end}}
-
-
-
-
+
+
Disk{{if .Server.DiskUsage}}{{printf "%.1f" .Server.DiskUsage}}{{else}}0.0{{end}}% {{if and .Server.DiskGB .Server.DiskUsage}}{{printf "%.0f" (usedAmount .Server.DiskGB .Server.DiskUsage)}} GB{{else}}-{{end}}
+

-

Loading charts…

+

Loading charts…

{{end}} -
-
-

Connection

-
- - - - - - - -
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}}
Status{{.Server.Status}}
-
- {{template "servers_resources_card" .}} +
+

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" .}} + {{if .PublicKey}}
-

Worker Access

-

Dockify connects with this public key. To prepare a new worker, run this on it as {{.Server.User}} (it installs Docker and adds the key to ~/.ssh/authorized_keys), then click Init Worker:

-
curl -fsSL https://raw.githubusercontent.com/coderbuzz/dockify/main/scripts/setup-worker.sh | bash -s -- '{{.PublicKey}}'
- Public key only: {{.PublicKey}} +

Worker access

+

Dockify connects with this public key. To prepare a new worker, run this on it as {{.Server.User}} (it installs Docker and adds the key to ~/.ssh/authorized_keys), then run Init worker:

+
+ +
curl -fsSL https://raw.githubusercontent.com/coderbuzz/dockify/main/scripts/setup-worker.sh | bash -s -- '{{.PublicKey}}'
+
+

Public key only: {{.PublicKey}}

{{end}} -
-
- Edit - - - -
-
- -
+
+

Apps {{len .Apps}}

+ {{if .Apps}} + + + + {{range .Apps}} + + + + + + + + + {{end}} + +
NameStatusDomainPortGit
{{.Name}}{{if .Domain}}{{.Domain}}{{end}}{{.Status}}{{.Domain}}{{.Port}}{{if .GitRepo}}{{.GitRepo}}@{{.GitBranch}}{{else}}-{{end}}
+ {{else}} +

No apps on this server.

+ {{end}}
+
+

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.
+
+ +
+ {{template "ui_sheet_start" (dict "ID" "prune-modal" "Title" (printf "Prune worker disk (%s)" .Server.Name))}}

Bersihkan cache Docker, image tidak terpakai, dan data sisa di server worker ini untuk membebaskan ruang disk.

@@ -165,43 +185,6 @@

Worker Access

} -
- -
- -
-

Apps {{len .Apps}} app(s)

- {{if .Apps}} -
- - - - - {{range .Apps}} - - - - - - - - - {{end}} - -
NameDomainPortStatusGit
{{.Name}}{{.Domain}}{{.Port}}{{.Status}}{{if .GitRepo}}{{.GitRepo}}@{{.GitBranch}}{{else}}-{{end}} - -
- {{else}} -

No apps on this server.

- {{end}} -
- {{if eq .Server.Status "online"}}