feat: Implement UVM Dashboard with frontend and backend integration

- Added dashboard.js for frontend functionality, including VM management and system health monitoring.
- Created dashboard route to serve the dashboard HTML.
- Introduced diagnostics route for token-protected host diagnostics.
- Enhanced VMs router with bulk actions, VM log retrieval, and port management.
- Updated lifecycle service to read VM log tails with truncation handling.
- Integrated static file serving for dashboard assets in the FastAPI server.
- Redirect root URL to the dashboard for improved user experience.
This commit is contained in:
2026-09-19 10:50:29 +05:30
parent 3d7af4adb8
commit b3b1b527e1
17 changed files with 2285 additions and 69 deletions
+45
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
import os
import re
import shutil
import time
@@ -39,6 +40,15 @@ class OperationResult:
error: str | None = None
@dataclass(frozen=True, slots=True)
class LogTail:
content: str
truncated: bool
_MAX_LOG_TAIL_BYTES = 262_144
class LifecycleService:
"""All mutating entry points use operation lock -> short state transaction."""
@@ -415,6 +425,41 @@ class LifecycleService:
self._validate_paths(vm)
return Path(vm.log)
def read_log_tail(self, identifier: str, *, tail: int) -> LogTail:
"""Read a bounded tail from a validated VM log without following it."""
if type(tail) is not int or tail < 0:
raise ValidationError("log tail must be a non-negative integer")
path = self.log_path(identifier)
try:
with path.open("rb") as stream:
end = stream.seek(0, os.SEEK_END)
if tail == 0:
return LogTail(content="", truncated=False)
start = max(0, end - _MAX_LOG_TAIL_BYTES)
previous = b""
if start:
stream.seek(start - 1)
previous = stream.read(1)
stream.seek(start)
content = stream.read(end - start)
except OSError as error:
raise UvmError("could not read VM log") from error
truncated = False
if start and previous != b"\n":
newline = content.find(b"\n")
if newline == -1:
truncated = True
else:
content = content[newline + 1:]
lines = content.splitlines(keepends=True)
if start and len(lines) < tail:
truncated = True
return LogTail(
content=b"".join(lines[-tail:]).decode("utf-8", errors="replace"),
truncated=truncated,
)
def diagnostics(self) -> list[dict]:
checks = [("root", require_root), ("kvm", check_kvm),
("Firecracker", self._verify_firecracker),