ui: tighten portal layout and status polling

This commit is contained in:
Brad Stein 2026-01-03 20:35:59 -03:00
parent 35f7b77c1b
commit d62ac0fd45
4 changed files with 59 additions and 9 deletions

View File

@ -14,7 +14,7 @@
</template> </template>
<script setup> <script setup>
import { onMounted, ref } from "vue"; import { onMounted, onUnmounted, ref } from "vue";
import TopBar from "./components/TopBar.vue"; import TopBar from "./components/TopBar.vue";
import { fallbackHardware, fallbackServices, fallbackNetwork, fallbackMetrics } from "./data/sample.js"; import { fallbackHardware, fallbackServices, fallbackNetwork, fallbackMetrics } from "./data/sample.js";
@ -27,12 +27,13 @@ const metricsData = ref(fallbackMetrics());
const statusLoading = ref(true); const statusLoading = ref(true);
const statusFetching = ref(false); const statusFetching = ref(false);
const statusError = ref(""); const statusError = ref("");
let pollTimerId = null;
async function refreshLabStatus() { async function refreshLabStatus() {
if (statusFetching.value) return; if (statusFetching.value) return;
statusFetching.value = true; statusFetching.value = true;
const controller = new AbortController(); const controller = new AbortController();
const timeoutId = window.setTimeout(() => controller.abort(), 6000); const timeoutId = window.setTimeout(() => controller.abort(), 10000);
try { try {
const resp = await fetch("/api/lab/status", { const resp = await fetch("/api/lab/status", {
headers: { Accept: "application/json" }, headers: { Accept: "application/json" },
@ -52,13 +53,23 @@ async function refreshLabStatus() {
window.clearTimeout(timeoutId); window.clearTimeout(timeoutId);
statusLoading.value = false; statusLoading.value = false;
statusFetching.value = false; statusFetching.value = false;
scheduleNextPoll();
} }
} }
onMounted(() => { onMounted(() => {
refreshLabStatus(); refreshLabStatus();
window.setInterval(refreshLabStatus, 30000);
}); });
onUnmounted(() => {
if (pollTimerId) window.clearTimeout(pollTimerId);
});
function scheduleNextPoll() {
if (pollTimerId) window.clearTimeout(pollTimerId);
const delayMs = labStatus.value ? 30000 : 8000;
pollTimerId = window.setTimeout(refreshLabStatus, delayMs);
}
</script> </script>
<style scoped> <style scoped>

View File

@ -39,6 +39,8 @@ const svgContent = ref("");
const renderKey = ref(props.cardId || `mermaid-${Math.random().toString(36).slice(2)}`); const renderKey = ref(props.cardId || `mermaid-${Math.random().toString(36).slice(2)}`);
const isOpen = ref(false); const isOpen = ref(false);
let initialized = false; let initialized = false;
let scheduledHandle = null;
let scheduledKind = "";
const renderDiagram = async () => { const renderDiagram = async () => {
if (!props.diagram) return; if (!props.diagram) return;
@ -65,10 +67,38 @@ const renderDiagram = async () => {
} }
}; };
onMounted(renderDiagram); function cancelScheduledRender() {
if (!scheduledHandle) return;
if (scheduledKind === "idle" && window.cancelIdleCallback) {
window.cancelIdleCallback(scheduledHandle);
} else {
window.clearTimeout(scheduledHandle);
}
scheduledHandle = null;
scheduledKind = "";
}
function scheduleRenderDiagram() {
cancelScheduledRender();
if (!props.diagram) return;
const runner = () => {
scheduledHandle = null;
scheduledKind = "";
renderDiagram();
};
if (window.requestIdleCallback) {
scheduledKind = "idle";
scheduledHandle = window.requestIdleCallback(runner, { timeout: 1500 });
} else {
scheduledKind = "timeout";
scheduledHandle = window.setTimeout(runner, 0);
}
}
onMounted(scheduleRenderDiagram);
watch( watch(
() => props.diagram, () => props.diagram,
() => renderDiagram() () => scheduleRenderDiagram()
); );
const onKeyDown = (event) => { const onKeyDown = (event) => {
@ -76,6 +106,7 @@ const onKeyDown = (event) => {
}; };
const open = () => { const open = () => {
if (!svgContent.value) scheduleRenderDiagram();
isOpen.value = true; isOpen.value = true;
}; };
@ -94,6 +125,7 @@ watch(isOpen, (value) => {
}); });
onUnmounted(() => { onUnmounted(() => {
cancelScheduledRender();
document.body.style.overflow = ""; document.body.style.overflow = "";
window.removeEventListener("keydown", onKeyDown); window.removeEventListener("keydown", onKeyDown);
}); });

View File

@ -587,16 +587,18 @@ h1 {
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
gap: 12px; gap: 12px;
margin-top: 12px; margin-top: 12px;
align-items: stretch;
} }
.account-stack { .account-stack {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 12px;
height: 100%;
} }
.account-stack .module { .account-stack .module {
flex: 1; flex: 1 1 0;
min-height: 0; min-height: 0;
} }

View File

@ -181,6 +181,10 @@ const sections = [
.section-grid { .section-grid {
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
} }
.tiles {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
} }
.hero { .hero {
@ -201,7 +205,8 @@ const sections = [
justify-content: space-between; justify-content: space-between;
gap: 18px; gap: 18px;
margin-bottom: 14px; margin-bottom: 14px;
min-height: 92px; height: 92px;
overflow: hidden;
} }
.group + .group { .group + .group {
@ -226,7 +231,7 @@ const sections = [
.tiles { .tiles {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); grid-template-columns: 1fr;
gap: 12px; gap: 12px;
} }
@ -238,7 +243,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; min-height: 120px;
} }
.tile:hover { .tile:hover {