Web UI UX Principles
Context
The web UIs are used by real operators managing a fleet of Proxmox hosts.
Operators scan dashboards quickly, act on alerts, and need to trust what
they see. Every color, icon, label, and layout choice either builds or
erodes that trust. A red dot on a healthy host wastes investigation time.
A confusing button label causes the wrong action.
Rule 1: Color is a language — use it consistently
Colors carry meaning. Users learn the mapping once and apply it everywhere.
Breaking the mapping forces users to re-evaluate every status indicator.
Green = good, running, healthy, reachable, success
Orange = attention needed, degraded, stale, unconfigured, limitation
Red = bad, error, failure, crashed, critical alert
Grey = neutral, unknown, no data, inactive, stopped (intentional)
Blue = informational, first-time setup, neutral action
Hard rules
- NEVER use red for non-error states. "No data," "stopped," "not configured,"
"no WoL support" are NOT errors. Use grey or orange.
- NEVER use green for states that haven't been confirmed. A host that hasn't
heartbeated yet is unknown (grey), not healthy (green).
- ALWAYS use the same color for the same state across all pages. If "offline"
is grey on the dashboard, it MUST be grey on the detail page.
- NEVER invent new color meanings. If you need a fifth state, discuss it
first. Every new color dilutes the existing vocabulary.
Why this matters
Previous bug: "No WoL" badges, stopped containers, disconnected mesh nodes,
and "no data" states all used red. Operators opened investigation tickets
for hosts that were functioning correctly. A systematic audit replaced all
of them with grey (inactive) or orange (limitation).
Rule 2: Icons must be instantly recognizable
An icon's job is to tell the user what something IS before they read the
label. If the icon requires thought, it's wrong.
Hard rules
- NEVER use abstract/metaphorical icons when a literal one exists.
router for router. hub for mesh. swap_horiz for bridge.
- NEVER reuse the same icon for different nav items. Users navigate by
icon shape — duplicates cause misclicks.
- NEVER use an icon that visually resembles something it's not. The
monitoring icon looks like a desktop computer — don't use it for
fleet/network views.
- ALWAYS test icons at 24px. If you can't identify it at that size
without the label, pick a different icon.
Previous bugs
monitoring icon was used for "View Fleet" button — users thought it
was a "Desktop" button. Changed to lan.
dns was used for both "Hosts" and "Containers" nav items. Changed
Containers to view_in_ar.
Rule 3: Labels speak human, not machine
Every piece of text visible to the user must be understandable by someone
who has never seen the source code.
Hard rules
- NEVER show raw exit codes, return codes, PIDs, or internal identifiers.
Map them to descriptions: exit code 2 → "host unreachable or task failed."
- NEVER show JSON, stack traces, or raw API responses in the UI.
- NEVER use technical jargon when a plain word exists. "Deploy" not
"converge." "Offline" not "unreachable=1."
- ALWAYS include units: "85% disk", "3.2 GB free", "4h 23m uptime" — not
"85", "3.2", "15732".
- Button labels describe the ACTION: "Start Deploy", "Add Host", "View Fleet."
Not "Submit", "Go", "OK."
Previous bugs
- "rc=2" in deploy badges — users guessed "release candidate 2."
Fixed with
exit_code_label().
- "100/100" for health — meaningless without context. Changed to
percentage with label.
Rule 4: Layout must survive the browser
Users resize windows, use tablets, split screens. The UI must work at
every reasonable width without manual adjustment.
Hard rules
- ALWAYS test at 1024px, 768px, and full-width. No horizontal scrolling
at 1024px.
- NEVER use fixed pixel widths for containers. Use
w-full, max-w-*,
flex-wrap, and responsive grid classes.
- NEVER let buttons overlap or escape their parent container. If buttons
don't fit, wrap them to the next line.
- ALWAYS use NiceGUI/Quasar's layout primitives (
ui.row, ui.column,
ui.card, ui.expansion) instead of custom CSS positioning.
- Cards in a grid MUST use
flex-wrap so they reflow at narrow widths
instead of overflowing.
Previous bugs
- "View Fleet" icon button overlapped "Build Images" and "View Fleet"
text buttons at narrow widths. Looked terrible at any browser size.
Rule 5: Reduce clicks to common actions
The most frequent workflows must be the shortest. Every extra click is
friction that accumulates over hundreds of uses.
Decision framework
Frequency | Max clicks to complete
Most-used action | 1 click (visible on dashboard)
Daily action | 2 clicks (one nav + one action)
Weekly action | 3 clicks (nav + page + action)
Setup/config | Any depth (done rarely)
Hard rules
- The dashboard MUST show fleet health at a glance — no clicks needed
to know if something is wrong.
- Host details MUST be one click from the fleet view (click card →
detail page).
- Adding a new host MUST be accessible from the fleet nodes page
directly, not buried in settings.
- NEVER put the primary workflow behind a menu or dropdown. If the main
use case is "check fleet health and add/update hosts," those actions
must be front and center.
Previous bugs
- "Full Deploy" was the primary dashboard action, but operators rarely
deploy to all hosts. The main workflow is per-host management. Fixed
by making host cards and fleet view the primary entry point.
Rule 6: States must be exhaustive and honest
Every possible state must have a visual representation. Missing states
create confusion ("why is this blank?").
The state inventory
For any entity (host, container, service), define ALL states:
healthy + heartbeating → green dot, metrics shown
reachable + no heartbeat → orange dot, "Online — no heartbeat"
unreachable → grey dot, "Offline"
error / crashed → red dot, error description shown
unknown / no data → grey dot, "—" for metrics
Hard rules
- NEVER leave a cell, badge, or indicator blank. Show "—" or "unknown"
for missing data. Blank space looks like a rendering bug.
- ALWAYS show degraded states honestly. "Reachable but no heartbeat" is
orange (degraded), not green (healthy) and not red (error).
- NEVER hide error details. If a host is unhealthy, show WHY — the
.errors list from the domain object.
- When telemetry is unavailable, show metrics as "—" or "0%" with
muted styling, not hidden. The user needs to know the data is absent,
not that the gauge is broken.
Previous bugs
- Fleet card showed "No nodes registered" when the API server wasn't
running. Hosts existed (in env), just weren't heartbeating. Fixed by
showing configured hosts with "waiting for heartbeats" message.
Rule 7: CSS must be modular and reusable
Custom one-off styles create visual drift between pages. A design system
means every page looks like it belongs to the same application.
Hard rules
- NEVER hardcode hex colors in page files. Use
theme.COLOR_SUCCESS,
theme.COLOR_WARNING, etc.
- NEVER write one-off CSS classes. If a pattern appears twice, extract
it to
theme.py as a global class.
- ALWAYS use the design system CSS classes:
.action-btn, .outline-btn,
.subtle-btn, .t-primary, .t-success, .t-error, .t-disabled,
.mono-val, .metric-label, .card-title, .section-label, .sep-accent.
- NEVER mix styling approaches. Use either Quasar utility classes OR
custom theme classes, not both inline on the same element.
- When adding a new visual pattern, add it to
theme.py with a semantic
name, not to the page file with inline styles.
Previous bugs
- Hardcoded
#4ade80 in mesh.py for batman status. Three other pages
used different greens for the same meaning. Fixed by using
theme.COLOR_SUCCESS.
- Signal quality colors hardcoded in bridge.py with a custom gradient.
Fixed by adding
theme.signal_color().
Rule 8: Information hierarchy — most important first
Users scan top-to-bottom, left-to-right. The most critical information
must be in the visual priority position.
Hard rules
- Dashboard: Fleet health score → online/offline counts → critical
alerts → action buttons. In that order.
- Host detail: Status + name + IP → resources (disk/memory) → guests →
deploy history → network → extensions. Most-actionable first.
- Alerts and errors ABOVE normal status. A critical alert buried below
a healthy-looking summary will be missed.
- NEVER show raw data dumps (log lines, full JSON) at the top level.
Summarize first, expand for details on demand.
Rule 9: Transitions and navigation must be predictable
Users build a mental model of the app's structure. Breaking that model
(unexpected navigation, changing page content without visual cue) causes
disorientation.
Hard rules
- Clicking a card or list item → detail view. ALWAYS.
- Back button → previous page. ALWAYS.
- Sidebar navigation → corresponding page. ALWAYS. No special behavior
on hover, no context-dependent destinations.
- NEVER change page content silently. If data refreshes, show a loading
indicator or timestamp update.
- Form submissions show feedback: success message, error message, or
loading state. NEVER leave the user wondering if their click worked.
Maintenance: the UX review loop
After any batch of UI changes:
- Human eye pass: Open each page, look at it as if you've never seen
it. Does anything look wrong? Are colors consistent? Do labels make
sense without context?
- Resize test: Drag the browser to 1024px, 768px, mobile. Do cards
wrap cleanly? Do buttons still fit? Are tables scrollable?
- State completeness: For every entity shown, verify all states
are represented. What does an empty fleet look like? What does a
single offline host look like?
- Workflow timing: How many clicks to do the most common task?
Can you reduce it?
- Update skills: If the review found issues, add them to the
"Previous bugs" section of this skill and
webui-design-system.
Anti-patterns
# BAD: "I'll fix the UX manually via SSH"
The fix must be in the automation. Manual deployment is not maintaining UX.
# BAD: "It works at my screen size"
Test at 3+ widths. Your screen is not the only screen.
# BAD: "Red stands out, so I'll use it for this badge"
Red means error. If it's not an error, don't use red. Stand out with
position, size, or a non-error color.
# BAD: "The user can figure out what rc=2 means"
They can't. They won't. Map it to human text.
# BAD: "I'll add the icon later"
An icon-less nav item or a wrong icon ships confusion to every user
until it's fixed. Get it right the first time.
1---2name: webui-ux-principles3description: UX design and maintenance principles for building human-intuitive web UIs. Use when designing new pages, reviewing UI changes, fixing layout issues, choosing icons/colors, or when the user asks about UX quality.4---56# Web UI UX Principles78## Context910The web UIs are used by real operators managing a fleet of Proxmox hosts.11Operators scan dashboards quickly, act on alerts, and need to trust what12they see. Every color, icon, label, and layout choice either builds or13erodes that trust. A red dot on a healthy host wastes investigation time.14A confusing button label causes the wrong action.1516## Rule 1: Color is a language — use it consistently1718Colors carry meaning. Users learn the mapping once and apply it everywhere.19Breaking the mapping forces users to re-evaluate every status indicator.2021```22Green = good, running, healthy, reachable, success23Orange = attention needed, degraded, stale, unconfigured, limitation24Red = bad, error, failure, crashed, critical alert25Grey = neutral, unknown, no data, inactive, stopped (intentional)26Blue = informational, first-time setup, neutral action27```2829### Hard rules30311. NEVER use red for non-error states. "No data," "stopped," "not configured,"32 "no WoL support" are NOT errors. Use grey or orange.332. NEVER use green for states that haven't been confirmed. A host that hasn't34 heartbeated yet is unknown (grey), not healthy (green).353. ALWAYS use the same color for the same state across all pages. If "offline"36 is grey on the dashboard, it MUST be grey on the detail page.374. NEVER invent new color meanings. If you need a fifth state, discuss it38 first. Every new color dilutes the existing vocabulary.3940### Why this matters4142Previous bug: "No WoL" badges, stopped containers, disconnected mesh nodes,43and "no data" states all used red. Operators opened investigation tickets44for hosts that were functioning correctly. A systematic audit replaced all45of them with grey (inactive) or orange (limitation).4647## Rule 2: Icons must be instantly recognizable4849An icon's job is to tell the user what something IS before they read the50label. If the icon requires thought, it's wrong.5152### Hard rules53541. NEVER use abstract/metaphorical icons when a literal one exists.55 `router` for router. `hub` for mesh. `swap_horiz` for bridge.562. NEVER reuse the same icon for different nav items. Users navigate by57 icon shape — duplicates cause misclicks.583. NEVER use an icon that visually resembles something it's not. The59 `monitoring` icon looks like a desktop computer — don't use it for60 fleet/network views.614. ALWAYS test icons at 24px. If you can't identify it at that size62 without the label, pick a different icon.6364### Previous bugs6566- `monitoring` icon was used for "View Fleet" button — users thought it67 was a "Desktop" button. Changed to `lan`.68- `dns` was used for both "Hosts" and "Containers" nav items. Changed69 Containers to `view_in_ar`.7071## Rule 3: Labels speak human, not machine7273Every piece of text visible to the user must be understandable by someone74who has never seen the source code.7576### Hard rules77781. NEVER show raw exit codes, return codes, PIDs, or internal identifiers.79 Map them to descriptions: exit code 2 → "host unreachable or task failed."802. NEVER show JSON, stack traces, or raw API responses in the UI.813. NEVER use technical jargon when a plain word exists. "Deploy" not82 "converge." "Offline" not "unreachable=1."834. ALWAYS include units: "85% disk", "3.2 GB free", "4h 23m uptime" — not84 "85", "3.2", "15732".855. Button labels describe the ACTION: "Start Deploy", "Add Host", "View Fleet."86 Not "Submit", "Go", "OK."8788### Previous bugs8990- "rc=2" in deploy badges — users guessed "release candidate 2."91 Fixed with `exit_code_label()`.92- "100/100" for health — meaningless without context. Changed to93 percentage with label.9495## Rule 4: Layout must survive the browser9697Users resize windows, use tablets, split screens. The UI must work at98every reasonable width without manual adjustment.99100### Hard rules1011021. ALWAYS test at 1024px, 768px, and full-width. No horizontal scrolling103 at 1024px.1042. NEVER use fixed pixel widths for containers. Use `w-full`, `max-w-*`,105 `flex-wrap`, and responsive grid classes.1063. NEVER let buttons overlap or escape their parent container. If buttons107 don't fit, wrap them to the next line.1084. ALWAYS use NiceGUI/Quasar's layout primitives (`ui.row`, `ui.column`,109 `ui.card`, `ui.expansion`) instead of custom CSS positioning.1105. Cards in a grid MUST use `flex-wrap` so they reflow at narrow widths111 instead of overflowing.112113### Previous bugs114115- "View Fleet" icon button overlapped "Build Images" and "View Fleet"116 text buttons at narrow widths. Looked terrible at any browser size.117118## Rule 5: Reduce clicks to common actions119120The most frequent workflows must be the shortest. Every extra click is121friction that accumulates over hundreds of uses.122123### Decision framework124125```126Frequency | Max clicks to complete127Most-used action | 1 click (visible on dashboard)128Daily action | 2 clicks (one nav + one action)129Weekly action | 3 clicks (nav + page + action)130Setup/config | Any depth (done rarely)131```132133### Hard rules1341351. The dashboard MUST show fleet health at a glance — no clicks needed136 to know if something is wrong.1372. Host details MUST be one click from the fleet view (click card →138 detail page).1393. Adding a new host MUST be accessible from the fleet nodes page140 directly, not buried in settings.1414. NEVER put the primary workflow behind a menu or dropdown. If the main142 use case is "check fleet health and add/update hosts," those actions143 must be front and center.144145### Previous bugs146147- "Full Deploy" was the primary dashboard action, but operators rarely148 deploy to all hosts. The main workflow is per-host management. Fixed149 by making host cards and fleet view the primary entry point.150151## Rule 6: States must be exhaustive and honest152153Every possible state must have a visual representation. Missing states154create confusion ("why is this blank?").155156### The state inventory157158For any entity (host, container, service), define ALL states:159160```161healthy + heartbeating → green dot, metrics shown162reachable + no heartbeat → orange dot, "Online — no heartbeat"163unreachable → grey dot, "Offline"164error / crashed → red dot, error description shown165unknown / no data → grey dot, "—" for metrics166```167168### Hard rules1691701. NEVER leave a cell, badge, or indicator blank. Show "—" or "unknown"171 for missing data. Blank space looks like a rendering bug.1722. ALWAYS show degraded states honestly. "Reachable but no heartbeat" is173 orange (degraded), not green (healthy) and not red (error).1743. NEVER hide error details. If a host is unhealthy, show WHY — the175 `.errors` list from the domain object.1764. When telemetry is unavailable, show metrics as "—" or "0%" with177 muted styling, not hidden. The user needs to know the data is absent,178 not that the gauge is broken.179180### Previous bugs181182- Fleet card showed "No nodes registered" when the API server wasn't183 running. Hosts existed (in env), just weren't heartbeating. Fixed by184 showing configured hosts with "waiting for heartbeats" message.185186## Rule 7: CSS must be modular and reusable187188Custom one-off styles create visual drift between pages. A design system189means every page looks like it belongs to the same application.190191### Hard rules1921931. NEVER hardcode hex colors in page files. Use `theme.COLOR_SUCCESS`,194 `theme.COLOR_WARNING`, etc.1952. NEVER write one-off CSS classes. If a pattern appears twice, extract196 it to `theme.py` as a global class.1973. ALWAYS use the design system CSS classes: `.action-btn`, `.outline-btn`,198 `.subtle-btn`, `.t-primary`, `.t-success`, `.t-error`, `.t-disabled`,199 `.mono-val`, `.metric-label`, `.card-title`, `.section-label`, `.sep-accent`.2004. NEVER mix styling approaches. Use either Quasar utility classes OR201 custom theme classes, not both inline on the same element.2025. When adding a new visual pattern, add it to `theme.py` with a semantic203 name, not to the page file with inline styles.204205### Previous bugs206207- Hardcoded `#4ade80` in mesh.py for batman status. Three other pages208 used different greens for the same meaning. Fixed by using209 `theme.COLOR_SUCCESS`.210- Signal quality colors hardcoded in bridge.py with a custom gradient.211 Fixed by adding `theme.signal_color()`.212213## Rule 8: Information hierarchy — most important first214215Users scan top-to-bottom, left-to-right. The most critical information216must be in the visual priority position.217218### Hard rules2192201. Dashboard: Fleet health score → online/offline counts → critical221 alerts → action buttons. In that order.2222. Host detail: Status + name + IP → resources (disk/memory) → guests →223 deploy history → network → extensions. Most-actionable first.2243. Alerts and errors ABOVE normal status. A critical alert buried below225 a healthy-looking summary will be missed.2264. NEVER show raw data dumps (log lines, full JSON) at the top level.227 Summarize first, expand for details on demand.228229## Rule 9: Transitions and navigation must be predictable230231Users build a mental model of the app's structure. Breaking that model232(unexpected navigation, changing page content without visual cue) causes233disorientation.234235### Hard rules2362371. Clicking a card or list item → detail view. ALWAYS.2382. Back button → previous page. ALWAYS.2393. Sidebar navigation → corresponding page. ALWAYS. No special behavior240 on hover, no context-dependent destinations.2414. NEVER change page content silently. If data refreshes, show a loading242 indicator or timestamp update.2435. Form submissions show feedback: success message, error message, or244 loading state. NEVER leave the user wondering if their click worked.245246## Maintenance: the UX review loop247248After any batch of UI changes:2492501. **Human eye pass**: Open each page, look at it as if you've never seen251 it. Does anything look wrong? Are colors consistent? Do labels make252 sense without context?2532. **Resize test**: Drag the browser to 1024px, 768px, mobile. Do cards254 wrap cleanly? Do buttons still fit? Are tables scrollable?2553. **State completeness**: For every entity shown, verify all states256 are represented. What does an empty fleet look like? What does a257 single offline host look like?2584. **Workflow timing**: How many clicks to do the most common task?259 Can you reduce it?2605. **Update skills**: If the review found issues, add them to the261 "Previous bugs" section of this skill and `webui-design-system`.262263## Anti-patterns264265```266# BAD: "I'll fix the UX manually via SSH"267The fix must be in the automation. Manual deployment is not maintaining UX.268269# BAD: "It works at my screen size"270Test at 3+ widths. Your screen is not the only screen.271272# BAD: "Red stands out, so I'll use it for this badge"273Red means error. If it's not an error, don't use red. Stand out with274position, size, or a non-error color.275276# BAD: "The user can figure out what rc=2 means"277They can't. They won't. Map it to human text.278279# BAD: "I'll add the icon later"280An icon-less nav item or a wrong icon ships confusion to every user281until it's fixed. Get it right the first time.282```