ui: improve account/apps layout and lab status responsiveness
This commit is contained in:
parent
6b9aaa3e53
commit
35f7b77c1b
@ -60,6 +60,9 @@ def register(app) -> None:
|
|||||||
if cached and (now - float(_LAB_STATUS_CACHE.get("ts", 0.0)) < settings.LAB_STATUS_CACHE_SEC):
|
if cached and (now - float(_LAB_STATUS_CACHE.get("ts", 0.0)) < settings.LAB_STATUS_CACHE_SEC):
|
||||||
return jsonify(cached)
|
return jsonify(cached)
|
||||||
|
|
||||||
|
t_total = time.perf_counter()
|
||||||
|
timings_ms: dict[str, int] = {}
|
||||||
|
|
||||||
connected = False
|
connected = False
|
||||||
atlas_up = False
|
atlas_up = False
|
||||||
atlas_known = False
|
atlas_known = False
|
||||||
@ -71,7 +74,9 @@ def register(app) -> None:
|
|||||||
|
|
||||||
# Atlas
|
# Atlas
|
||||||
try:
|
try:
|
||||||
|
t_probe = time.perf_counter()
|
||||||
atlas_grafana_ok = _http_ok(settings.GRAFANA_HEALTH_URL, expect_substring="ok")
|
atlas_grafana_ok = _http_ok(settings.GRAFANA_HEALTH_URL, expect_substring="ok")
|
||||||
|
timings_ms["grafana"] = int((time.perf_counter() - t_probe) * 1000)
|
||||||
if atlas_grafana_ok:
|
if atlas_grafana_ok:
|
||||||
connected = True
|
connected = True
|
||||||
atlas_up = True
|
atlas_up = True
|
||||||
@ -82,7 +87,9 @@ def register(app) -> None:
|
|||||||
|
|
||||||
if not atlas_known:
|
if not atlas_known:
|
||||||
try:
|
try:
|
||||||
|
t_probe = time.perf_counter()
|
||||||
value = _vm_query("up")
|
value = _vm_query("up")
|
||||||
|
timings_ms["victoria_metrics"] = int((time.perf_counter() - t_probe) * 1000)
|
||||||
if value is not None:
|
if value is not None:
|
||||||
connected = True
|
connected = True
|
||||||
atlas_known = True
|
atlas_known = True
|
||||||
@ -93,7 +100,9 @@ def register(app) -> None:
|
|||||||
|
|
||||||
# Oceanus (node-exporter direct probe)
|
# Oceanus (node-exporter direct probe)
|
||||||
try:
|
try:
|
||||||
|
t_probe = time.perf_counter()
|
||||||
if _http_ok(settings.OCEANUS_NODE_EXPORTER_URL):
|
if _http_ok(settings.OCEANUS_NODE_EXPORTER_URL):
|
||||||
|
timings_ms["oceanus_node_exporter"] = int((time.perf_counter() - t_probe) * 1000)
|
||||||
connected = True
|
connected = True
|
||||||
oceanus_known = True
|
oceanus_known = True
|
||||||
oceanus_up = True
|
oceanus_up = True
|
||||||
@ -101,14 +110,16 @@ def register(app) -> None:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
timings_ms["total"] = int((time.perf_counter() - t_total) * 1000)
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"connected": connected,
|
"connected": connected,
|
||||||
"atlas": {"up": atlas_up, "known": atlas_known, "source": atlas_source},
|
"atlas": {"up": atlas_up, "known": atlas_known, "source": atlas_source},
|
||||||
"oceanus": {"up": oceanus_up, "known": oceanus_known, "source": oceanus_source},
|
"oceanus": {"up": oceanus_up, "known": oceanus_known, "source": oceanus_source},
|
||||||
"checked_at": int(now),
|
"checked_at": int(now),
|
||||||
|
"timings_ms": timings_ms,
|
||||||
}
|
}
|
||||||
|
|
||||||
_LAB_STATUS_CACHE["ts"] = now
|
_LAB_STATUS_CACHE["ts"] = now
|
||||||
_LAB_STATUS_CACHE["value"] = payload
|
_LAB_STATUS_CACHE["value"] = payload
|
||||||
return jsonify(payload)
|
return jsonify(payload)
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,7 @@ VM_QUERY_TIMEOUT_SEC = float(os.getenv("VM_QUERY_TIMEOUT_SEC", "2"))
|
|||||||
HTTP_CHECK_TIMEOUT_SEC = float(os.getenv("HTTP_CHECK_TIMEOUT_SEC", "2"))
|
HTTP_CHECK_TIMEOUT_SEC = float(os.getenv("HTTP_CHECK_TIMEOUT_SEC", "2"))
|
||||||
K8S_API_TIMEOUT_SEC = float(os.getenv("K8S_API_TIMEOUT_SEC", "5"))
|
K8S_API_TIMEOUT_SEC = float(os.getenv("K8S_API_TIMEOUT_SEC", "5"))
|
||||||
LAB_STATUS_CACHE_SEC = float(os.getenv("LAB_STATUS_CACHE_SEC", "30"))
|
LAB_STATUS_CACHE_SEC = float(os.getenv("LAB_STATUS_CACHE_SEC", "30"))
|
||||||
GRAFANA_HEALTH_URL = os.getenv("GRAFANA_HEALTH_URL", "https://metrics.bstein.dev/api/health")
|
GRAFANA_HEALTH_URL = os.getenv("GRAFANA_HEALTH_URL", "http://grafana.monitoring.svc.cluster.local/api/health")
|
||||||
OCEANUS_NODE_EXPORTER_URL = os.getenv("OCEANUS_NODE_EXPORTER_URL", "http://192.168.22.24:9100/metrics")
|
OCEANUS_NODE_EXPORTER_URL = os.getenv("OCEANUS_NODE_EXPORTER_URL", "http://192.168.22.24:9100/metrics")
|
||||||
|
|
||||||
AI_CHAT_API = os.getenv("AI_CHAT_API", "http://ollama.ai.svc.cluster.local:11434").rstrip("/")
|
AI_CHAT_API = os.getenv("AI_CHAT_API", "http://ollama.ai.svc.cluster.local:11434").rstrip("/")
|
||||||
|
|||||||
@ -25,19 +25,33 @@ const networkData = ref(fallbackNetwork());
|
|||||||
const metricsData = ref(fallbackMetrics());
|
const metricsData = ref(fallbackMetrics());
|
||||||
|
|
||||||
const statusLoading = ref(true);
|
const statusLoading = ref(true);
|
||||||
|
const statusFetching = ref(false);
|
||||||
const statusError = ref("");
|
const statusError = ref("");
|
||||||
|
|
||||||
async function refreshLabStatus() {
|
async function refreshLabStatus() {
|
||||||
|
if (statusFetching.value) return;
|
||||||
|
statusFetching.value = true;
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = window.setTimeout(() => controller.abort(), 6000);
|
||||||
try {
|
try {
|
||||||
const resp = await fetch("/api/lab/status", { headers: { Accept: "application/json" } });
|
const resp = await fetch("/api/lab/status", {
|
||||||
|
headers: { Accept: "application/json" },
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
if (!resp.ok) throw new Error(`status ${resp.status}`);
|
if (!resp.ok) throw new Error(`status ${resp.status}`);
|
||||||
labStatus.value = await resp.json();
|
labStatus.value = await resp.json();
|
||||||
statusError.value = "";
|
statusError.value = "";
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
labStatus.value = null;
|
labStatus.value = null;
|
||||||
statusError.value = "Live data unavailable";
|
if (err?.name === "AbortError") {
|
||||||
|
statusError.value = "Live data timed out";
|
||||||
|
} else {
|
||||||
|
statusError.value = "Live data unavailable";
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
window.clearTimeout(timeoutId);
|
||||||
statusLoading.value = false;
|
statusLoading.value = false;
|
||||||
|
statusFetching.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -25,7 +25,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section v-if="auth.ready && auth.authenticated">
|
<section v-if="auth.ready && auth.authenticated">
|
||||||
<div class="grid two">
|
<div class="account-grid">
|
||||||
<div class="card module">
|
<div class="card module">
|
||||||
<div class="module-head">
|
<div class="module-head">
|
||||||
<h2>Mail</h2>
|
<h2>Mail</h2>
|
||||||
@ -123,6 +123,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="account-stack">
|
||||||
<div class="card module">
|
<div class="card module">
|
||||||
<div class="module-head">
|
<div class="module-head">
|
||||||
<h2>Vaultwarden</h2>
|
<h2>Vaultwarden</h2>
|
||||||
@ -200,6 +201,7 @@
|
|||||||
<div v-if="jellyfin.syncDetail" class="hint mono jellyfin-detail">{{ jellyfin.syncDetail }}</div>
|
<div v-if="jellyfin.syncDetail" class="hint mono jellyfin-detail">{{ jellyfin.syncDetail }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="admin.enabled" class="card module admin">
|
<div v-if="admin.enabled" class="card module admin">
|
||||||
<div class="module-head">
|
<div class="module-head">
|
||||||
@ -580,13 +582,24 @@ h1 {
|
|||||||
max-width: 640px;
|
max-width: 640px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid.two {
|
.account-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.account-stack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-stack .module {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.module {
|
.module {
|
||||||
padding: 18px;
|
padding: 18px;
|
||||||
}
|
}
|
||||||
@ -716,9 +729,13 @@ button.primary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 820px) {
|
@media (max-width: 820px) {
|
||||||
.grid.two {
|
.account-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.account-stack .module {
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin {
|
.admin {
|
||||||
|
|||||||
@ -133,29 +133,14 @@ const sections = [
|
|||||||
description: "Build and ship: source control, CI, registry, and GitOps.",
|
description: "Build and ship: source control, CI, registry, and GitOps.",
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
title: "Source & CI",
|
title: "Dev Stack",
|
||||||
apps: [
|
apps: [
|
||||||
{ name: "Gitea", url: "https://scm.bstein.dev", target: "_blank", description: "Git hosting and collaboration." },
|
{ name: "Gitea", url: "https://scm.bstein.dev", target: "_blank", description: "Git hosting and collaboration." },
|
||||||
{ name: "Jenkins", url: "https://ci.bstein.dev", target: "_blank", description: "CI pipelines and automation." },
|
{ name: "Jenkins", url: "https://ci.bstein.dev", target: "_blank", description: "CI pipelines and automation." },
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Registry & Deploy",
|
|
||||||
apps: [
|
|
||||||
{ name: "Harbor", url: "https://registry.bstein.dev", target: "_blank", description: "Artifact registry." },
|
{ name: "Harbor", url: "https://registry.bstein.dev", target: "_blank", description: "Artifact registry." },
|
||||||
{ name: "GitOps", url: "https://cd.bstein.dev", target: "_blank", description: "GitOps UI for Flux." },
|
{ name: "GitOps", url: "https://cd.bstein.dev", target: "_blank", description: "GitOps UI for Flux." },
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Observability",
|
|
||||||
apps: [
|
|
||||||
{ name: "Grafana", url: "https://metrics.bstein.dev", target: "_blank", description: "Dashboards and monitoring." },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Infra & Secrets",
|
|
||||||
apps: [
|
|
||||||
{ name: "Vault", url: "https://secret.bstein.dev", target: "_blank", description: "Secrets management for infrastructure and apps." },
|
{ name: "Vault", url: "https://secret.bstein.dev", target: "_blank", description: "Secrets management for infrastructure and apps." },
|
||||||
|
{ name: "Grafana", url: "https://metrics.bstein.dev", target: "_blank", description: "Dashboards and monitoring." },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -216,6 +201,7 @@ const sections = [
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 18px;
|
gap: 18px;
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
|
min-height: 92px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.group + .group {
|
.group + .group {
|
||||||
@ -232,6 +218,10 @@ const sections = [
|
|||||||
margin: 6px 0 0;
|
margin: 6px 0 0;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
max-width: 820px;
|
max-width: 820px;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tiles {
|
.tiles {
|
||||||
@ -248,6 +238,7 @@ const sections = [
|
|||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
background: rgba(255, 255, 255, 0.02);
|
background: rgba(255, 255, 255, 0.02);
|
||||||
transition: border-color 160ms ease, transform 160ms ease;
|
transition: border-color 160ms ease, transform 160ms ease;
|
||||||
|
min-height: 112px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile:hover {
|
.tile:hover {
|
||||||
@ -265,6 +256,10 @@ const sections = [
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.eyebrow {
|
.eyebrow {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user