feat: introduce Nest workspace for local Firecracker management

- Added a new server module for the Nest workspace, integrating with UVM's API.
- Updated project description to reflect the new Nest functionality.
- Created tests for the Nest server and CLI, ensuring proper functionality and error handling.
- Refactored existing UVM server code to separate Nest and UVM functionalities.
- Removed the old UVM dashboard assets package as it is now part of the Nest workspace.
- Enhanced error messages for clarity regarding server shutdown commands.
This commit is contained in:
2026-09-25 13:45:57 +05:30
parent 660ba9efee
commit dca7f78dae
22 changed files with 456 additions and 138 deletions
+2 -2
View File
@@ -1,4 +1,4 @@
"""The sole public interface of this project: the local uvm CLI."""
"""Command-line interface for UVM's local machine management services."""
from __future__ import annotations
@@ -56,7 +56,7 @@ def build_parser(application: Application) -> argparse.ArgumentParser:
parser.add_argument(
"--serve",
action="store_true",
help="launch the FastAPI management server instead of running a CLI command",
help="launch the UVM management API; use nest --serve for the workspace dashboard",
)
parser.add_argument(
"--host",
-1
View File
@@ -1 +0,0 @@
# UVM Dashboard static assets package.
-944
View File
@@ -1,944 +0,0 @@
@font-face {
font-family: Manrope;
font-style: normal;
font-weight: 200 800;
font-display: swap;
src: url('../fonts/manrope-latin.woff2') format('woff2');
}
:root {
--canvas: #e8e7ed;
--canvas-deep: #dddae4;
--paper: #faf9fc;
--white: #fff;
--ink: #35333e;
--ink-soft: #55515f;
--muted: #78717f;
--faint: #a29cae;
--line: rgba(110, 99, 135, .11);
--acid: #ede39f;
--yellow: #e6d783;
--yellow-soft: #f4efce;
--yellow-ink: #746633;
--peach: #ddb5a9;
--peach-soft: #f3e5e0;
--sage: #a9b9a1;
--sage-soft: #e9eee6;
--lilac: #b9aed0;
--lilac-soft: #eae4f2;
--red: #bd766e;
--red-soft: #f3e4e1;
--shadow-card: 0 5px 14px rgba(66, 52, 92, .035), 0 18px 34px -23px rgba(66, 52, 92, .17);
--radius: 21px;
--sans: Manrope, 'Avenir Next', 'Segoe UI', sans-serif;
--mono: 'SFMono-Regular', Consolas, 'Liberation Mono', monospace;
--ease: cubic-bezier(.22, .68, 0, 1.01);
}
*, *::before, *::after { box-sizing: border-box; }
html { min-height: 100%; background: #dad8e0; }
body {
margin: 0;
color: var(--ink);
background: radial-gradient(ellipse at 20% 0, #eeedf1, transparent 65%), #dad8e0;
font-family: var(--sans);
font-size: 12px;
font-weight: 450;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
button, input, select, textarea { font: inherit; }
button, a, input, select { -webkit-tap-highlight-color: transparent; }
button { cursor: pointer; }
button, input, select { color: inherit; }
button { border: 0; }
button:disabled { cursor: wait; opacity: .55; }
a { color: inherit; text-decoration: none; }
h1, h2, h3, p { margin: 0; }
h1, h2, h3, strong { font-weight: 550; }
h1, h2, h3 { line-height: 1.3; }
h1:focus { outline: none; }
fieldset { min-width: 0; }
input { min-width: 0; }
input::placeholder { color: #918a9e; }
input[type='checkbox'] { width: 14px; height: 14px; accent-color: #9f9259; cursor: pointer; }
::selection { background: #ece1aa; color: var(--ink); }
:focus-visible { outline: 2px solid #a79967; outline-offset: 4px; }
[hidden] { display: none !important; }
.icon-sprite { position: absolute; width: 0; height: 0; overflow: hidden; }
.icon { display: block; width: 18px; height: 18px; flex: 0 0 auto; fill: none; stroke: currentColor; stroke-width: 1.45; stroke-linecap: round; stroke-linejoin: round; }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
.muted { color: var(--muted); }
.mono { font-family: var(--mono); font-size: 10px; }
.danger-text { color: #a45f57 !important; }
.subtle-label { display: block; color: var(--muted); font-size: 10px; font-weight: 450; letter-spacing: .015em; }
.dot { display: inline-block; width: 6px; height: 6px; flex: 0 0 auto; border-radius: 50%; background: var(--faint); }
.dot-yellow { background: var(--yellow); box-shadow: 0 0 0 3px rgba(230, 215, 131, .11); }
.dot-peach { background: var(--peach); }
.dot-sage { background: var(--sage); }
.dot-lilac { background: var(--lilac); }
.dot-error { background: var(--red); }
.status-inline { display: inline-flex; align-items: center; gap: 7px; }
.button, .icon-button, .quiet-link, .mini-link { display: inline-flex; align-items: center; justify-content: center; gap: 8px; }
.button {
min-height: 37px;
padding: 8px 17px;
border: 1px solid rgba(255, 255, 255, .8);
border-radius: 30px;
background: rgba(255, 255, 255, .57);
color: var(--ink-soft);
font-size: 11px;
font-weight: 550;
line-height: 1.4;
white-space: nowrap;
transition: background .2s, box-shadow .2s, transform .25s var(--ease);
}
.button:hover:not(:disabled) { transform: translateY(-1px); background: #fff; box-shadow: 0 4px 12px #5145660b; }
.button:active:not(:disabled) { transform: translateY(0); }
.button .icon { width: 15px; height: 15px; }
.button-primary { background: var(--acid); border-color: #f6efc5; color: #554d30; box-shadow: 0 5px 13px #9784460a; }
.button-primary:hover:not(:disabled) { background: #e9dc91; box-shadow: 0 6px 16px #97844616; }
.button-danger { background: var(--red-soft); color: #96544d; border-color: #e8cfc9; }
.button-danger:hover:not(:disabled) { background: #edd3cd; }
.button-quiet { background: transparent; border-color: transparent; color: var(--muted); box-shadow: none; }
.button-small { min-height: 31px; font-size: 10px; padding: 6px 12px; }
.button-wide { width: 100%; justify-content: space-between; padding-inline: 20px; }
.button-row { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
.icon-button { width: 33px; height: 33px; padding: 0; border: 1px solid transparent; border-radius: 50%; background: transparent; color: var(--muted); transition: background .2s, color .2s, transform .2s; }
.icon-button:hover:not(:disabled) { background: #ffffffa8; color: var(--ink); }
.icon-button .icon { width: 16px; height: 16px; }
.quiet-link { justify-content: flex-start; padding: 0; background: transparent; color: var(--muted); font-size: 10px; font-weight: 450; transition: color .2s; }
.quiet-link:hover { color: var(--ink); }
.quiet-link .icon { width: 15px; height: 15px; }
.mini-link { color: var(--muted); padding: 3px; border-radius: 50%; }
.mini-link:hover { color: var(--ink); background: #fff8; }
.mini-link .icon { width: 13px; height: 13px; }
.round-icon { display: inline-flex; align-items: center; justify-content: center; width: 39px; height: 39px; flex: 0 0 auto; background: #f7f5fa; border: 1px solid #fff9; border-radius: 14px; color: var(--muted); }
.tag { display: inline-flex; align-items: center; gap: 6px; font-size: 10px; line-height: 1.4; padding: 6px 10px; border-radius: 20px; background: #ffffff67; color: var(--muted); white-space: nowrap; }
.tag .icon { width: 13px; height: 13px; }
.badge { display: inline-flex; align-items: center; gap: 6px; padding: 4px 9px; background: #ffffff69; border-radius: 20px; font-size: 10px; line-height: 1.4; font-weight: 450; color: var(--ink-soft); white-space: nowrap; }
.badge::before { content: ''; width: 5px; height: 5px; flex: 0 0 auto; background: var(--faint); border-radius: 50%; }
.badge-running::before { background: var(--yellow); }
.badge-stopped::before { background: var(--peach); }
.badge-good::before { background: #a1b292; }
.badge-attention::before { background: var(--red); }
.badge-attention { color: #a26259; background: #f5e9e57a; }
.badge-pending::before { background: var(--lilac); }
.port-chip { display: inline-flex; align-items: center; gap: 5px; border-radius: 7px; padding: 4px 7px; background: #f4f1f7; color: #756d81; font-size: 9px; white-space: nowrap; }
.port-chip small { color: #918699; font-size: 8px; }
.port-list { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; min-height: 34px; color: var(--muted); font-size: 11px; }
.port-list .port-chip { padding: 7px 11px; font-size: 11px; background: #eee9f3; }
.port-list .port-chip small { font-size: 9px; }
/* A quiet, two-part navigation panel, lifted over the workspace. */
.app-shell {
display: grid;
grid-template-columns: 402px minmax(0, 1fr);
max-width: 1840px;
height: calc(100dvh - 48px);
min-height: 680px;
margin: 24px auto;
width: calc(100% - 48px);
overflow: hidden;
border: 1px solid #ffffffac;
border-radius: 25px;
background: linear-gradient(135deg, #ecebf1 0, #e6e4ec 100%);
box-shadow: 0 24px 70px -35px #493e6640, 0 2px 7px #61546d06;
}
.workspace-drawer {
display: grid;
grid-template-columns: minmax(0, 1fr) 172px;
gap: 16px;
padding: 25px 17px 24px 20px;
overflow-y: auto;
overscroll-behavior: contain;
background: linear-gradient(125deg, #f1f0f5dd, #f0eef4ba);
border-radius: 24px 23px 28px 24px;
border-right: 1px solid #ffffff4d;
box-shadow: 7px 0 27px -18px #71628422;
z-index: 2;
scrollbar-width: none;
}
.sidebar { display: flex; flex-direction: column; min-width: 0; }
.brand { display: inline-flex; align-items: center; gap: 8px; width: fit-content; color: #4d4857; line-height: 1; }
.brand > span:nth-child(2) { font-size: 27px; font-weight: 650; letter-spacing: -1.8px; }
.brand-period { color: #b3a166; }
.brand-tag { align-self: end; margin: 0 0 3px 2px; color: #898291; font-size: 8px; letter-spacing: .025em; }
.brand-flower { display: block; position: relative; width: 20px; height: 20px; flex: 0 0 auto; transform: rotate(-12deg); }
.brand-flower i { position: absolute; width: 8px; height: 8px; border-radius: 2px 6px 2px 6px; background: var(--yellow); }
.brand-flower i:nth-child(1) { left: 1px; top: 1px; }
.brand-flower i:nth-child(2) { right: 1px; top: 1px; transform: rotate(90deg); background: #d9d3df; }
.brand-flower i:nth-child(3) { left: 1px; bottom: 1px; transform: rotate(90deg); background: #b6adbf; }
.brand-flower i:nth-child(4) { right: 1px; bottom: 1px; }
.sidebar-heading { display: flex; gap: 10px; margin: 33px 0 22px; }
.sidebar-heading-mark { margin-top: 2px; color: #827b8e; font-size: 17px; }
.sidebar-heading h2 { font-size: 20px; font-weight: 450; letter-spacing: -.055em; line-height: 1.4; }
.sidebar-heading h2 span { display: block; }
.sidebar-search { display: flex; align-items: center; gap: 9px; padding: 0 4px; color: var(--muted); background: transparent; text-align: left; font-size: 10px; }
.sidebar-search .icon { width: 15px; height: 15px; }
.sidebar-search kbd { margin-left: auto; font-family: var(--sans); font-size: 10px; color: var(--faint); }
.primary-nav { display: grid; gap: 5px; margin-top: 35px; }
.nav-item { display: flex; align-items: center; gap: 10px; min-height: 38px; padding: 8px 10px; color: var(--muted); border-radius: 25px; font-size: 11px; font-weight: 450; transition: background .25s, box-shadow .25s, color .25s; }
.nav-item > .icon { width: 15px; height: 15px; }
.nav-item:hover { background: #ffffff5e; color: var(--ink); }
.nav-item.active { background: #ffffffed; box-shadow: 0 5px 14px #62537608; color: var(--ink); }
.nav-count { margin-left: auto; color: var(--muted); font-size: 9px; }
.nav-indicator { display: none; width: 4px; height: 4px; margin-left: auto; background: var(--lilac); border-radius: 50%; }
.nav-item.active .nav-indicator { display: block; }
.nav-health-dot { width: 5px; height: 5px; margin-left: auto; border-radius: 50%; }
.nav-item .nav-end-icon { width: 12px; height: 12px; margin-left: auto; color: var(--faint); }
.nav-create { margin-top: 9px; }
.sidebar-secondary { margin: 20px 7px 0; border-top: 1px solid var(--line); padding-top: 17px; }
.sidebar-secondary .nav-item { padding-inline: 3px; font-size: 10px; }
.sidebar-bottom { margin-top: auto; padding: 50px 5px 0; }
.workspace-identity { display: flex; align-items: center; gap: 9px; padding-top: 18px; border-top: 1px solid var(--line); }
.workspace-avatar { display: grid; place-items: center; width: 29px; height: 32px; flex: 0 0 auto; border: 1px solid #fff; border-radius: 10px; background: #e5dfed; color: #867b92; font-size: 18px; font-weight: 500; }
.workspace-identity > div { display: grid; gap: 4px; }
.workspace-identity strong { font-size: 9px; font-weight: 550; }
.workspace-identity > div > span { display: flex; align-items: center; gap: 5px; color: var(--muted); font-size: 8px; line-height: 1.2; }
.workspace-identity .dot { width: 4px; height: 4px; }
.sidebar-bottom > .quiet-link { font-size: 9px; margin-top: 22px; }
.sidebar-bottom > .quiet-link .icon { width: 13px; height: 13px; }
.context-rail { display: flex; flex-direction: column; gap: 17px; min-width: 0; padding-top: 53px; }
.insight-card { position: relative; display: block; min-height: 161px; padding: 15px 14px 11px; border: 1px solid #ffffffd9; border-radius: 19px; background: linear-gradient(140deg, #fff, #fbfafc); box-shadow: var(--shadow-card); transition: transform .3s var(--ease), box-shadow .3s; }
.insight-card::before { content: ''; position: absolute; top: -3px; left: 27px; right: 27px; height: 6px; border-radius: 20px; background: #e7cfdc; filter: blur(3px); z-index: -1; }
.insight-card .card-caption { gap: 4px; font-size: 9px; }
.insight-card .card-caption .icon { width: 12px; height: 12px; color: #9b8caa; }
.insight-card p { color: var(--muted); font-size: 10px; line-height: 1.55; min-height: 42px; margin-top: 9px; }
.insight-card p strong { color: #59505f; font-weight: 650; }
.insight-matrix { display: grid; place-items: center; height: 35px; margin-top: 5px; background-image: radial-gradient(#b9adb93b .75px, transparent .9px); background-size: 4px 4px; mask-image: radial-gradient(ellipse, #000 15%, transparent 70%); }
.insight-matrix span { display: block; width: 21px; height: 21px; background: repeating-conic-gradient(#92837b 0 25%, transparent 0 50%) 50% / 4px 4px; mask-image: radial-gradient(ellipse at center 75%, #000, transparent 70%); }
.insight-footer { display: flex; align-items: center; gap: 5px; margin-top: 6px; font-size: 7px; color: #99919f; }
.insight-footer .dot { width: 4px; height: 4px; }
.collection { display: block; position: relative; border-radius: 18px; transition: transform .3s var(--ease); }
.collection:hover, .insight-card:hover { transform: translateY(-3px); }
.collection-label { position: relative; display: flex; align-items: center; justify-content: space-between; gap: 10px; min-height: 65px; padding: 13px 14px; border: 1px solid #ffffffe6; border-radius: 18px; background: linear-gradient(130deg, #fffffff7, #fcfbfee6); box-shadow: var(--shadow-card); z-index: 1; }
.collection-label > div { display: grid; gap: 3px; }
.collection-label strong { font-size: 11px; font-weight: 550; }
.collection-label span { color: var(--muted); font-size: 9px; }
.collection-label > .icon { width: 13px; height: 13px; color: #8f849f; }
.collection-art { position: relative; overflow: hidden; height: 64px; margin: 0 3px -11px; }
.paper-machine { position: absolute; width: 100px; height: 91px; padding: 14px; border: 1px solid #fff; border-radius: 10px; background: linear-gradient(150deg, #fff, #eae8ed); box-shadow: -3px 1px 12px #6f5d8810; }
.back-paper { left: 11px; top: 18px; transform: rotate(-14deg); }
.front-paper { right: 6px; top: 5px; transform: rotate(9deg); }
.paper-machine i { display: block; height: 2px; margin-top: 5px; border-radius: 3px; background: #dcd8e1; }
.paper-machine i:nth-last-child(2) { width: 60%; }
.paper-machine > span { display: block; width: 11px; height: 3px; background: #e7dca5; border-radius: 3px; margin-top: 5px; }
.paper-machine .icon { width: 18px; height: 18px; color: #b5aaba; margin-bottom: 6px; }
.collection-machines::after { content: ''; position: absolute; bottom: -2px; left: 21px; right: 21px; height: 13px; background: #ebe5c9; border-radius: 50%; filter: blur(10px); }
.network-art { height: 62px; margin-bottom: -12px; }
.network-paper { position: absolute; left: 0; right: 0; top: 7px; height: 79px; padding: 12px; border: 1px solid #eeeeef; border-radius: 16px; background: #e7e4ebaa; box-shadow: inset 0 7px 9px #fff6; }
.network-paper::before { content: ''; position: absolute; left: 10px; right: 10px; top: 9px; height: 67px; background: #f3f0f7; border-radius: 11px; box-shadow: 0 0 0 1px #fff7; }
.network-paper .icon { position: absolute; width: 16px; height: 16px; left: 18px; top: 19px; color: #b8acbe; }
.network-paper i { position: absolute; top: 33px; width: 5px; height: 5px; border-radius: 50%; background: #c9bece; }
.network-paper i:nth-of-type(1) { left: 50px; }
.network-paper i:nth-of-type(2) { left: 88px; top: 19px; background: #e3d694; }
.network-paper i:nth-of-type(3) { right: 13px; }
.network-line { position: absolute; left: 52px; right: 16px; top: 20px; height: 17px; border-top: 1px solid #d9cedf; border-left: 1px solid #d9cedf; border-right: 1px solid #d9cedf; border-radius: 5px; z-index: 1; }
.collection-network::after { content: ''; position: absolute; left: 14px; right: 14px; bottom: -3px; height: 10px; border-radius: 20px; background: #e4cad6; filter: blur(5px); }
.collection-new .collection-label { background: #faf9fc9c; border-color: #ffffff96; box-shadow: 0 5px 15px #62537603; }
.rail-footnote { margin-top: auto; padding: 7px 14px 0; color: #a098ac; font-size: 8px; line-height: 1.65; }
/* The canvas stays light; grouping comes from tone and space. */
.workspace { display: flex; flex-direction: column; min-width: 0; min-height: 0; }
.topbar { display: flex; align-items: center; gap: 10px; min-height: 72px; padding: 0 27px; }
.workspace-crumb { display: inline-flex; align-items: center; gap: 8px; min-height: 29px; padding: 5px 13px; border-radius: 25px; background: #ffffffab; box-shadow: 0 3px 12px #67577d03; font-size: 10px; white-space: nowrap; }
.workspace-crumb .dot { width: 5px; height: 5px; }
.crumb-divider { color: #c0b9c9; padding: 0 1px; }
#breadcrumb { color: #8e8499; }
.refresh-button { width: 28px; height: 28px; }
.refresh-button .icon { width: 13px; height: 13px; }
.refresh-button.is-loading .icon { animation: spin 1.3s linear infinite; }
.global-search { position: relative; display: flex; align-items: center; gap: 8px; max-width: 260px; min-width: 135px; width: 100%; margin-left: auto; padding: 5px 2px; }
.global-search > .icon { width: 14px; height: 14px; color: #62596f; }
.global-search input { width: 100%; outline: none; border: 0; background: transparent; font-size: 10px; padding: 4px 0; }
.global-search:focus-within { border-bottom: 1px solid #c7bdd3; }
.global-search kbd { white-space: nowrap; color: #a499b1; font-family: var(--sans); font-size: 9px; }
.topbar-create { width: 29px; height: 29px; border: 1px solid #fff9; background: #ffffff60; margin-left: 6px; }
.mobile-menu { display: none; }
.page-content { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 12px 27px 25px; scrollbar-gutter: stable; }
.page { animation: page-appear .35s var(--ease) both; }
.page-heading { display: flex; align-items: center; justify-content: space-between; gap: 20px; margin-bottom: 23px; min-height: 52px; }
.page-heading h1 { font-size: 27px; font-weight: 450; letter-spacing: -.055em; margin-top: 5px; overflow-wrap: anywhere; }
.heading-period { color: #aa9aac; }
.page-heading .subtle-label { font-size: 9px; }
.date-label { font-size: 9px; color: #898090; align-self: end; padding-bottom: 4px; }
.overview-metrics { display: grid; grid-template-columns: .98fr 1.02fr 1fr; gap: 13px; }
.metric-card { min-width: 0; min-height: 153px; padding: 16px 18px; border: 1px solid #ffffff73; border-radius: var(--radius); background: linear-gradient(120deg, #f9f8fba8, #f6f4f987); box-shadow: 0 5px 15px #55456602; }
.card-caption { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.card-caption h2 { font-size: 12px; font-weight: 500; letter-spacing: -.025em; }
.caption-icon { width: 15px; height: 15px; color: #a59bb0; }
.fleet-number { display: flex; align-items: center; gap: 16px; margin: 12px 0 13px; }
.fleet-number > strong { font-size: 39px; font-weight: 350; letter-spacing: -.07em; line-height: 1.1; }
.fleet-number > span { color: var(--muted); font-size: 9px; line-height: 1.55; }
.metric-bottom { display: flex; justify-content: space-between; gap: 8px; font-size: 9px; }
.metric-bottom .dot { width: 5px; height: 5px; }
.signal-readouts { display: flex; justify-content: space-between; gap: 10px; margin-top: 16px; }
.signal-readouts > div { display: grid; align-items: center; grid-template-columns: 5px 1fr; gap: 2px 6px; }
.signal-readouts .dot { width: 4px; height: 4px; }
.signal-readouts strong { font-size: 16px; font-weight: 450; line-height: 1.4; }
.signal-readouts small { grid-column: 2; color: var(--muted); font-size: 9px; }
.segmented-track { display: flex; height: 3px; margin-top: 18px; border-radius: 5px; background: #e2dce9; overflow: hidden; }
.segmented-track span { width: 0; transition: width .7s var(--ease); }
.segmented-track span:first-child { background: linear-gradient(90deg, #b3c0a9, #d4dba6); }
.segmented-track span:last-child { background: linear-gradient(90deg, #e0c4ae, #dbaa9f); }
.range-labels { display: flex; justify-content: space-between; gap: 8px; margin-top: 8px; color: #94899f; font-size: 7px; }
.resource-line { display: flex; align-items: baseline; justify-content: space-between; gap: 6px; margin-top: 19px; }
.resource-line > span { font-size: 12px; letter-spacing: -.02em; }
.resource-line > small { color: var(--muted); font-size: 8px; text-align: right; }
.resource-track { height: 3px; border-radius: 4px; background: #e5dfca8a; margin-top: 6px; overflow: hidden; }
.resource-track > span { display: block; width: 0; height: 100%; border-radius: inherit; background: #e6d88e; box-shadow: 0 0 5px #e7d89555; transition: width .7s var(--ease); }
.second-resource { margin-top: 15px; }
.memory-track { background: #e6dfeb; }
.memory-track > span { background: #beb0d1; }
.section-heading { display: flex; align-items: center; justify-content: space-between; gap: 15px; margin-bottom: 18px; }
.section-heading h2 { font-size: 14px; font-weight: 500; letter-spacing: -.03em; }
.section-heading > div > span { display: block; color: var(--muted); font-size: 10px; margin-top: 4px; }
.portfolio-section { margin-top: 26px; }
.portfolio-section > .section-heading { margin: 0 3px 12px; }
.portfolio-content { min-height: 184px; }
.table-scroll { overflow-x: auto; scrollbar-width: thin; }
.portfolio-table { width: 100%; min-width: 565px; border-collapse: separate; border-spacing: 0 6px; text-align: left; }
.portfolio-table th { color: var(--muted); font-size: 10px; font-weight: 450; padding: 0 10px 5px; white-space: nowrap; }
.portfolio-table td { padding: 12px 10px; color: var(--ink-soft); font-size: 11px; transition: background .2s; }
.portfolio-table th:first-child, .portfolio-table td:first-child { padding-left: 14px; }
.portfolio-table tbody tr { background: #faf8fc22; }
.portfolio-table tbody tr:nth-child(2n) { background: #ffffff7d; }
.portfolio-table tbody tr:hover { background: #ffffffbf; }
.portfolio-table td:first-child { border-radius: 13px 0 0 13px; }
.portfolio-table td:last-child { border-radius: 0 13px 13px 0; width: 25px; padding-inline: 6px; }
.portfolio-table .badge { font-size: 9px; padding: 4px 7px; }
.portfolio-table .mini-link .icon { width: 12px; height: 12px; }
.portfolio-name { display: flex; align-items: center; gap: 9px; color: var(--ink); font-size: 11px; }
.portfolio-name > .icon { width: 13px; height: 13px; color: #b4a6bd; }
.portfolio-name a:hover { text-decoration: underline; text-underline-offset: 3px; }
.resource-inline { white-space: nowrap; font-size: 10px; }
.resource-inline span { color: #aca0b6; padding: 0 4px; }
.soft-panel { min-width: 0; padding: 21px; border: 1px solid #ffffff69; border-radius: var(--radius); background: linear-gradient(120deg, #f7f5fa8c, #f3f0f775); }
.overview-bottom { display: grid; grid-template-columns: 1.3fr 1fr; gap: 14px; margin-top: 22px; }
.activity-panel { padding: 19px 20px 13px; }
.activity-panel .section-heading { margin-bottom: 8px; }
.activity-body { display: flex; align-items: center; justify-content: center; gap: 34px; }
.fleet-ring { position: relative; width: 208px; max-width: 56%; flex: 0 0 auto; aspect-ratio: 1; }
.fleet-ring > svg { display: block; width: 100%; height: 100%; overflow: visible; }
.fleet-ring circle { fill: none; stroke-width: 3; }
.ring-track { stroke: #e2dce9; }
.ring-segment { transform: rotate(-90deg); transform-origin: center; stroke-dasharray: 0 100; stroke-linecap: round; transition: stroke-dasharray .8s var(--ease), stroke-dashoffset .8s var(--ease); }
.ring-running { stroke: #e7d98b; }
.ring-stopped { stroke: #dcb2a5; }
.ring-other { stroke: #baabcb; }
.ring-center { position: absolute; inset: 0; display: flex; flex-direction: column; justify-content: center; align-items: center; padding-top: 7px; }
.ring-center > span { color: var(--muted); font-size: 9px; }
.ring-center > strong { font-size: 43px; font-weight: 250; letter-spacing: -.06em; line-height: 1.4; }
.ring-center > strong small { font-size: 26px; font-weight: 300; }
.ring-center > span:last-child { font-size: 8px; }
.ring-legend { display: grid; gap: 21px; flex: 1; max-width: 150px; padding-right: 3px; }
.ring-legend > div { display: flex; align-items: flex-start; gap: 7px; }
.ring-legend .dot { width: 4px; height: 4px; margin-top: 5px; }
.ring-legend span { color: var(--ink-soft); font-size: 10px; line-height: 1.4; }
.ring-legend small { display: block; color: var(--muted); font-size: 8px; margin-top: 4px; }
.ring-legend strong { margin-left: auto; color: var(--ink-soft); font-size: 13px; font-weight: 400; }
.notes-panel { padding: 20px 20px 15px; }
.notes-panel .section-heading { margin-bottom: 12px; }
.note-row { display: flex; align-items: center; gap: 10px; padding: 13px 0; border-bottom: 1px solid #b5a6c016; }
.note-row > .icon { width: 15px; height: 15px; color: #a89ab3; }
.note-row > div { min-width: 0; }
.note-row strong { display: block; color: var(--ink-soft); font-size: 11px; font-weight: 450; }
.note-row small { display: block; color: var(--muted); font-size: 9px; margin-top: 2px; }
.note-row > span { margin-left: auto; font-size: 11px; color: var(--muted); white-space: nowrap; }
.note-row .icon:last-child { width: 11px; height: 11px; }
.note-row:hover strong { color: #4f405b; }
.notes-footer { margin-top: 15px; width: 100%; justify-content: space-between; font-size: 9px; }
.page-footer { display: flex; justify-content: space-between; gap: 12px; color: #a292ad; margin: 21px 3px 0; font-size: 8px; }
.page-footer > span:first-child { display: flex; align-items: center; gap: 6px; }
.page-footer .dot { width: 4px; height: 4px; }
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; min-height: 215px; padding: 30px 18px; text-align: center; border-radius: 18px; background: #f9f7fc47; }
.empty-state .empty-icon { position: relative; display: grid; place-items: center; width: 43px; height: 43px; margin-bottom: 4px; border: 1px solid #ffffffed; border-radius: 15px; background: linear-gradient(135deg, #fff, #e7e1ee); color: #b0a1bd; box-shadow: 0 8px 13px -8px #9183aa38; transform: rotate(-7deg); }
.empty-state .empty-icon::before { content: ''; position: absolute; inset: 4px -5px -3px 5px; border: 1px solid #ffffffbf; border-radius: 15px; z-index: -1; }
.empty-state h3 { font-size: 14px; font-weight: 450; letter-spacing: -.035em; }
.empty-state p { max-width: 290px; color: var(--muted); font-size: 11px; }
.empty-state .button { margin-top: 7px; }
.portfolio-content .empty-state { min-height: 191px; padding: 22px 15px; border: 1px solid #ffffff4d; }
.portfolio-content .empty-state h3 { font-size: 13px; }
.portfolio-content .empty-state p { font-size: 9px; max-width: none; }
.portfolio-content .empty-state .button { min-height: 29px; font-size: 9px; }
.loading-state { display: grid; place-items: center; min-height: 170px; color: var(--muted); font-size: 11px; }
/* The machine collection. */
.compact-metrics { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 13px; margin-bottom: 25px; }
.compact-metric { display: flex; align-items: center; gap: 15px; min-height: 100px; padding: 20px; border: 1px solid #ffffff80; border-radius: 19px; background: #f9f7fba6; }
.compact-metric > .round-icon { width: 37px; height: 41px; background: #eee8f3; color: #9d8daa; }
.compact-metric:nth-child(2) > .round-icon { background: #f3eed5; color: #ac9a65; }
.compact-metric:nth-child(3) > .round-icon { background: #efe2e0; color: #ba9692; }
.compact-metric > div { min-width: 0; }
.compact-metric strong { display: block; font-size: 25px; font-weight: 350; line-height: 1.25; letter-spacing: -.05em; overflow-wrap: anywhere; }
.compact-metric span { color: var(--muted); font-size: 10px; }
.compact-metric .metric-word { font-size: 18px; }
.fleet-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 15px; margin-bottom: 15px; }
.filter-tabs { display: flex; gap: 3px; padding: 4px; border: 1px solid #ffffff57; border-radius: 28px; background: #ffffff21; }
.filter-tab { padding: 8px 13px; border-radius: 22px; background: transparent; color: var(--muted); font-size: 11px; transition: background .2s, color .2s, box-shadow .2s; white-space: nowrap; }
.filter-tab.active { background: #fff; color: #5f496c; box-shadow: 0 3px 9px #68517508; }
.filter-tab:hover { color: var(--ink); }
.select-wrap { position: relative; display: inline-flex; align-items: center; }
.select-wrap::after { content: ''; position: absolute; right: 12px; width: 5px; height: 5px; border-right: 1px solid #92819f; border-bottom: 1px solid #92819f; transform: translateY(-2px) rotate(45deg); pointer-events: none; }
.select-wrap select { appearance: none; max-width: 100%; min-height: 32px; border: 1px solid #ffffff70; border-radius: 18px; background: #f7f4fa9c; padding: 6px 28px 6px 12px; color: #897494; font-size: 9px; cursor: pointer; }
.fleet-list-panel { padding: 5px 16px 0; border: 1px solid #ffffff73; border-radius: 21px; background: #f8f5fb57; }
.list-search-row { display: flex; align-items: center; justify-content: space-between; gap: 14px; padding: 9px 1px 13px; }
.search-field { display: flex; align-items: center; gap: 10px; max-width: 310px; width: 100%; color: #a18caf; }
.search-field .icon { width: 15px; height: 15px; }
.search-field input { width: 100%; padding: 7px 0; border: 0; outline: 0; background: transparent; font-size: 10px; }
.search-field:focus-within { border-bottom: 1px solid #c7b9d4; }
.list-search-row > span { font-size: 9px; color: #a18cab; white-space: nowrap; }
.machine-table { width: 100%; min-width: 690px; border-collapse: separate; border-spacing: 0 7px; text-align: left; }
.machine-table th { padding: 3px 9px 9px; color: var(--muted); font-size: 10px; font-weight: 450; }
.machine-table td { padding: 14px 9px; background: #ffffff91; color: var(--ink-soft); font-size: 11px; transition: background .2s; }
.machine-table tbody tr:nth-child(2n) td { background: #ffffff47; }
.machine-table tbody tr:hover td { background: #fff; }
.machine-table tr.selected td { background: #f6f0d9bb; }
.machine-table td:first-child { border-radius: 13px 0 0 13px; }
.machine-table td:last-child { border-radius: 0 13px 13px 0; }
.machine-table .check-cell { width: 28px; padding-right: 3px; padding-left: 10px; }
.machine-table input[type='checkbox'] { vertical-align: middle; }
.machine-name { display: flex; align-items: center; gap: 9px; }
.machine-name > .round-icon { width: 29px; height: 33px; border-radius: 10px; background: #eee7f280; }
.machine-name .round-icon .icon { width: 15px; height: 15px; color: #b39cc0; }
.machine-name > div { min-width: 80px; }
.machine-name a { display: block; max-width: 175px; color: var(--ink); font-size: 12px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.machine-name a:hover { text-decoration: underline; text-underline-offset: 4px; }
.machine-name small { display: block; color: var(--muted); font-size: 9px; margin-top: 3px; }
.allocation-cell { white-space: nowrap; }
.allocation-cell small { display: block; color: var(--muted); font-size: 10px; margin-top: 3px; }
.table-ports { display: flex; flex-wrap: wrap; gap: 3px; max-width: 135px; }
.table-ports .muted { font-size: 9px; color: #a78cb3; }
.row-controls { display: flex; align-items: center; justify-content: flex-end; gap: 1px; }
.row-controls .icon-button { width: 27px; height: 27px; color: #aa91b6; }
.row-controls .icon { width: 13px; height: 13px; }
.row-controls-heading { width: 67px; }
.list-footer { display: flex; justify-content: space-between; gap: 15px; padding: 17px 1px; color: #aa90b6; font-size: 8px; }
.bulk-controls { display: flex; align-items: center; gap: 9px; margin: 2px 0 13px; padding: 7px 10px; border: 1px solid #f6efc8; border-radius: 14px; background: #f3edc67d; }
.bulk-controls > span { margin-right: auto; font-size: 10px; color: #8a7945; }
.bulk-controls .icon-button { width: 25px; height: 25px; }
.operation-results { margin-top: 18px; padding: 17px 20px; border: 1px solid #fff8; border-radius: 17px; background: #f7f1eb; color: #957364; }
.operation-results header { display: flex; align-items: center; justify-content: space-between; gap: 15px; }
.operation-results h2 { font-size: 12px; }
.operation-results ul { margin: 7px 0 0; padding-left: 17px; font-size: 10px; }
.operation-results li { padding: 3px 0; overflow-wrap: anywhere; }
/* Network and inspection. */
.network-layout { display: grid; grid-template-columns: .85fr 1.4fr; gap: 16px; }
.network-map-panel { padding: 24px; }
.fabric-diagram { display: flex; flex-direction: column; align-items: center; padding: 23px 0; }
.fabric-node { display: flex; flex-direction: column; align-items: center; gap: 5px; position: relative; z-index: 1; }
.fabric-node > .round-icon { background: #fbf9fc; width: 45px; height: 45px; border-radius: 15px; margin-bottom: 4px; box-shadow: 0 8px 16px #78618c07; }
.fabric-node strong { font-size: 11px; font-weight: 500; }
.fabric-node small { color: #a08bac; font-size: 8px; }
.fabric-connector { width: 1px; height: 31px; margin: 8px 0; background: linear-gradient(#d7cbe1, #dccf94); }
.fabric-gateway .round-icon { background: #f4eecf; color: #b09d61; }
.fabric-branches { position: relative; display: flex; justify-content: space-between; width: 150px; height: 29px; margin-top: 21px; border-top: 1px solid #d8cce0; border-radius: 6px; }
.fabric-branches::before { content: ''; position: absolute; left: 50%; bottom: 100%; width: 1px; height: 21px; background: #d8cce0; }
.fabric-branches span { display: block; width: 1px; height: 20px; background: #d8cce0; }
.fabric-guests { display: flex; justify-content: space-between; width: 173px; margin-bottom: 16px; }
.fabric-guests .icon { width: 24px; height: 24px; color: #bba8c8; }
.fabric-diagram > .muted { font-size: 9px; color: #a38baf; }
.panel-footnote { color: var(--muted); font-size: 10px; line-height: 1.8; margin-top: 13px; overflow-wrap: anywhere; }
.policy-row { display: block; padding: 16px 2px; border-bottom: 1px solid #b9a6c51f; }
.policy-row:last-child { border-bottom: 0; }
.policy-row-heading { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 7px; }
.policy-row-heading strong { font-size: 12px; font-weight: 500; overflow-wrap: anywhere; }
.policy-row-heading .icon { width: 13px; height: 13px; color: #ad97b9; }
.policy-row-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 10px; color: var(--muted); font-size: 10px; }
.policy-row .table-ports { max-width: none; }
.policy-row:hover strong { color: #967249; }
.back-link { margin: 3px 0 21px; }
.detail-heading { margin-bottom: 22px; }
.detail-heading #detail-id { display: block; margin-top: 7px; color: #a58cb2; font-size: 8px; word-break: break-all; }
.detail-heading h1 { max-width: 450px; }
.detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; }
.detail-card { padding: 23px; }
.detail-list { margin: 0; }
.detail-list > div { display: flex; align-items: baseline; justify-content: space-between; gap: 15px; padding: 10px 0; border-bottom: 1px solid #bcabca21; }
.detail-list > div:last-child { border-bottom: 0; }
.detail-list dt { color: var(--muted); font-size: 11px; }
.detail-list dd { max-width: 66%; margin: 0; color: var(--ink-soft); text-align: right; font-size: 11px; overflow-wrap: anywhere; }
.detail-list .mono { font-size: 10px; }
.ssh-panel { display: flex; align-items: center; justify-content: space-between; gap: 20px; margin-top: 15px; padding: 18px 21px; }
.ssh-panel > div:first-child { display: flex; align-items: center; gap: 12px; }
.ssh-panel h2 { font-size: 12px; font-weight: 500; }
.ssh-panel p { font-size: 10px; color: var(--muted); margin-top: 4px; max-width: 300px; }
.ssh-command { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 5px 8px 5px 14px; border: 1px solid #fff9; border-radius: 12px; background: #fdfbfc9c; }
.ssh-command code { font-family: var(--mono); font-size: 10px; color: #918060; overflow-wrap: anywhere; }
.ssh-command .icon-button { width: 27px; height: 27px; }
.ports-panel, .log-panel, .resources-panel { margin-top: 15px; }
.storage-field { margin-top: 18px; }
.port-editor { padding-top: 5px; }
.log-output { max-height: 320px; min-height: 145px; margin: 0; padding: 17px; border: 1px solid #ffffff7a; border-radius: 13px; background: #e9e2ed8c; color: var(--ink-soft); font-family: var(--mono); font-size: 11px; line-height: 1.9; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; }
.detail-delete-row { display: flex; align-items: center; justify-content: space-between; gap: 15px; margin-top: 20px; padding: 0 4px; color: #ad94b6; font-size: 9px; }
/* Provisioning uses the same paper-like surfaces. */
.create-layout { display: grid; grid-template-columns: minmax(0, 1.7fr) minmax(220px, .9fr); align-items: start; gap: 17px; }
.create-fields { display: grid; gap: 15px; min-width: 0; }
.form-section { padding: 24px; }
.form-section .section-heading { margin-bottom: 22px; }
.form-section .section-heading h2 { display: flex; align-items: center; gap: 9px; }
.form-section .section-heading > div > span { padding-left: 31px; font-size: 9px; }
.step-number { display: inline-grid; place-items: center; width: 23px; height: 23px; border-radius: 50%; background: #e9e0ee; color: #ac91b7; font-size: 9px; }
.field { display: flex; flex-direction: column; gap: 7px; min-width: 0; }
.field > span:first-child { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; color: var(--ink-soft); font-size: 11px; }
.field small { color: var(--muted); font-size: 9px; font-weight: 400; line-height: 1.6; }
.field input { width: 100%; min-height: 39px; padding: 10px 12px; border: 1px solid #ffffffb3; border-radius: 11px; background: #fff9; color: var(--ink-soft); font-size: 12px; transition: background .2s, border-color .2s, box-shadow .2s; }
.field input:focus { outline: none; background: #ffffffed; border-color: #c3b3a0; box-shadow: 0 0 0 3px #e6dca329; }
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-top: 18px; }
.preset-fieldset { margin: 19px 0 0; padding: 0; border: 0; }
.preset-fieldset legend { color: var(--muted); font-size: 10px; padding: 0 0 9px; }
.size-presets { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 9px; }
.size-preset { display: flex; flex-direction: column; align-items: flex-start; gap: 5px; padding: 12px; border: 1px solid #fff9; border-radius: 13px; background: #ffffff3b; transition: background .2s, border-color .2s; }
.size-preset .icon { width: 17px; height: 17px; color: #b59dbf; margin-bottom: 4px; }
.size-preset strong { font-size: 11px; font-weight: 450; color: var(--ink-soft); }
.size-preset span { font-size: 9px; color: var(--muted); white-space: nowrap; }
.size-preset.active { border-color: #e6dca2; background: #f6f0d991; }
.size-preset.active .icon { color: #b7a367; }
.size-preset.active strong { color: #9b8854; }
.size-preset:hover { background: #fff9; }
.password-field { display: flex; align-items: center; position: relative; }
.password-field input { padding-right: 37px; }
.password-field > .icon-button { position: absolute; right: 5px; width: 27px; height: 27px; }
.toggle-field { position: relative; display: flex; align-items: center; justify-content: space-between; gap: 14px; margin-bottom: 18px; cursor: pointer; }
.toggle-field > span:first-child { display: grid; gap: 4px; }
.toggle-field strong { font-size: 11px; font-weight: 450; color: var(--ink-soft); }
.toggle-field small { font-size: 9px; color: var(--muted); }
.toggle-field > input { position: absolute; right: 0; opacity: 0; width: 33px; height: 20px; }
.toggle-track { position: relative; width: 31px; height: 18px; flex: 0 0 auto; border-radius: 15px; background: #d8cce1; transition: background .25s; }
.toggle-track::after { content: ''; position: absolute; width: 12px; height: 12px; top: 3px; left: 3px; border-radius: 50%; background: #fff; box-shadow: 0 1px 4px #79658721; transition: transform .25s var(--ease); }
.toggle-field > input:checked + .toggle-track { background: #d7c782; }
.toggle-field > input:checked + .toggle-track::after { transform: translateX(13px); }
.toggle-field > input:focus-visible + .toggle-track { outline: 2px solid #aa9560; outline-offset: 4px; }
.create-summary { position: sticky; top: 0; padding: 24px 22px; background: linear-gradient(140deg, #fefcffa6, #f5f0f76b); }
.summary-art { position: relative; height: 105px; margin-bottom: 20px; }
.summary-art > div, .summary-art > span { position: absolute; width: 90px; height: 72px; left: 50%; top: 14px; border: 1px solid #fff; border-radius: 17px; background: #eeebf1; box-shadow: 0 10px 21px -10px #9b87ad40; }
.summary-art > div:nth-child(1) { transform: translateX(-38%) rotate(15deg); background: #e8e2ed; }
.summary-art > div:nth-child(2) { transform: translateX(-62%) rotate(-12deg); background: #f4f0f8; }
.summary-art > span { display: grid; place-items: center; transform: translateX(-50%) rotate(-3deg); background: linear-gradient(130deg, #fffeff, #f1eadb); }
.summary-art .icon { width: 31px; height: 31px; color: #b8a990; }
.create-summary > .subtle-label { font-size: 8px; color: #ab91b3; }
.create-summary > h2 { margin-top: 8px; color: var(--ink-soft); font-size: 20px; font-weight: 400; letter-spacing: -.055em; overflow-wrap: anywhere; }
.create-summary > p:not(.form-error) { margin-top: 10px; color: var(--muted); font-size: 10px; }
.create-summary .detail-list { margin: 23px 0; }
.create-summary .detail-list dt, .create-summary .detail-list dd { font-size: 10px; }
.create-summary .button { font-size: 10px; }
.summary-cancel { display: flex; justify-content: center; margin-top: 13px; font-size: 9px; color: #b29ab9; }
.summary-note { display: flex; justify-content: center; align-items: center; gap: 6px; color: #b9a3bd; font-size: 8px; margin-top: 26px; }
.summary-note .icon { width: 12px; height: 12px; }
.form-error { color: #b1726b; font-size: 10px; line-height: 1.7; margin: 9px 0; overflow-wrap: anywhere; }
.form-error:empty { display: none; }
/* Host readiness and installation. */
.health-layout { display: grid; grid-template-columns: 1.35fr 1fr; gap: 16px; align-items: start; }
.health-side { display: grid; gap: 16px; }
.diagnostics-panel { padding: 22px; }
.diagnostics-list { display: grid; gap: 8px; }
.diagnostic-item { display: flex; align-items: flex-start; gap: 11px; padding: 13px 12px; border: 1px solid #ffffff61; border-radius: 13px; background: #ffffff47; }
.diagnostic-item > .diagnostic-icon { display: grid; place-items: center; width: 25px; height: 25px; flex: 0 0 auto; border-radius: 9px; background: #e8ebe1; color: #98a383; }
.diagnostic-icon .icon { width: 13px; height: 13px; }
.diagnostic-item.attention > .diagnostic-icon { background: #efe0d8; color: #c19e8a; }
.diagnostic-item > div { min-width: 0; flex: 1; }
.diagnostic-item h3 { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; color: var(--ink-soft); font-size: 11px; font-weight: 500; }
.diagnostic-item h3 > span { font-size: 9px; font-weight: 450; white-space: nowrap; color: #7a8562; }
.diagnostic-item.attention h3 > span { color: #a2856a; }
.diagnostic-item p { margin-top: 5px; color: var(--muted); font-size: 10px; line-height: 1.7; overflow-wrap: anywhere; }
.install-panel { padding: 24px; }
.install-panel > .round-icon { width: 44px; height: 44px; background: #f2eacb; color: #b09c67; margin-bottom: 18px; }
.install-panel h2 { font-size: 16px; font-weight: 450; letter-spacing: -.04em; }
.install-panel > p { margin-top: 9px; color: var(--muted); font-size: 11px; line-height: 1.8; }
.install-assets { display: grid; gap: 12px; margin: 24px 0; padding: 17px 0; border-block: 1px solid #beabcc24; }
.install-assets > span { display: flex; align-items: center; gap: 9px; font-size: 11px; color: var(--muted); }
.install-assets .icon { width: 14px; height: 14px; color: #bca5c3; }
.install-panel .toggle-field { margin-top: 6px; }
.install-panel .toggle-field strong { font-size: 9px; }
.install-panel .button { font-size: 10px; }
.install-result { max-height: 280px; margin: 17px 0 0; padding: 13px; border-radius: 12px; background: #e9e1ee; color: #95749d; font-family: var(--mono); font-size: 9px; line-height: 1.8; white-space: pre-wrap; overflow-wrap: anywhere; overflow-y: auto; }
.server-info { padding: 23px; }
.server-info .detail-list { margin: 0 0 18px; }
.server-info .detail-list > div { flex-direction: column; gap: 3px; }
.server-info .detail-list dd { max-width: 100%; text-align: left; }
.server-info .quiet-link { font-size: 9px; }
/* Authentication is part of the same workspace, not a different product. */
.auth-screen { display: grid; place-items: center; min-height: 100dvh; padding: 32px; }
.auth-window { display: grid; grid-template-columns: 1.08fr 1fr; width: min(100%, 990px); min-height: 620px; overflow: hidden; border: 1px solid #ffffffe0; border-radius: 29px; background: #f3f0f6; box-shadow: 0 25px 75px -36px #7158834d; }
.auth-story { display: flex; flex-direction: column; position: relative; padding: 35px 40px 26px; overflow: hidden; background: linear-gradient(130deg, #edeaf1, #e4deeb); border-right: 1px solid #ffffffa1; border-radius: 28px; }
.auth-story-copy { position: relative; margin-top: 55px; z-index: 1; }
.auth-story-copy > .subtle-label { font-size: 9px; color: #a68dad; }
.auth-story-copy h1 { font-size: 43px; letter-spacing: -.065em; line-height: 1.2; font-weight: 400; color: #64556e; margin-top: 14px; }
.auth-story-copy p { color: var(--muted); font-size: 12px; line-height: 1.9; margin-top: 18px; }
.auth-sculpture { position: relative; flex: 1; min-height: 255px; }
.sculpture-card { position: absolute; width: 155px; height: 125px; left: 50%; top: 74px; border-radius: 22px; border: 1px solid #ffffffe0; background: linear-gradient(130deg, #fefcff, #eee6f1); box-shadow: 0 18px 25px -15px #7b608d59; }
.sculpture-back { transform: translateX(-63%) rotate(-22deg) translateY(-25px); background: #e4dbea; }
.sculpture-middle { transform: translateX(-37%) rotate(15deg) translateY(-15px); }
.sculpture-front { display: flex; flex-direction: column; align-items: flex-start; justify-content: center; gap: 12px; padding: 20px 24px; transform: translateX(-50%) rotate(-4deg); background: linear-gradient(130deg, #fff, #f2ecd9); }
.sculpture-front > .icon { width: 30px; height: 30px; color: #b5a68c; }
.sculpture-front > span { color: #a58c8a; font-size: 10px; }
.sculpture-lights { display: flex; gap: 5px; }
.sculpture-lights i { display: block; width: 4px; height: 4px; border-radius: 50%; background: #e0cf85; }
.sculpture-lights i:nth-child(2) { background: #d5cbdc; }
.sculpture-orbit { position: absolute; width: 260px; height: 260px; left: 50%; top: 3px; border: 1px solid #c9b9d438; border-radius: 50%; transform: translateX(-50%); }
.orbit-inner { width: 206px; height: 206px; top: 30px; border-color: #fff7; }
.orbit-outer::after { content: ''; position: absolute; top: 31px; left: 36px; width: 7px; height: 7px; border-radius: 50%; background: #e1d094; box-shadow: 0 0 0 5px #e6d9b326; }
.sculpture-caption { position: absolute; left: 0; right: 0; top: 240px; display: flex; align-items: center; justify-content: center; gap: 6px; color: #af93b5; font-size: 8px; }
.sculpture-caption .dot { width: 4px; height: 4px; }
.auth-story-footer { position: relative; margin-top: 37px; color: #ae96b5; font-size: 8px; }
.auth-panel { display: flex; flex-direction: column; align-items: center; justify-content: center; position: relative; padding: 48px 45px; background: linear-gradient(150deg, #faf8fc, #f1ebf5); }
.auth-panel-inner { width: 100%; max-width: 310px; }
.auth-lock { width: 43px; height: 43px; background: #eee7f2; color: #a18aaa; margin-bottom: 28px; }
.auth-panel h2 { margin: 9px 0 13px; font-size: 27px; font-weight: 400; letter-spacing: -.055em; color: #735b7d; }
.auth-panel-inner > p { color: var(--muted); font-size: 12px; line-height: 1.85; }
.auth-panel form { margin-top: 29px; }
.auth-panel .field input { background: #fdfbfe; border-color: #e7dcec; min-height: 43px; }
.auth-panel .button { margin-top: 16px; min-height: 41px; }
.auth-panel-inner > .auth-note { display: flex; align-items: center; gap: 6px; font-size: 8px; color: #b39bbd; margin-top: 19px; }
.auth-note .icon { width: 12px; height: 12px; }
.auth-help { position: absolute; bottom: 30px; color: #b39bbd; font-size: 8px; }
/* Small interactions, accessible dialogs, and a useful search. */
.search-results { position: absolute; top: calc(100% + 10px); right: -8px; width: 330px; max-width: calc(100vw - 40px); max-height: 380px; padding: 7px; overflow-y: auto; z-index: 12; border: 1px solid #fff; border-radius: 17px; background: #faf7fceF; box-shadow: 0 12px 35px #6f538526; backdrop-filter: blur(20px); }
.search-result { display: flex; align-items: center; gap: 9px; padding: 10px; border-radius: 11px; color: #8b6f98; font-size: 10px; }
.search-result[aria-selected='true'], .search-result:hover { background: #eee5f4; }
.search-result > .icon { width: 15px; height: 15px; color: #b398c2; }
.search-result > div { min-width: 0; flex: 1; }
.search-result strong { display: block; font-size: 10px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.search-result small { display: block; margin-top: 2px; font-size: 8px; color: #b296c0; }
.search-empty { padding: 20px; color: #ac8cb9; font-size: 10px; }
.action-menu { position: fixed; width: 181px; padding: 6px; z-index: 30; border: 1px solid #fff; border-radius: 15px; background: #fcf9feee; backdrop-filter: blur(18px); box-shadow: 0 12px 30px #71538025; }
.action-menu button { display: flex; align-items: center; gap: 9px; padding: 10px; width: 100%; border-radius: 10px; background: transparent; color: #8b6b9a; font-size: 10px; text-align: left; }
.action-menu button:hover, .action-menu button:focus { background: #ede3f4; outline: none; }
.action-menu .icon { width: 14px; height: 14px; color: #b599c3; }
.action-menu .menu-danger { color: #b77970; }
.action-menu .menu-danger .icon { color: #c7978f; }
.notice { display: flex; align-items: center; gap: 9px; padding: 12px 15px; margin-bottom: 20px; border: 1px solid #fff9; border-radius: 13px; background: #f3ebe0; color: #a38a67; font-size: 10px; overflow-wrap: anywhere; }
.notice > .icon { width: 15px; height: 15px; }
.notice > .quiet-link { margin-left: auto; flex-shrink: 0; color: inherit; }
.notice-error { background: #f3e6e3; color: #ad7973; }
.confirm-dialog { width: min(420px, calc(100% - 34px)); max-height: calc(100dvh - 50px); padding: 29px; border: 1px solid #fff; border-radius: 24px; background: #f8f5fb; color: var(--ink); box-shadow: 0 30px 90px #69537738; overflow-y: auto; }
.confirm-dialog[open] { animation: dialog-appear .25s var(--ease); }
.confirm-dialog::backdrop { background: #6655722e; backdrop-filter: blur(6px); }
.confirm-dialog .round-icon { background: #ece4f1; margin-bottom: 19px; }
.confirm-dialog .subtle-label { font-size: 9px; }
.confirm-dialog h2 { color: var(--ink); font-size: 21px; font-weight: 450; letter-spacing: -.045em; margin-top: 8px; overflow-wrap: anywhere; }
.confirm-dialog p { color: var(--muted); font-size: 12px; line-height: 1.8; margin-top: 13px; }
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 26px; }
.toast-container { position: fixed; bottom: 27px; right: 30px; display: grid; gap: 9px; max-width: min(370px, calc(100vw - 34px)); z-index: 60; }
.toast { display: flex; align-items: flex-start; gap: 10px; padding: 14px 17px; border: 1px solid #fff; border-radius: 15px; background: #fbf8fef5; color: #9271a1; box-shadow: 0 9px 35px #77568521; font-size: 11px; animation: toast-appear .35s var(--ease); overflow-wrap: anywhere; }
.toast > .icon { width: 16px; height: 16px; color: #b2b98a; margin-top: 1px; }
.toast.toast-error { color: #b7786e; }
.toast.toast-error > .icon { color: #c68d81; }
.noscript-message { padding: 20px; text-align: center; color: #8c6d98; }
.nav-scrim { display: none; }
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes page-appear { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
@keyframes dialog-appear { from { opacity: 0; transform: translateY(8px) scale(.98); } to { opacity: 1; transform: translateY(0) scale(1); } }
@keyframes toast-appear { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
* { scrollbar-width: thin; scrollbar-color: #c9bdd354 transparent; }
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-thumb { border-radius: 8px; background: #c9bdd36b; }
::-webkit-scrollbar-thumb:hover { background: #b5a4c18f; }
@media (min-width: 1650px) {
.app-shell { grid-template-columns: 423px minmax(0, 1fr); }
.workspace-drawer { grid-template-columns: minmax(0, 1fr) 174px; gap: 19px; padding-inline: 24px 21px; }
.nav-item { font-size: 11px; min-height: 40px; }
.sidebar-heading h2 { font-size: 23px; }
.context-rail { gap: 20px; }
.insight-card { min-height: 177px; padding: 17px; }
.insight-card .card-caption { font-size: 10px; }
.insight-card p { font-size: 10px; }
.collection-label { min-height: 72px; }
.collection-label strong { font-size: 11px; }
.collection-label span { font-size: 9px; }
.collection-art { height: 79px; }
.topbar { min-height: 79px; }
.page-content { padding: 15px 33px 30px; }
.metric-card { min-height: 169px; padding: 20px 23px; }
.card-caption h2 { font-size: 12px; }
.fleet-number { margin-block: 15px; }
.fleet-number > strong { font-size: 45px; }
.fleet-number > span, .metric-bottom, .signal-readouts small { font-size: 9px; }
.range-labels { font-size: 8px; }
.signal-readouts { margin-top: 20px; }
.resource-line > span { font-size: 12px; }
.resource-line > small { font-size: 8px; }
.resource-line { margin-top: 23px; }
.second-resource { margin-top: 17px; }
.section-heading h2 { font-size: 15px; }
.section-heading > div > span { font-size: 10px; }
.portfolio-section { margin-top: 31px; }
.portfolio-table th { font-size: 9px; }
.portfolio-table td { padding-block: 15px; font-size: 10px; }
.portfolio-table .badge { font-size: 9px; }
.portfolio-name { font-size: 11px; }
.overview-bottom { margin-top: 27px; }
.fleet-ring { width: 226px; }
.ring-center > strong { font-size: 51px; }
.ring-center > span, .ring-legend span { font-size: 10px; }
.ring-center > span:last-child, .ring-legend small { font-size: 8px; }
.ring-legend { max-width: 195px; gap: 25px; }
.note-row { padding-block: 18px; }
.note-row strong { font-size: 11px; }
.note-row small { font-size: 9px; }
.notes-footer { font-size: 10px; }
.machine-table th { font-size: 10px; }
.machine-table td { padding-block: 18px; font-size: 11px; }
.machine-name a { max-width: 260px; font-size: 12px; }
.machine-name small { font-size: 9px; }
}
@media (max-width: 1360px) {
.app-shell { grid-template-columns: 342px minmax(0, 1fr); width: calc(100% - 32px); height: calc(100dvh - 32px); margin: 16px auto; }
.workspace-drawer { grid-template-columns: minmax(0, 1fr) 142px; gap: 12px; padding-inline: 16px 14px; }
.sidebar-heading h2 { font-size: 19px; }
.brand-tag { display: none; }
.nav-item { font-size: 9px; gap: 8px; padding-inline: 9px; }
.insight-card { padding-inline: 12px; }
.insight-card .card-caption { font-size: 8px; }
.insight-card p { font-size: 8px; }
.collection-label { padding-inline: 12px; }
.collection-label span { font-size: 7px; }
.topbar { padding-inline: 20px; }
.page-content { padding-inline: 20px; }
.metric-card { padding: 15px; }
.card-caption h2 { font-size: 10px; }
.fleet-number > span { font-size: 7px; }
.fleet-number { gap: 10px; }
.metric-bottom { font-size: 7px; }
.overview-bottom { grid-template-columns: 1.15fr 1fr; }
.activity-panel, .notes-panel { padding-inline: 16px; }
.fleet-ring { width: 165px; }
.ring-center > strong { font-size: 37px; }
.ring-legend { gap: 18px; }
.ring-legend small { display: none; }
.note-row { padding-block: 11px; }
.note-row strong { font-size: 8px; }
.note-row small { font-size: 7px; }
.notes-footer { font-size: 7px; }
.create-layout { grid-template-columns: minmax(0, 1.7fr) minmax(195px, .85fr); gap: 13px; }
.form-section { padding: 19px; }
.create-summary { padding: 21px 17px; }
.size-preset { padding: 10px 9px; }
.size-preset span { font-size: 7px; }
.compact-metric { padding: 17px; gap: 11px; }
.health-layout { grid-template-columns: 1.2fr 1fr; gap: 13px; }
}
@media (max-width: 1150px) {
.app-shell { grid-template-columns: 200px minmax(0, 1fr); }
.workspace-drawer { display: flex; padding: 25px 17px; }
.sidebar { width: 100%; }
.context-rail { display: none; }
.brand-tag { display: inline; }
.nav-item { font-size: 10px; }
.sidebar-heading h2 { font-size: 21px; }
.overview-bottom { grid-template-columns: 1.2fr 1fr; }
.health-layout { grid-template-columns: 1.15fr 1fr; }
.diagnostics-panel, .install-panel, .server-info { padding: 19px; }
.create-layout { grid-template-columns: minmax(0, 1.7fr) minmax(208px, .9fr); }
.detail-heading { align-items: flex-start; }
.ssh-panel { align-items: flex-start; flex-direction: column; gap: 15px; }
.ssh-command { width: 100%; }
}
@media (max-width: 900px) {
.app-shell { grid-template-columns: 182px minmax(0, 1fr); }
.workspace-drawer { padding-inline: 13px; }
.brand-tag { display: none; }
.nav-item { font-size: 9px; }
.global-search { max-width: 185px; }
.global-search kbd { display: none; }
.topbar { padding-inline: 18px; gap: 6px; }
.workspace-crumb { font-size: 8px; gap: 6px; padding-inline: 10px; }
.page-content { padding-inline: 18px; }
.page-heading h1 { font-size: 24px; }
.date-label { font-size: 8px; }
.overview-metrics { grid-template-columns: 1fr 1fr; gap: 11px; }
.resources-metric { grid-column: 1 / -1; min-height: auto; display: grid; grid-template-columns: 1fr 1fr; column-gap: 28px; padding-block: 14px; }
.resources-metric .card-caption { grid-column: 1 / -1; }
.resources-metric .resource-line { grid-row: 2; margin-top: 13px; }
.resources-metric .resource-track { grid-row: 3; }
.resources-metric .second-resource { grid-column: 2; }
.overview-bottom { grid-template-columns: 1fr; }
.activity-body { justify-content: center; gap: 30px; }
.fleet-ring { width: 185px; }
.ring-legend { max-width: 165px; gap: 23px; }
.ring-legend small { display: block; }
.notes-panel { padding: 20px; }
.note-row { padding-block: 12px; }
.note-row strong { font-size: 10px; }
.note-row small { font-size: 8px; }
.notes-footer { font-size: 9px; }
.compact-metrics { gap: 9px; }
.compact-metric { padding: 15px; gap: 9px; }
.compact-metric > .round-icon { display: none; }
.compact-metric strong { font-size: 24px; }
.compact-metric span { font-size: 8px; }
.filter-tab { padding-inline: 10px; font-size: 9px; }
.fleet-toolbar { flex-wrap: wrap; gap: 11px; }
.health-layout, .network-layout { grid-template-columns: 1fr; }
.health-side { grid-template-columns: 1fr 1fr; gap: 13px; align-items: start; }
.fabric-diagram { padding-block: 10px; }
.network-map-panel .fabric-diagram { display: grid; grid-template-columns: 1fr 50px 1fr; justify-items: center; }
.network-map-panel .fabric-connector { height: 1px; width: 45px; }
.network-map-panel .fabric-branches, .network-map-panel .fabric-guests { display: none; }
.network-map-panel .fabric-diagram > .muted { grid-column: 1 / -1; margin-top: 25px; }
.network-map-panel .panel-footnote { text-align: center; }
.create-layout { grid-template-columns: 1fr; }
.create-summary { position: static; padding: 23px; }
.summary-art { display: none; }
.create-summary .detail-list { display: grid; grid-template-columns: 1fr 1fr; gap: 0 25px; margin-block: 15px; }
.create-summary > .button { max-width: 300px; margin: auto; display: flex; }
.summary-note { margin-top: 17px; }
.detail-grid { grid-template-columns: 1fr; }
.detail-heading { flex-direction: column; gap: 17px; }
.auth-window { min-height: 590px; }
.auth-story { padding: 29px; }
.auth-story-copy h1 { font-size: 37px; }
.auth-panel { padding-inline: 30px; }
}
@media (max-width: 700px) {
html, body { background: #e9e5ef; }
.app-shell { display: block; width: calc(100% - 20px); height: auto; min-height: calc(100dvh - 20px); margin: 10px; border-radius: 21px; overflow: clip; }
.workspace { min-height: calc(100dvh - 22px); }
.workspace-drawer { position: fixed; inset: 10px auto 10px 10px; width: 237px; z-index: 42; padding: 25px 20px; background: #f2edf7f5; border: 1px solid #fff; border-radius: 22px; backdrop-filter: blur(25px); transform: translateX(calc(-100% - 16px)); visibility: hidden; transition: transform .3s var(--ease), visibility .3s; box-shadow: 15px 0 45px #72508412; }
.app-shell.nav-open .workspace-drawer { visibility: visible; transform: translateX(0); }
.nav-scrim:not([hidden]) { display: block; position: fixed; inset: 0; z-index: 41; background: #80648e36; backdrop-filter: blur(3px); }
.brand-tag { display: inline; }
.nav-item { font-size: 11px; min-height: 41px; }
.sidebar-heading h2 { font-size: 23px; }
.sidebar-bottom { padding-top: 35px; }
.sidebar-bottom > .quiet-link { margin-top: 20px; }
.mobile-menu { display: inline-flex; width: 29px; height: 29px; flex: 0 0 auto; }
.topbar { min-height: 61px; padding-inline: 14px; gap: 7px; }
.workspace-crumb { background: transparent; padding-inline: 2px; font-size: 10px; box-shadow: none; }
.workspace-crumb > span:not(:last-child) { display: none; }
#breadcrumb { color: #92749d; }
.refresh-button { margin-right: 2px; }
.global-search { max-width: 215px; min-width: 80px; }
.global-search input { font-size: 9px; }
.topbar-create { flex-shrink: 0; margin-left: 1px; }
.page-content { flex: 1 0 auto; overflow: visible; padding: 13px 17px 24px; scrollbar-gutter: auto; }
.page-heading { gap: 12px; margin-bottom: 20px; }
.page-heading h1 { font-size: 26px; }
.page-heading .subtle-label { font-size: 9px; }
.date-label { display: none; }
.metric-card { min-height: 147px; padding: 17px; }
.card-caption h2 { font-size: 11px; }
.fleet-number > strong { font-size: 39px; }
.fleet-number > span { font-size: 8px; }
.metric-bottom { font-size: 8px; }
.resources-metric { min-height: 101px; }
.portfolio-section { margin-top: 25px; }
.portfolio-table { min-width: 540px; }
.overview-bottom { margin-top: 19px; }
.activity-panel, .notes-panel { padding: 20px; }
.activity-body { gap: 24px; }
.fleet-ring { width: 192px; }
.ring-center > strong { font-size: 43px; }
.ring-legend span { font-size: 10px; }
.ring-legend small { font-size: 8px; }
.page-footer { flex-direction: column; gap: 5px; font-size: 8px; margin-top: 20px; }
.compact-metrics { margin-bottom: 21px; gap: 8px; }
.compact-metric { min-height: 83px; padding: 14px; }
.compact-metric strong { font-size: 26px; }
.compact-metric .metric-word { font-size: 17px; }
.compact-metric span { font-size: 8px; }
.fleet-toolbar { margin-bottom: 13px; }
.fleet-list-panel { padding-inline: 11px; }
.filter-tabs { width: 100%; justify-content: space-between; gap: 0; }
.filter-tab { flex: 1; font-size: 9px; padding-inline: 9px; }
.fleet-toolbar > .select-wrap { margin-left: auto; }
.list-search-row { gap: 8px; }
.search-field input { font-size: 9px; }
.list-search-row > span { font-size: 8px; }
.list-footer { font-size: 8px; }
.list-footer > span:last-child { display: none; }
.bulk-controls { flex-wrap: wrap; gap: 7px; }
.bulk-controls > span { font-size: 9px; }
.bulk-controls select { font-size: 8px; }
.bulk-controls .button { font-size: 9px; }
.health-side { grid-template-columns: 1fr; }
.health-layout { gap: 13px; }
.install-panel, .server-info, .diagnostics-panel { padding: 22px; }
.diagnostic-item p { font-size: 9px; }
.diagnostic-item h3 { font-size: 11px; }
.install-panel > p { font-size: 11px; }
.install-panel .toggle-field strong { font-size: 10px; }
.server-info .detail-list > div { flex-direction: row; }
.server-info .detail-list dd { max-width: 65%; text-align: right; }
.form-section { padding: 21px; }
.create-summary { padding: 23px; }
.size-preset { padding: 12px; }
.size-preset span { font-size: 8px; }
.field input { font-size: 12px; }
.detail-delete-row { align-items: flex-start; }
.detail-delete-row > span { max-width: 170px; }
.ports-panel > .section-heading, .log-panel > .section-heading, .resources-panel > .section-heading { align-items: flex-start; }
.ports-panel > .section-heading > div:first-child, .log-panel > .section-heading > div:first-child, .resources-panel > .section-heading > div:first-child { flex: 1; }
.auth-screen { padding: 20px; }
.auth-window { grid-template-columns: 1fr; min-height: 0; max-width: 440px; }
.auth-story { min-height: 240px; padding: 26px; border-right: 0; }
.auth-story-copy { margin-top: 27px; }
.auth-story-copy > .subtle-label { font-size: 8px; }
.auth-story-copy h1 { font-size: 34px; margin-top: 8px; }
.auth-story-copy p { display: none; }
.auth-sculpture { position: absolute; width: 130px; height: 150px; min-height: 0; right: -2px; bottom: 23px; opacity: .65; transform: scale(.62); transform-origin: right bottom; }
.sculpture-caption { display: none; }
.auth-story-footer { margin-top: 19px; font-size: 7px; }
.auth-panel { padding: 31px 28px 60px; }
.auth-lock { display: none; }
.auth-panel h2 { font-size: 25px; }
.auth-panel form { margin-top: 22px; }
.auth-help { bottom: 23px; }
.toast-container { bottom: 17px; right: 17px; }
.notice { align-items: flex-start; font-size: 9px; }
}
@media (max-width: 390px) {
.topbar { gap: 3px; padding-inline: 11px; }
.workspace-crumb { font-size: 9px; }
.global-search input { font-size: 8px; }
.page-content { padding-inline: 13px; }
.page-heading h1 { font-size: 23px; }
.page-heading > .button { padding-inline: 12px; font-size: 9px; }
.page-heading > .tag { font-size: 8px; padding-inline: 8px; }
.card-caption h2 { font-size: 10px; }
.metric-card { padding: 14px; }
.fleet-number > span { font-size: 7px; }
.metric-bottom { font-size: 7px; }
.signal-readouts small { font-size: 7px; }
.range-labels { font-size: 6px; }
.activity-body { gap: 12px; }
.fleet-ring { width: 160px; }
.ring-center > strong { font-size: 36px; }
.ring-legend span { font-size: 9px; }
.ring-legend small { font-size: 7px; }
.form-section { padding: 17px; }
.size-presets { gap: 7px; }
.size-preset { padding: 10px; }
.size-preset span { font-size: 7px; }
.form-grid { gap: 11px; }
.field > span:first-child { font-size: 9px; }
.section-heading h2 { font-size: 12px; }
.log-panel .section-heading { flex-wrap: wrap; }
.auth-screen { padding: 13px; }
}
@media (max-height: 820px) and (min-width: 1151px) {
.context-rail { gap: 13px; padding-top: 46px; }
.insight-card { min-height: 150px; }
.collection-art { height: 53px; }
.collection-label { min-height: 60px; }
.rail-footnote { display: none; }
.sidebar-bottom { padding-top: 30px; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; scroll-behavior: auto !important; }
}
-92
View File
@@ -1,92 +0,0 @@
Copyright 2018 The Manrope Project Authors (https://github.com/googlefonts/manrope)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
-216
View File
@@ -1,216 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#e8e7ed" />
<meta name="color-scheme" content="light" />
<title>UVM Control</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='10' fill='%23eee7ac'/%3E%3Cpath d='M9 11v7a7 7 0 0 0 14 0v-7' fill='none' stroke='%234b4854' stroke-width='3' stroke-linecap='round'/%3E%3C/svg%3E" />
<link rel="preload" href="/dashboard/static/fonts/manrope-latin.woff2" as="font" type="font/woff2" crossorigin />
<link rel="stylesheet" href="/dashboard/static/css/dashboard.css?v=2" />
<script src="/dashboard/static/js/dashboard.js?v=2" defer></script>
</head>
<body>
<svg class="icon-sprite" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<symbol id="i-grid" viewBox="0 0 24 24"><rect x="3.5" y="3.5" width="6" height="6" rx="1.7"/><rect x="14.5" y="3.5" width="6" height="6" rx="1.7"/><rect x="3.5" y="14.5" width="6" height="6" rx="1.7"/><rect x="14.5" y="14.5" width="6" height="6" rx="1.7"/></symbol>
<symbol id="i-server" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="6" rx="2"/><rect x="3" y="14" width="18" height="6" rx="2"/><path d="M7 7h.01M7 17h.01M15 7h3M15 17h3"/></symbol>
<symbol id="i-network" viewBox="0 0 24 24"><rect x="9" y="3" width="6" height="5" rx="1.5"/><rect x="2" y="16" width="6" height="5" rx="1.5"/><rect x="16" y="16" width="6" height="5" rx="1.5"/><path d="M12 8v4M5 16v-4h14v4"/></symbol>
<symbol id="i-heart" viewBox="0 0 24 24"><path d="M20.8 5.8a5.2 5.2 0 0 0-7.4 0L12 7.2l-1.4-1.4a5.2 5.2 0 0 0-7.4 7.4L12 22l8.8-8.8a5.2 5.2 0 0 0 0-7.4Z" transform="translate(0 -2) scale(1 0.95)"/><path d="M5 11h3l2-3 3 7 2-4h4"/></symbol>
<symbol id="i-plus" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></symbol>
<symbol id="i-arrow" viewBox="0 0 24 24"><path d="M5 12h14m-5-5 5 5-5 5"/></symbol>
<symbol id="i-back" viewBox="0 0 24 24"><path d="M19 12H5m5-5-5 5 5 5"/></symbol>
<symbol id="i-chevron" viewBox="0 0 24 24"><path d="m9 5 7 7-7 7"/></symbol>
<symbol id="i-search" viewBox="0 0 24 24"><circle cx="10.5" cy="10.5" r="6.5"/><path d="m16 16 4.5 4.5"/></symbol>
<symbol id="i-refresh" viewBox="0 0 24 24"><path d="M20 10a8 8 0 0 0-14-4L3 9m0-6v6h6M4 14a8 8 0 0 0 14 4l3-3m-6 0h6v6"/></symbol>
<symbol id="i-book" viewBox="0 0 24 24"><path d="M12 5v15M12 5C8 2 3 4 3 4v15s5-2 9 1c4-3 9-1 9-1V4s-5-2-9 1Z"/></symbol>
<symbol id="i-logout" viewBox="0 0 24 24"><path d="M10 4H5a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h5M8 12h13m-5-5 5 5-5 5"/></symbol>
<symbol id="i-lock" viewBox="0 0 24 24"><rect x="5" y="10" width="14" height="11" rx="3"/><path d="M8 10V7a4 4 0 0 1 8 0v3M12 14v3"/></symbol>
<symbol id="i-spark" viewBox="0 0 24 24"><path d="m12 3 2.7 6.3L21 12l-6.3 2.7L12 21l-2.7-6.3L3 12l6.3-2.7L12 3Z"/></symbol>
<symbol id="i-shield" viewBox="0 0 24 24"><path d="m12 3 8 3v6c0 5-8 9-8 9s-8-4-8-9V6l8-3Z"/><path d="m8 12 3 3 5-6"/></symbol>
<symbol id="i-cpu" viewBox="0 0 24 24"><rect x="6" y="6" width="12" height="12" rx="3"/><rect x="9" y="9" width="6" height="6" rx="1"/><path d="M9 3v3m6-3v3M9 18v3m6-3v3M3 9h3m-3 6h3m12-6h3m-3 6h3"/></symbol>
<symbol id="i-memory" viewBox="0 0 24 24"><rect x="3" y="6" width="18" height="12" rx="2"/><path d="M7 10h3v4H7zm7 0h3v4h-3zM7 18v3m5-3v3m5-3v3"/></symbol>
<symbol id="i-play" viewBox="0 0 24 24"><path d="m8 4 12 8-12 8V4Z"/></symbol>
<symbol id="i-stop" viewBox="0 0 24 24"><rect x="6" y="6" width="12" height="12" rx="2"/></symbol>
<symbol id="i-more" viewBox="0 0 24 24"><circle cx="5" cy="12" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/></symbol>
<symbol id="i-trash" viewBox="0 0 24 24"><path d="M3 6h18M9 6V3h6v3M5 6l1 15h12l1-15M10 10v7m4-7v7"/></symbol>
<symbol id="i-copy" viewBox="0 0 24 24"><rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h3"/></symbol>
<symbol id="i-terminal" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="16" rx="3"/><path d="m7 9 3 3-3 3m6 0h4"/></symbol>
<symbol id="i-check" viewBox="0 0 24 24"><path d="m5 12 4 4L19 6"/></symbol>
<symbol id="i-close" viewBox="0 0 24 24"><path d="m6 6 12 12M6 18 18 6"/></symbol>
<symbol id="i-info" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 11v6M12 7h.01"/></symbol>
<symbol id="i-menu" viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h16"/></symbol>
<symbol id="i-eye" viewBox="0 0 24 24"><path d="M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12Z"/><circle cx="12" cy="12" r="3"/></symbol>
<symbol id="i-download" viewBox="0 0 24 24"><path d="M12 3v12m-5-5 5 5 5-5M4 16v4h16v-4"/></symbol>
</svg>
<section id="auth-screen" class="auth-screen">
<div class="auth-window">
<div class="auth-story">
<a href="#overview" class="brand" aria-label="UVM Control"><span class="brand-flower" aria-hidden="true"><i></i><i></i><i></i><i></i></span><span>uvm<span class="brand-period">.</span></span><span class="brand-tag">workspace</span></a>
<div class="auth-story-copy"><span class="subtle-label">A little space for big ideas</span><h1>Your compute.<br />A little calmer.</h1><p>A considered workspace for your machines,<br />your network, and everything in between.</p></div>
<div class="auth-sculpture" aria-hidden="true">
<div class="sculpture-orbit orbit-outer"></div><div class="sculpture-orbit orbit-inner"></div>
<div class="sculpture-card sculpture-back"><span></span><i></i><i></i><i></i></div>
<div class="sculpture-card sculpture-middle"><span></span><i></i><i></i><i></i></div>
<div class="sculpture-card sculpture-front"><svg class="icon"><use href="#i-server"/></svg><span>Room to create.</span><div class="sculpture-lights"><i></i><i></i><i></i></div></div>
<div class="sculpture-caption"><span class="dot dot-yellow"></span>Small footprint. Your possibilities.</div>
</div>
<span class="auth-story-footer">MicroVMs, thoughtfully managed.</span>
</div>
<main class="auth-panel">
<div class="auth-panel-inner">
<span class="round-icon auth-lock"><svg class="icon"><use href="#i-lock"/></svg></span>
<span class="subtle-label">Your local workspace</span>
<h2>Welcome to UVM</h2>
<p>Bring your machines into view.<br />Connect with your management token to begin.</p>
<form id="auth-form">
<label class="field" for="token-input"><span>Management token</span><input id="token-input" name="token" type="password" placeholder="Enter your API token" autocomplete="off" spellcheck="false" required aria-describedby="auth-error" /></label>
<p id="auth-error" class="form-error" role="alert"></p>
<button id="auth-btn" class="button button-primary button-wide" type="submit"><span>Open workspace</span><svg class="icon"><use href="#i-arrow"/></svg></button>
</form>
<p class="auth-note"><svg class="icon"><use href="#i-shield"/></svg>Saved only for this browser session.</p>
</div>
<a class="quiet-link auth-help" href="/docs" target="_blank" rel="noopener">Explore the API <svg class="icon"><use href="#i-arrow"/></svg></a>
</main>
</div>
</section>
<section id="main-app" class="app-shell" hidden>
<button id="nav-scrim" class="nav-scrim" aria-label="Close navigation" hidden></button>
<aside id="workspace-nav" class="workspace-drawer" aria-label="Workspace navigation">
<div class="sidebar">
<a class="brand" href="#overview" aria-label="UVM Control overview"><span class="brand-flower" aria-hidden="true"><i></i><i></i><i></i><i></i></span><span>uvm<span class="brand-period">.</span></span><span class="brand-tag">workspace</span></a>
<div class="sidebar-heading"><span class="sidebar-heading-mark" aria-hidden="true">↳</span><h2><span id="sidebar-title">Management</span><span id="sidebar-subtitle">Overview</span></h2></div>
<button class="sidebar-search" type="button" data-action="search"><svg class="icon"><use href="#i-search"/></svg><span>Find in workspace</span><kbd>/</kbd></button>
<nav class="primary-nav" aria-label="Primary">
<a class="nav-item" href="#overview" data-nav="overview"><svg class="icon"><use href="#i-grid"/></svg><span>Overview</span><span class="nav-indicator"></span></a>
<a class="nav-item" href="#vms" data-nav="vms"><svg class="icon"><use href="#i-server"/></svg><span>Machines</span><span class="nav-count" id="nav-vm-count">—</span></a>
<a class="nav-item" href="#network" data-nav="network"><svg class="icon"><use href="#i-network"/></svg><span>Network & ports</span><span class="nav-indicator"></span></a>
<a class="nav-item" href="#system" data-nav="system"><svg class="icon"><use href="#i-heart"/></svg><span>Host health</span><span class="nav-health-dot" id="nav-health-dot"></span></a>
<a class="nav-item nav-create" href="#create" data-nav="create"><svg class="icon"><use href="#i-plus"/></svg><span>New machine</span><svg class="icon nav-end-icon"><use href="#i-plus"/></svg></a>
</nav>
<div class="sidebar-secondary"><a class="nav-item" href="/docs" target="_blank" rel="noopener"><svg class="icon"><use href="#i-book"/></svg><span>API reference</span><svg class="icon nav-end-icon"><use href="#i-arrow"/></svg></a></div>
<div class="sidebar-bottom">
<div class="workspace-identity"><span class="workspace-avatar">u</span><div><strong>Local workspace</strong><span><i id="connection-dot" class="dot"></i><span id="connection-label">Connecting</span></span></div></div>
<button id="logout-btn" class="quiet-link" type="button"><svg class="icon"><use href="#i-logout"/></svg>End session</button>
</div>
</div>
<div class="context-rail" aria-label="Workspace at a glance">
<a href="#system" class="insight-card">
<div class="card-caption"><span>Workspace insights</span><svg class="icon"><use href="#i-spark"/></svg></div>
<p id="insight-copy">A fresh perspective on your local compute.</p>
<div class="insight-matrix" aria-hidden="true"><span></span></div>
<span class="insight-footer"><span class="dot dot-yellow"></span><span id="insight-footer">Looking at the details</span></span>
</a>
<a href="#vms" class="collection collection-machines">
<div class="collection-art machine-art" aria-hidden="true"><div class="paper-machine back-paper"><span></span><i></i><i></i></div><div class="paper-machine front-paper"><svg class="icon"><use href="#i-server"/></svg><i></i><i></i><span></span></div></div>
<div class="collection-label"><div><strong>Machines</strong><span id="rail-machines">Your private fleet</span></div><svg class="icon"><use href="#i-chevron"/></svg></div>
</a>
<a href="#network" class="collection collection-network">
<div class="collection-art network-art" aria-hidden="true"><div class="network-paper"><span class="network-line"></span><i></i><i></i><i></i><svg class="icon"><use href="#i-network"/></svg></div></div>
<div class="collection-label"><div><strong>Network</strong><span id="rail-network">Connections, considered</span></div><svg class="icon"><use href="#i-chevron"/></svg></div>
</a>
<a href="#system" class="collection collection-simple"><div class="collection-label"><div><strong>Host health</strong><span id="rail-health">Checking the essentials</span></div><svg class="icon"><use href="#i-chevron"/></svg></div></a>
<a href="#create" class="collection collection-simple collection-new"><div class="collection-label"><div><strong>Something new?</strong><span>Make room for an idea</span></div><svg class="icon"><use href="#i-plus"/></svg></div></a>
<span class="rail-footnote">A small cloud.<br />Entirely yours.</span>
</div>
</aside>
<div class="workspace">
<header class="topbar">
<button id="nav-toggle" class="icon-button mobile-menu" type="button" aria-label="Open navigation" aria-expanded="false" aria-controls="workspace-nav"><svg class="icon"><use href="#i-menu"/></svg></button>
<div class="workspace-crumb"><span class="dot dot-yellow"></span><span>Workspace</span><span class="crumb-divider">/</span><span id="breadcrumb">Overview</span></div>
<button id="refresh-btn" class="icon-button refresh-button" type="button" aria-label="Refresh workspace" title="Refresh workspace"><svg class="icon"><use href="#i-refresh"/></svg></button>
<div class="global-search">
<svg class="icon"><use href="#i-search"/></svg><input id="global-search" type="search" placeholder="Find a machine or a page" autocomplete="off" role="combobox" aria-label="Search workspace" aria-autocomplete="list" aria-expanded="false" aria-controls="search-results" /><kbd>⌘ K</kbd>
<div id="search-results" class="search-results" role="listbox" aria-label="Search results" hidden></div>
</div>
<a href="#create" class="icon-button topbar-create" aria-label="Create a new machine" title="Create a new machine"><svg class="icon"><use href="#i-plus"/></svg></a>
</header>
<main id="main-content" class="page-content">
<div id="connection-banner" class="notice notice-error" role="status" hidden><svg class="icon"><use href="#i-info"/></svg><span id="connection-message"></span><button class="quiet-link" type="button" data-action="refresh">Try again</button></div>
<section id="page-overview" class="page" aria-labelledby="overview-heading" hidden>
<div class="page-heading"><div><span class="subtle-label">The bigger picture</span><h1 id="overview-heading" tabindex="-1">Workspace overview<span class="heading-period">.</span></h1></div><span id="today-label" class="date-label"></span></div>
<div class="overview-metrics">
<article class="metric-card fleet-metric"><div class="card-caption"><h2>Your machine fleet</h2><a href="#vms" class="mini-link" aria-label="View all machines"><svg class="icon"><use href="#i-chevron"/></svg></a></div><div class="fleet-number"><strong id="metric-total">—</strong><span>little machines,<br />endless possibilities</span></div><div class="metric-bottom"><span class="status-inline"><i class="dot dot-yellow"></i><span id="metric-running">Waiting for the fleet</span></span><span id="metric-stopped" class="muted">—</span></div></article>
<article class="metric-card"><div class="card-caption"><h2>Host signals</h2><a href="#system" class="mini-link" aria-label="View host signals"><svg class="icon"><use href="#i-chevron"/></svg></a></div><div class="signal-readouts"><div><span class="dot dot-sage"></span><strong id="checks-ready">—</strong><small>Ready</small></div><div><span class="dot dot-peach"></span><strong id="checks-attention">—</strong><small>Attention</small></div><div><span class="dot dot-lilac"></span><strong id="checks-total">—</strong><small>Checks</small></div></div><div class="segmented-track" aria-hidden="true"><span id="checks-ready-bar"></span><span id="checks-attention-bar"></span></div><div class="range-labels"><span>Host readiness</span><span id="checks-summary">Taking a look</span></div></article>
<article class="metric-card resources-metric"><div class="card-caption"><h2>Resource allocation</h2><svg class="icon caption-icon"><use href="#i-cpu"/></svg></div><div class="resource-line"><span id="metric-cpu">— vCPU</span><small id="cpu-active-label">Allocated</small></div><div class="resource-track"><span id="cpu-allocation-bar"></span></div><div class="resource-line second-resource"><span id="metric-ram">— MiB</span><small id="ram-active-label">Allocated</small></div><div class="resource-track memory-track"><span id="ram-allocation-bar"></span></div></article>
</div>
<section class="portfolio-section" aria-labelledby="portfolio-heading"><header class="section-heading"><div><h2 id="portfolio-heading">Machine portfolio</h2><span>A place for every workload.</span></div><a class="quiet-link" href="#vms">View all <svg class="icon"><use href="#i-arrow"/></svg></a></header><div id="overview-machines" class="portfolio-content"><div class="loading-state">Bringing your machines into view…</div></div></section>
<div class="overview-bottom">
<article class="activity-panel soft-panel"><header class="section-heading"><div><h2>Fleet activity</h2><span>A snapshot of your machine states.</span></div><svg class="icon caption-icon"><use href="#i-grid"/></svg></header><div class="activity-body"><div class="fleet-ring" role="img" id="fleet-ring" aria-label="Waiting for machine activity"><svg viewBox="0 0 200 200" aria-hidden="true"><circle class="ring-track" cx="100" cy="100" r="82"/><circle id="ring-running" class="ring-segment ring-running" cx="100" cy="100" r="82" pathLength="100"/><circle id="ring-stopped" class="ring-segment ring-stopped" cx="100" cy="100" r="82" pathLength="100"/><circle id="ring-other" class="ring-segment ring-other" cx="100" cy="100" r="82" pathLength="100"/></svg><div class="ring-center"><span>Running now</span><strong id="ring-percent">—<small>%</small></strong><span id="ring-total">Your fleet, in balance</span></div></div><div class="ring-legend"><div><i class="dot dot-yellow"></i><span>Running<small>Doing their thing</small></span><strong id="legend-running">—</strong></div><div><i class="dot dot-peach"></i><span>Stopped<small>Taking a breather</small></span><strong id="legend-stopped">—</strong></div><div><i class="dot dot-lilac"></i><span>Other states<small>A closer look</small></span><strong id="legend-other">—</strong></div></div></div></article>
<article class="notes-panel soft-panel"><header class="section-heading"><div><h2>Worth a little attention</h2><span>The useful details, together.</span></div><svg class="icon caption-icon"><use href="#i-spark"/></svg></header><div id="workspace-notes" class="workspace-notes"></div><a href="#system" class="notes-footer quiet-link">A closer look at your host <svg class="icon"><use href="#i-arrow"/></svg></a></article>
</div>
<footer class="page-footer"><span><i class="dot" id="footer-dot"></i>Live from your local control plane</span><span id="last-updated">Waiting for an update</span></footer>
</section>
<section id="page-vms" class="page" aria-labelledby="machines-heading" hidden>
<div class="page-heading"><div><span class="subtle-label">A collection of possibilities</span><h1 id="machines-heading" tabindex="-1">Your machines<span class="heading-period">.</span></h1></div><a class="button button-primary" href="#create"><svg class="icon"><use href="#i-plus"/></svg>New machine</a></div>
<div id="fleet-summary" class="compact-metrics"></div>
<div class="fleet-toolbar"><div class="filter-tabs" role="group" aria-label="Filter machines by state"><button type="button" class="filter-tab active" data-filter="all" aria-pressed="true">All machines</button><button type="button" class="filter-tab" data-filter="running" aria-pressed="false">Running</button><button type="button" class="filter-tab" data-filter="stopped" aria-pressed="false">Stopped</button><button type="button" class="filter-tab" data-filter="attention" aria-pressed="false">Attention</button></div><label class="select-wrap"><span class="sr-only">Sort machines</span><select id="vm-sort"><option value="recent">Newest first</option><option value="name">Name A–Z</option><option value="status">Machine state</option></select></label></div>
<div class="fleet-list-panel"><div class="list-search-row"><label class="search-field"><svg class="icon"><use href="#i-search"/></svg><input id="vm-search" type="search" placeholder="Find by name, ID, or address" aria-label="Search machines" /></label><span id="visible-count" class="muted"></span></div><div id="bulk-controls" class="bulk-controls" hidden><span id="bulk-count"></span><label class="select-wrap"><span class="sr-only">Bulk action</span><select id="bulk-action"><option value="start">Start machines</option><option value="stop">Stop machines</option><option value="restart">Restart machines</option><option value="ports-sync">Sync port policies</option><option value="delete">Delete machines</option></select></label><button id="bulk-run-btn" class="button button-small button-primary" type="button">Apply</button><button class="icon-button" type="button" data-action="clear-selection" aria-label="Clear selection"><svg class="icon"><use href="#i-close"/></svg></button></div><div id="machine-table-wrap" class="table-scroll"><table class="machine-table"><thead><tr><th class="check-cell"><input id="vm-select-all" type="checkbox" aria-label="Select all visible machines" /></th><th>Machine</th><th>State</th><th>Allocation</th><th>Ingress</th><th>Policy</th><th class="row-controls-heading"><span class="sr-only">Actions</span></th></tr></thead><tbody id="machine-rows"></tbody></table></div><div id="machines-empty" hidden></div><div class="list-footer"><span id="fleet-list-caption">Your private machine registry.</span><span>Made for a small footprint.</span></div></div>
<div id="bulk-results" class="operation-results" aria-live="polite" hidden></div>
</section>
<section id="page-network" class="page" aria-labelledby="network-heading" hidden>
<div class="page-heading"><div><span class="subtle-label">Good connections start here</span><h1 id="network-heading" tabindex="-1">Network & ports<span class="heading-period">.</span></h1></div><span class="tag"><svg class="icon"><use href="#i-network"/></svg>Private IPv4 · TCP</span></div>
<div id="network-summary" class="compact-metrics"></div>
<div class="network-layout"><article class="soft-panel network-map-panel"><header class="section-heading"><div><h2>Your private fabric</h2><span>One host. Room to connect.</span></div></header><div class="fabric-diagram"><div class="fabric-node"><span class="round-icon"><svg class="icon"><use href="#i-server"/></svg></span><strong>Local host</strong><small>UVM control plane</small></div><div class="fabric-connector"></div><div class="fabric-node fabric-gateway"><span class="round-icon"><svg class="icon"><use href="#i-network"/></svg></span><strong id="network-gateway">No gateway yet</strong><small>Private gateway</small></div><div class="fabric-branches" aria-hidden="true"><span></span><span></span><span></span></div><div class="fabric-guests" aria-hidden="true"><svg class="icon"><use href="#i-server"/></svg><svg class="icon"><use href="#i-server"/></svg><svg class="icon"><use href="#i-server"/></svg></div><span id="network-machine-count" class="muted">Waiting for your fleet</span></div><p class="panel-footnote">Port rules allow private guest TCP traffic. HTTP and HTTPS are descriptive labels for TCP services.</p></article><article class="soft-panel network-policies"><header class="section-heading"><div><h2>Ports, with purpose</h2><span>Choose a machine to manage its rules.</span></div><svg class="icon caption-icon"><use href="#i-shield"/></svg></header><div id="network-policies"></div></article></div>
</section>
<section id="page-vm-detail" class="page" aria-labelledby="detail-heading" hidden>
<a class="quiet-link back-link" href="#vms"><svg class="icon"><use href="#i-back"/></svg>Back to machines</a>
<div id="detail-loading" class="loading-state">Getting to know this machine…</div>
<div id="detail-content" hidden>
<div class="page-heading detail-heading"><div><span class="subtle-label">A closer look</span><h1 id="detail-heading" tabindex="-1"></h1><span id="detail-id" class="mono muted"></span></div><div id="detail-actions" class="button-row"></div></div>
<div id="detail-error" class="notice notice-error" hidden></div>
<div class="detail-grid"><article class="soft-panel detail-card"><header class="section-heading"><h2>The essentials</h2><span id="detail-status"></span></header><dl id="detail-config" class="detail-list"></dl></article><article class="soft-panel detail-card"><header class="section-heading"><h2>A place in the network</h2><svg class="icon caption-icon"><use href="#i-network"/></svg></header><dl id="detail-network" class="detail-list"></dl></article></div>
<article class="soft-panel resources-panel">
<header class="section-heading"><div><h2>Room to grow</h2><span>Adjust CPU, memory, and private storage.</span></div><button id="edit-resources-btn" class="button button-small" type="button" data-action="edit-resources">Resize resources</button></header>
<p id="resize-note" class="panel-footnote">Resource changes stop the machine, apply the new size, and start it again with its existing IP and guest credentials.</p>
<form id="resize-form" hidden>
<div class="form-grid"><label class="field" for="resize-cpu"><span>CPU capacity</span><input id="resize-cpu" type="number" min="0.01" step="any" placeholder="Keep current CPU" /></label><label class="field" for="resize-ram"><span>Memory</span><input id="resize-ram" type="text" placeholder="Keep current memory" /><small>At least 128 MiB. For example, 4G.</small></label></div>
<label class="field storage-field" for="resize-storage"><span>Private storage</span><input id="resize-storage" type="text" placeholder="Keep current size (at least 30G)" /><small>Minimum 30 GiB. A smaller disk must still fit the existing filesystem data.</small></label>
<p id="resize-error" class="form-error" role="alert"></p>
<div class="button-row"><button id="resize-submit" class="button button-primary button-small" type="submit">Resize &amp; restart</button><button class="button button-quiet button-small" type="button" data-action="cancel-resize">Cancel</button></div>
</form>
</article>
<article class="soft-panel ssh-panel"><div><span class="round-icon"><svg class="icon"><use href="#i-terminal"/></svg></span><div><h2>A direct connection</h2><p id="ssh-hint" class="muted"></p></div></div><div class="ssh-command"><code id="ssh-command"></code><button class="icon-button" data-action="copy-ssh" type="button" aria-label="Copy SSH command"><svg class="icon"><use href="#i-copy"/></svg></button></div></article>
<article class="soft-panel ports-panel"><header class="section-heading"><div><h2>Private port policy</h2><span>Just the connections this machine needs.</span></div><div class="button-row"><button id="edit-ports-btn" class="button button-small" type="button" data-action="edit-ports">Edit rules</button><button id="sync-ports-btn" class="icon-button" type="button" data-action="sync-ports" aria-label="Sync port policy" title="Sync port policy"><svg class="icon"><use href="#i-refresh"/></svg></button></div></header><div id="detail-ports" class="port-list"></div><form id="port-editor" class="port-editor" hidden><label class="field" for="port-input"><span>Port rules <small>PORT:TCP, PORT:HTTP, or PORT:HTTPS</small></span><input id="port-input" type="text" placeholder="22:TCP, 80:HTTP, 443:HTTPS" spellcheck="false" /></label><p id="port-error" class="form-error" role="alert"></p><div class="button-row"><button class="button button-primary button-small" type="submit" value="set">Replace rules</button><button id="open-ports-btn" class="button button-small" type="submit" value="open">Open ports</button><button id="close-ports-btn" class="button button-small" type="submit" value="close">Close ports</button><button class="button button-quiet button-small" type="button" data-action="cancel-ports">Cancel</button></div><p class="panel-footnote">An empty replacement creates a managed policy with no inbound TCP ports.</p></form></article>
<article class="soft-panel log-panel"><header class="section-heading"><div><h2>A window into the runtime</h2><span>Recent Firecracker log entries.</span></div><div class="button-row"><label class="select-wrap"><span class="sr-only">Log tail length</span><select id="log-tail"><option value="100">100 lines</option><option value="250">250 lines</option><option value="500">500 lines</option><option value="1000">1,000 lines</option></select></label><button id="log-refresh-btn" class="icon-button" type="button" data-action="refresh-logs" aria-label="Refresh logs"><svg class="icon"><use href="#i-refresh"/></svg></button></div></header><pre id="log-output" class="log-output" tabindex="0"></pre><p id="log-meta" class="panel-footnote"></p></article>
<div class="detail-delete-row"><span>Every machine has its own private disk.</span><button class="quiet-link danger-text" type="button" data-action="delete-current"><svg class="icon"><use href="#i-trash"/></svg>Delete machine</button></div>
</div>
</section>
<section id="page-create" class="page" aria-labelledby="create-heading" hidden>
<div class="page-heading"><div><span class="subtle-label">Every idea starts somewhere</span><h1 id="create-heading" tabindex="-1">Make something new<span class="heading-period">.</span></h1></div><a class="quiet-link" href="#vms">Back to machines <svg class="icon"><use href="#i-arrow"/></svg></a></div>
<form id="create-form" class="create-layout">
<div class="create-fields"><section class="soft-panel form-section"><header class="section-heading"><div><h2><span class="step-number">01</span>A name and a little space</h2><span>Give your new machine a shape.</span></div></header><label class="field" for="create-name"><span>Machine name <small>Optional</small></span><input id="create-name" type="text" name="name" placeholder="e.g. my-little-server" pattern="[a-z][a-z0-9\-]{0,62}" maxlength="63" autocomplete="off" /></label><fieldset class="preset-fieldset"><legend>Start with a size</legend><div class="size-presets"><button class="size-preset active" type="button" data-preset="small" aria-pressed="true"><svg class="icon"><use href="#i-server"/></svg><strong>Small</strong><span>1 vCPU · 512 MiB</span></button><button class="size-preset" type="button" data-preset="balanced" aria-pressed="false"><svg class="icon"><use href="#i-server"/></svg><strong>Balanced</strong><span>2 vCPU · 1 GiB</span></button><button class="size-preset" type="button" data-preset="roomy" aria-pressed="false"><svg class="icon"><use href="#i-server"/></svg><strong>Roomy</strong><span>4 vCPU · 2 GiB</span></button></div></fieldset><div class="form-grid"><label class="field" for="create-cpu"><span>CPU capacity</span><input id="create-cpu" name="cpu" type="number" value="1" min="0.01" step="any" required /><small>Fractional requests round up to whole vCPUs.</small></label><label class="field" for="create-ram"><span>Memory</span><input id="create-ram" name="ram" type="text" value="512M" required /><small>At least 128 MiB. Try 512M, 1G, or 2G.</small></label></div></section>
<section class="soft-panel form-section"><label class="field" for="create-storage"><span>Private storage</span><input id="create-storage" name="storage" type="text" value="30G" required /><small>Minimum 30 GiB. Try 30G, 50G, or 1T. You can resize this disk later.</small></label></section>
<section class="soft-panel form-section"><header class="section-heading"><div><h2><span class="step-number">02</span>A way to connect</h2><span>Private networking and guest access.</span></div></header><label class="field" for="create-guest-ip"><span>Guest IPv4 address <small>Optional</small></span><input id="create-guest-ip" name="guest_ip" type="text" placeholder="Automatically assigned" autocomplete="off" /></label><div class="form-grid"><label class="field" for="create-username"><span>Guest user</span><input id="create-username" name="username" type="text" value="root" autocomplete="username" required /></label><label class="field" for="create-password"><span>Guest password</span><span class="password-field"><input id="create-password" name="password" type="password" value="root" autocomplete="new-password" maxlength="128" required /><button class="icon-button" type="button" data-action="toggle-password" aria-label="Show password" aria-pressed="false"><svg class="icon"><use href="#i-eye"/></svg></button></span></label></div><p class="panel-footnote">Missing users are created with a home directory and sudo access using their guest password. Credentials are applied to the machine’s private disk.</p></section>
<section class="soft-panel form-section"><header class="section-heading"><div><h2><span class="step-number">03</span>Open the right doors</h2><span>Choose which private TCP services are allowed.</span></div></header><label class="toggle-field"><span><strong>Include the default SSH rule</strong><small>Allow TCP port 22 for terminal access.</small></span><input id="create-default-ssh" name="default_ssh" type="checkbox" checked /><span class="toggle-track" aria-hidden="true"></span></label><label class="field" for="create-ports"><span>Additional port rules <small>Optional</small></span><input id="create-ports" name="ports" type="text" placeholder="80:HTTP, 443:HTTPS" spellcheck="false" /><small>Separate rules with commas. HTTP and HTTPS both use TCP.</small></label></section></div>
<aside class="create-summary soft-panel"><div class="summary-art" aria-hidden="true"><div></div><div></div><span><svg class="icon"><use href="#i-server"/></svg></span></div><span class="subtle-label">Your next little machine</span><h2 id="create-summary-name">Room for an idea.</h2><p>A private disk, a network identity,<br />and a fresh start.</p><dl id="create-summary-details" class="detail-list"></dl><p id="create-error" class="form-error" role="alert"></p><button id="create-submit" class="button button-primary button-wide" type="submit"><span>Create machine</span><svg class="icon"><use href="#i-arrow"/></svg></button><a href="#vms" class="quiet-link summary-cancel">Maybe later</a><span class="summary-note"><svg class="icon"><use href="#i-spark"/></svg>Built on Firecracker</span></aside>
</form>
</section>
<section id="page-system" class="page" aria-labelledby="system-heading" hidden>
<div class="page-heading"><div><span class="subtle-label">Behind every little machine</span><h1 id="system-heading" tabindex="-1">A look at your host<span class="heading-period">.</span></h1></div><button id="diagnostics-refresh-btn" class="button" type="button" data-action="diagnostics"><svg class="icon"><use href="#i-refresh"/></svg>Run checks</button></div>
<div id="health-summary" class="compact-metrics"></div>
<div class="health-layout"><article class="soft-panel diagnostics-panel"><header class="section-heading"><div><h2>The essentials, checked</h2><span>Readiness checks from your local host.</span></div><span id="diagnostics-count" class="tag">Checking</span></header><div id="diagnostics-list" class="diagnostics-list"><div class="loading-state">Checking in with your host…</div></div></article><div class="health-side"><article class="soft-panel install-panel"><span class="round-icon"><svg class="icon"><use href="#i-download"/></svg></span><h2>A foundation for your fleet</h2><p>Install Firecracker, a guest kernel, and a root filesystem. The default Ubuntu 22.04 image includes SSH, sudo, and apt.</p><div class="install-assets"><span><svg class="icon"><use href="#i-cpu"/></svg>Firecracker</span><span><svg class="icon"><use href="#i-terminal"/></svg>Guest kernel</span><span><svg class="icon"><use href="#i-server"/></svg>Root filesystem</span></div><label class="toggle-field"><span><strong>Refresh existing assets</strong><small>Refresh downloads and rebuild the Ubuntu image.</small></span><input id="install-force" type="checkbox" /><span class="toggle-track" aria-hidden="true"></span></label><button id="install-btn" class="button button-primary button-wide" type="button" data-action="install"><span>Install assets</span><svg class="icon"><use href="#i-arrow"/></svg></button><pre id="install-result" class="install-result" role="status" hidden></pre></article><article class="soft-panel server-info"><header class="section-heading"><h2>Close to home</h2><svg class="icon caption-icon"><use href="#i-network"/></svg></header><dl class="detail-list"><div><dt>API origin</dt><dd id="info-api-url"></dd></div><div><dt>Last host check</dt><dd id="info-observed">Not checked yet</dd></div><div><dt>Connection</dt><dd>Session token</dd></div></dl><a href="/docs" target="_blank" rel="noopener" class="quiet-link">Open API reference <svg class="icon"><use href="#i-arrow"/></svg></a></article></div></div>
</section>
</main>
</div>
</section>
<div id="action-menu" class="action-menu" role="menu" aria-label="Machine actions" hidden></div>
<div id="toast-container" class="toast-container" aria-live="polite" aria-atomic="false"></div>
<dialog id="confirm-dialog" class="confirm-dialog" aria-labelledby="confirm-title" aria-describedby="confirm-body"><form method="dialog"><span class="round-icon" id="confirm-icon"><svg class="icon"><use href="#i-info"/></svg></span><span class="subtle-label">A moment before we continue</span><h2 id="confirm-title"></h2><p id="confirm-body"></p><div class="dialog-actions"><button class="button button-quiet" value="cancel" autofocus>Cancel</button><button id="confirm-ok" class="button button-primary" value="confirm">Confirm</button></div></form></dialog>
<noscript><p class="noscript-message">Enable JavaScript to connect to your UVM workspace.</p></noscript>
</body>
</html>
File diff suppressed because it is too large Load Diff
-17
View File
@@ -1,17 +0,0 @@
"""Dashboard frontend routes."""
from __future__ import annotations
from pathlib import Path
from fastapi import APIRouter
from fastapi.responses import HTMLResponse
router = APIRouter()
_DASHBOARD_HTML = Path(__file__).parent.parent / "dashboard" / "static" / "index.html"
@router.get("", response_class=HTMLResponse)
def get_dashboard() -> str:
return _DASHBOARD_HTML.read_text(encoding="utf-8")
+1 -1
View File
@@ -25,7 +25,7 @@ def exit_server(
if server is None:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="server shutdown is unavailable outside uvm --serve",
detail="server shutdown is unavailable outside nest --serve or uvm --serve",
)
background_tasks.add_task(_request_server_exit, server)
return ServerExitResponse()
+10 -13
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
from collections.abc import Callable
from ipaddress import ip_address
from pathlib import Path
import ssl
@@ -27,8 +28,7 @@ def create_api(application: Application | None = None, *, host: str | None = Non
try:
from fastapi import FastAPI
from fastapi.responses import JSONResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from fastapi.responses import JSONResponse
except ModuleNotFoundError as error:
raise UvmError(
"FastAPI server support is not installed. Install this project with its dependencies."
@@ -39,7 +39,6 @@ def create_api(application: Application | None = None, *, host: str | None = Non
from .routers.installation import router as installation_router
from .routers.vms import router as vms_router
from .routers.diagnostics import router as diagnostics_router
from .routers.dashboard import router as dashboard_router
api = FastAPI(
title="uvm",
@@ -61,18 +60,16 @@ def create_api(application: Application | None = None, *, host: str | None = Non
api.include_router(vms_router)
api.include_router(diagnostics_router)
static_dir = Path(__file__).parent / "dashboard" / "static"
api.mount("/dashboard/static", StaticFiles(directory=static_dir), name="dashboard_static")
api.include_router(dashboard_router, prefix="/dashboard")
@api.get("/")
async def root() -> Any:
return RedirectResponse(url="/dashboard")
return api
def run_server(application: Application, *, host: str, port: int) -> None:
def run_server(
application: Application,
*,
host: str,
port: int,
api_factory: Callable[..., Any] | None = None,
) -> None:
"""Run Uvicorn after enforcing the local-management security boundary."""
settings = application.settings
@@ -93,7 +90,7 @@ def run_server(application: Application, *, host: str, port: int) -> None:
if settings.api_tls_cert is not None:
options["ssl_certfile"] = str(settings.api_tls_cert)
options["ssl_keyfile"] = str(settings.api_tls_key)
api = create_api(application, host=host)
api = (api_factory or create_api)(application, host=host)
server = uvicorn.Server(uvicorn.Config(api, **options))
api.state.uvicorn_server = server
server.run()