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
12 changes: 1 addition & 11 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,7 @@ Set `DOCKIFY_DEV_MOCK=true` to use mock SSH client — no real VMs needed for UI

## UI Style Guide

**UI baru atau perubahan UI WAJIB mengikuti `docs/DESIGN-SYSTEM.md`** (token 3 palet × terang/gelap, partial komponen, hierarki aksi, checklist). Rencana revamp: `docs/UI-REVAMP-PLAN.md`. Bagian di bawah ini menjelaskan kondisi sebelum revamp dan akan diganti saat S8.

All styles live in `internal/http/templates/layout.html` (single `<style>` block). Full design tokens and component patterns in `SPEC.md`.

When editing templates or adding new UI:
- Always consult SPEC.md for the color system, component patterns, and CSS conventions
- No CSS frameworks — fully custom
- Class naming: lowercase with hyphens
- Use CSS variables (custom properties) for theming
- Dark mode via `<html class="dark">`, toggle persisted in `localStorage('dockify-theme')`
- Responsive: single `@media (max-width: 600px)` breakpoint
UI work **must** follow `docs/DESIGN-SYSTEM.md` (3 palettes × light/dark/system tokens, shared partials, action hierarchy, PR checklist). The revamp plan and measurements: `docs/UI-REVAMP-PLAN.md`, `docs/ui-revamp/`. Styles live in `internal/http/static/app.css`; no CSS framework, no hex in templates (except xterm themes), no inline `style=""` except `--w` values. Run `node tests/e2e/ui.mjs` (see Key Commands) before opening a UI PR.

## Git Workflow

Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Self-hosted Docker app deployment platform. Deploy Docker Compose stacks to your
- **SSH console** — interactive terminal via browser (WebSocket + xterm.js)
- **Dashboard** — live resource monitoring and deployment status at a glance
- **Encrypted backups** — export/import config with AES-GCM passphrase encryption
- **Web UI** — manage everything from the browser, dark/light mode
- **Web UI** — manage everything from the browser, three color palettes with light, dark or system theme

## Architecture

Expand Down Expand Up @@ -249,7 +249,7 @@ For an organization package, the token's owner must have read access to the pack

### Resource Monitoring

Dockify collects CPU, RAM, and disk usage from all online servers every 10 seconds in the background. Resource cards update via HTMX partial refresh (no page reload). A manual refresh button is available on each server detail page.
Dockify collects CPU, RAM, and disk usage from all online servers every 10 seconds in the background. Resource cards update via HTMX partial refresh (no page reload).

- **CPU** — core count via `nproc`, usage % via two-sample `/proc/stat` (1s interval)
- **RAM** — total and usage via `/proc/meminfo` (`MemTotal`/`MemAvailable`), with human-readable "used / free" info
Expand Down Expand Up @@ -514,12 +514,12 @@ The **About** page (`/about`) shows the current version, project description, an

Dockify's web UI is built with Go `html/template` + HTMX and fully custom CSS (no framework):

- **Dark/Light mode** — toggle via nav button, persisted in `localStorage` (`dockify-theme`)
- **Appearance** — three palettes (Indigo, Navy, Teal) and Light / Dark / System theme, chosen in Settings and persisted in `localStorage` (`dockify_palette`, `dockify_theme`); the theme follows your device until you pick one
- **Mobile first** — compact header with a bottom tab bar on phones and tabs under the header on desktop; lists turn into card rows below 700px
- **HTMX partial updates** — resource cards, log viewer, and status badges update without full page reloads
- **No CDN scripts** — htmx and xterm.js are embedded in the binary and served from `/static/`; a Content-Security-Policy limits scripts to Dockify itself (only Google Fonts is loaded from outside)
- **Responsive** — adapts to mobile screen sizes (single breakpoint at 600px)
- **No CDN scripts** — htmx and xterm.js are embedded in the binary and served from `/static/` and loaded only on the pages that need them; a Content-Security-Policy limits scripts to Dockify itself (only Google Fonts is loaded from outside)
- **Status badges** — color-coded for all server and app states
- **Confirm dialogs** — destructive actions (undeploy, delete, rollback) require confirmation
- **Confirm dialogs** — destructive actions (undeploy, remove, redeploy, rollback) sit in a Danger zone and require confirmation
- **Relative timestamps** — deployment and resource times rendered in browser timezone

## Dev Mode
Expand Down
70 changes: 11 additions & 59 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,10 @@ internal/
stats_ws.go # WebSocket real-time server stats (1s interval)
app_stats_ws.go # WebSocket real-time app container stats
templates.go # Embedded templates, template funcs, Render (into a buffer, then write)
static/ # Vendored htmx 2.0.4, xterm 5.3.0, xterm-addon-fit 0.8.0 (SOURCES.md: URLs + sha256)
static/ # app.css (tokens + components), app.js, list.js, charts.js, console.js; vendored htmx 2.0.4, xterm 5.3.0, xterm-addon-fit 0.8.0 (SOURCES.md: URLs + sha256)
templates/ # HTML templates (pages, layout, partials)
layout.html # head/nav/scripts blocks, all CSS (single <style> block)
layout.html # head/nav/scripts blocks (CSS lives in static/app.css)
ui_*.html # Shared partials: ui_icon, ui_sheet, ui_confirm, ui_alert, ui_segmented, ui_listbar
dashboard.html # Dashboard with stats cards + server/app summary tables
servers.html # Server list page (servers_list.html: table partial)
servers_add.html # Add server form
Expand All @@ -72,7 +73,6 @@ internal/
apps_add.html # Deploy app form, also used for edit (simple/advanced toggle)
apps_detail.html # App detail: info, compose, env vars, files, logs, deployments, live charts, console
apps_stats_card.html # HTMX partial: app stats
live_charts.html # Partial: live chart script (included by layout)
settings.html # Settings: webhook secret, export/import links
about.html # About page: version, update check/run, sponsor link
export.html # Export page: passphrase generator, download YAML
Expand Down Expand Up @@ -480,63 +480,15 @@ There are three monitoring paths:

## UI Style Guide

All styles live in `internal/http/templates/layout.html` as a single `<style>` block. No external CSS framework — fully custom Vanilla CSS.
The normative reference is [`docs/DESIGN-SYSTEM.md`](docs/DESIGN-SYSTEM.md); the revamp plan and measurements are in `docs/UI-REVAMP-PLAN.md` and `docs/ui-revamp/`. Summary:

### Typography
- **Primary UI Font Stack:** `'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif` (body, headers, navigation, buttons, labels)
- **Monospace Font Stack:** `'JetBrains Mono', 'Fira Code', ui-monospace, 'SF Mono', 'Cascadia Code', monospace` (IPs, ports, domains, badges, terminal, logs, secrets, code)
- **Base size:** 14px, line-height: 1.5
- **Headings:** weight 600; h1 = 1.35em, h2 = 1.1em

### Color System & Design Tokens

| Token | Light (`:root`) | Dark (`html.dark`) | Usage |
|---|---|---|---|
| `--bg` | `#f8fafc` | `#090d16` | App background |
| `--bg-elevated` | `#ffffff` | `#0f172a` | Raised surface |
| `--bg-card` | `#ffffff` | `#1e293b` | Card background |
| `--border` | `#e2e8f0` | `rgba(255, 255, 255, 0.08)` | Subtle borders |
| `--border-hover` | `#cbd5e1` | `rgba(255, 255, 255, 0.18)` | Interactive borders |
| `--text` | `#0f172a` | `#f8fafc` | Primary text |
| `--text-muted` | `#475569` | `#94a3b8` | Muted secondary text |
| `--text-dim` | `#94a3b8` | `#64748b` | Dimmed tertiary text |
| `--accent` | `#4f46e5` | `#6366f1` | Primary brand accent (Indigo) |
| `--accent-hover` | `#4338ca` | `#818cf8` | Hover accent |
| `--green` | `#10b981` | `#34d399` | Success / online / running |
| `--red` | `#ef4444` | `#f87171` | Error / offline / failed |
| `--yellow` | `#f59e0b` | `#fbbf24` | Warning / pending |
| `--orange` | `#f97316` | `#fb923c` | Info / deploying |
| `--blue` | `#3b82f6` | `#60a5fa` | Links / neutral interactive |
| `--shadow-sm` | `0 1px 2px 0 rgba(0, 0, 0, 0.05)` | `0 1px 2px 0 rgba(0, 0, 0, 0.3)` | Subtle card elevation |
| `--shadow-md` | `0 4px 6px -1px rgba(0,0,0,0.07)` | `0 4px 6px -1px rgba(0,0,0,0.4)` | Card hover / dropdown elevation |

### Dark/Light Mode
- Default: light mode (`<html>` without class)
- Dark: `<html class="dark">`
- Toggle button in nav uses `localStorage('dockify-theme')`, defaults to OS preference
- Icons: ☀ (light mode), ☾ (dark mode)

### Component Patterns

**Nav:** `<div class="top-nav">` — `.logo` (bold), `.nav-links` (flex). Sticky top bar with glassmorphism backdrop (`backdrop-filter: blur(12px)`). Includes a responsive mobile drawer navigation for viewports under 680px.

**Cards:** `<div class="card">` — `border: 1px solid var(--border)`, `border-radius: 8px`, `background: var(--bg-card)`, `box-shadow: var(--shadow-sm)`. Includes smooth hover micro-transitions (`transform: translateY(-1px)`).

**Buttons:** `<button class="btn">` or `<a class="btn">`
- `btn-primary` — filled indigo accent, white text
- `btn-ghost` — transparent, muted text with hover transition
- `btn-red` — red text, subtle red border hover
- `btn-danger` — filled red background for destructive actions

**Tables:** `<table>` — `<thead>` (uppercase th, letter-spacing), rounded container wrapping, row hover highlights.

**Badges:** `<span class="badge badge-{status}">` — Status pill badges with optional pulsing indicator dot (`<span class="badge-dot"></span>`) for live online/running/deploying states.

**Logs & Terminal:** Terminal and log container styled with dark glass topbar, macOS-style window controls, and crisp monospace log streaming.

### CSS Conventions
- CSS custom properties for all design tokens (no hardcoded color values)
- Fully responsive across desktop, tablet, and mobile viewports
- **Stack:** Go `html/template` + htmx + one custom stylesheet (`internal/http/static/app.css`), no CSS framework, no Node at build or runtime. Fonts: Plus Jakarta Sans (UI) and JetBrains Mono (technical values) from Google Fonts with `display=swap`.
- **Theme:** `<html data-palette="indigo|navy|teal" data-theme="light|dark">`, chosen in Settings → Appearance (palette) and the header button or Settings (light / dark / system, default system). Preferences are in `localStorage` (`dockify_palette`, `dockify_theme`); a head script applies them before first paint. Components only use layer-2 tokens (`--ink`, `--surface`, `--accent-fill`, ...), never hex.
- **Shell:** compact header (theme, sign out) plus a bottom tab bar below 900px and a tab row under the header from 900px; active item is set server-side (`aria-current`).
- **Components:** shared partials (`ui_icon`, `ui_sheet`, `ui_confirm`, `ui_alert`, `ui_segmented`, `ui_listbar`) and classes (`.btn-*`, `.card`, `.badge-*`, `.field`, `.rows` tables that become card rows on phones). Dialogs are native `<dialog>`; there is no `confirm()` or `alert()`.
- **Action hierarchy:** one primary action per screen; dangerous actions (redeploy, rollback, init, prune, undeploy, remove, clear history, roll secret, update) are destructive-outline triggers, grouped in a "Danger zone" on detail pages, and always confirmed in an alert dialog.
- **Loading:** `xterm`, charts, console and htmx are loaded only where used (xterm on first Console click).
- **Checks:** `node tests/e2e/ui.mjs` (screenshots, overflow, DOM nodes, navigation timing, WCAG contrast, quick a11y); see `AGENTS.md`.

## Security Considerations

Expand Down
61 changes: 18 additions & 43 deletions internal/http/static/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,6 @@ body:not(.login-page) { padding-bottom: calc(56px + env(safe-area-inset-bottom))
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
}
.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 */
Expand All @@ -340,7 +339,7 @@ form .grid { gap: 1.2em; margin-bottom: 0.9em; }
padding: 0.45em 0.9em;
font-size: 0.9em;
font-family: inherit;
border-radius: 6px;
border-radius: 12px;
border: 1px solid var(--line);
background: var(--surface);
color: var(--ink);
Expand Down Expand Up @@ -491,21 +490,13 @@ th[aria-sort=ascending] .icon, th[aria-sort=descending] .icon { opacity: 1; }
th[aria-sort=ascending] .icon { transform: rotate(180deg); }
.rows tbody td { padding: 10px 12px; vertical-align: middle; font-size: 14px; border-bottom: 1px solid var(--line); }
.rows tbody tr:last-child td { border-bottom: 0; }
.rows tbody tr:hover td { background: var(--sunk); }
.rows tbody tr:hover td { background-color: var(--sunk); }
.rows .c-name > a:first-child { font-size: 15px; font-weight: 700; display: block; }
.rows .c-name small { display: block; font-size: 12px; color: var(--muted); }
.rows .c-name small a { font-weight: 400; color: var(--muted); }
.rows .c-name small a.ext::after { content: " \2197"; }
.rows .c-num { font-size: 12px; white-space: nowrap; }
.rows .badge { white-space: nowrap; }
progress.bar { display: block; width: 100%; max-width: 96px; height: 4px; margin-top: 4px; border: 0; border-radius: 2px; background: var(--chart-soft); appearance: none; -webkit-appearance: none; }
progress.bar::-webkit-progress-bar { background: var(--chart-soft); border-radius: 2px; }
progress.bar::-webkit-progress-value { background: var(--chart); border-radius: 2px; }
progress.bar::-moz-progress-bar { background: var(--chart); border-radius: 2px; }
progress.bar-warn::-webkit-progress-value { background: var(--warn); }
progress.bar-warn::-moz-progress-bar { background: var(--warn); }
progress.bar-danger::-webkit-progress-value { background: var(--danger); }
progress.bar-danger::-moz-progress-bar { background: var(--danger); }
@media (max-width: 699px) {
table.rows, .rows tbody { display: block; border: 0; background: none; box-shadow: none; border-radius: 0; overflow: visible; }
.rows thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
Expand Down Expand Up @@ -667,26 +658,11 @@ pre {
box-shadow: var(--shadow);
}

/* Secret Row */
.secret-row {
display: flex;
align-items: center;
padding: 0.5em 0;
border-bottom: 1px solid var(--line);
gap: 0.6em;
}
.secret-row code { flex: 1; 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(--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 */
@keyframes blink-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
/* Dev Mock Bar */
.dev-mock-bar {
background: var(--warn-bg);
color: var(--warn-text);
Expand Down Expand Up @@ -721,7 +697,6 @@ pre {

/* Console Section */
.console-section { margin-top: 1.2em; }
.console-section #console-toggle { margin-bottom: 0.6em; }
.console-container {
border: 1px solid var(--line);
border-radius: 8px;
Expand All @@ -740,8 +715,6 @@ pre {
#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; }
.stat-gauge { min-width: 0; }
Expand All @@ -758,19 +731,6 @@ pre {
.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;
gap: 0.3em;
margin-left: 0.6em;
padding: 0.15em 0.5em;
font-size: 0.72em;
font-weight: 600;
color: var(--ok);
background: var(--ok-bg);
border-radius: 9999px;
border: 1px solid color-mix(in srgb, var(--ok) 20%, transparent);
}
.live-badge--hidden { opacity: 0; visibility: hidden; }

/* Responsive Media Queries */
Expand Down Expand Up @@ -894,3 +854,18 @@ pre.code { margin: 0; padding: 12px; background: var(--sunk); border: 1px solid
/* 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; }

/* Usage cells: the bar is the cell background (no extra DOM nodes), sub-text from data-sub */
.bar-cell {
--bar: var(--chart);
background-image: linear-gradient(var(--bar), var(--bar)), linear-gradient(var(--chart-soft), var(--chart-soft));
background-repeat: no-repeat;
background-position: left bottom;
background-size: var(--w, 0) 4px, 100% 4px;
}
.bar-cell.bar-warn { --bar: var(--warn); }
.bar-cell.bar-danger { --bar: var(--danger); }
.c-num[data-sub]::after { content: attr(data-sub); display: block; font-size: 11px; color: var(--muted); }
@media (max-width: 699px) { .rows .bar-cell { padding-bottom: 8px; } }
@media (min-width: 700px) { .rows-applist .c-name small { display: none; } }
.rows tbody tr { content-visibility: auto; contain-intrinsic-size: 0 64px; }
30 changes: 22 additions & 8 deletions internal/http/static/app.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
var assetV = (document.currentScript && document.currentScript.src.split("?v=")[1]) || "";
// Toast for client-side messages (role=status, or role=alert for errors).
function showToast(msg, isError) {
var t = document.createElement("div");
t.className = "toast";
t.setAttribute("role", isError ? "alert" : "status");
t.textContent = msg;
document.body.appendChild(t);
setTimeout(function () { t.remove(); }, 4000);
}
// Sheets (<dialog class="sheet">): openSheet/closeSheet, backdrop click closes, Escape is native.
function openSheet(id) {
var d = document.getElementById(id);
Expand Down Expand Up @@ -39,18 +49,22 @@ document.addEventListener('click', function (e) {
if (!b) return;
var src = document.querySelector(b.getAttribute('data-copy'));
if (!src) return;
var text = src.textContent.trim();
var isField = src.tagName === 'INPUT' || src.tagName === 'TEXTAREA';
var text = (isField ? src.value : 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);
if (isField) src.select();
else {
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);
Expand All @@ -67,7 +81,7 @@ function loadXterm(cb) {
xtermLoading = new Promise(function (resolve, reject) {
var l = document.createElement('link');
l.rel = 'stylesheet';
l.href = 'static/xterm.min.css';
l.href = 'static/xterm.min.css?v=' + assetV;
document.head.appendChild(l);
function add(src, next) {
var s = document.createElement('script');
Expand All @@ -76,7 +90,7 @@ function loadXterm(cb) {
s.onerror = reject;
document.head.appendChild(s);
}
add('static/xterm.min.js', function () { add('static/xterm-addon-fit.min.js', resolve); });
add('static/xterm.min.js?v=' + assetV, function () { add('static/xterm-addon-fit.min.js?v=' + assetV, resolve); });
});
}
xtermLoading.then(cb, function () { xtermLoading = null; });
Expand Down
Loading
Loading