Cleanup superuser dashboard tabs

This commit is contained in:
Jeppe Bundgaard
2026-07-06 14:07:36 +02:00
parent 22f8b5f203
commit b8959cb7bb
4 changed files with 317 additions and 240 deletions
@@ -1,5 +1,5 @@
<script setup>
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { RouterLink } from "vue-router";
import { useI18n } from "vue-i18n";
import {
@@ -16,6 +16,7 @@ import {
const { t, te, locale } = useI18n();
const MAX_GATEWAY_CARDS = 8;
const DEFAULT_DASHBOARD_TAB = "infrastructure";
const gatewayStatusPriority = Object.freeze({
OFFLINE: 0,
DEGRADED: 1,
@@ -77,7 +78,14 @@ const gatewayFleetUsage = ref(createEmptyGatewayFleetUsage());
const gatewaySectionSuppressed = ref(false);
const gatewayDepartments = ref({});
const gatewayDepartmentsLoaded = ref(false);
const activeDashboardTab = ref(DEFAULT_DASHBOARD_TAB);
const showGatewaySection = computed(() => canViewGateways.value && !gatewaySectionSuppressed.value);
const dashboardTabKeys = computed(() => [
DEFAULT_DASHBOARD_TAB,
...(showGatewaySection.value ? ["gateways"] : []),
"modules",
"sessions",
]);
const gatewaySummaryCards = computed(() => [
{
id: "total",
@@ -187,6 +195,16 @@ const isStale = computed(() => {
return nowTick.value - lastLoadedAt.value.getTime() > (refreshAfterSeconds.value * 2000);
});
watch(
() => [activeDashboardTab.value, dashboardTabKeys.value.join("|")],
() => {
if (!dashboardTabKeys.value.includes(activeDashboardTab.value)) {
activeDashboardTab.value = DEFAULT_DASHBOARD_TAB;
}
},
{ immediate: true }
);
const loadStatus = async ({ force = false } = {}) => {
const [snapshotValue] = await Promise.all([
getSuperuserSystemStatus({ force }),
@@ -707,203 +725,250 @@ function modulePath(key) {
</ul>
</div>
<section class="system-status-section">
<div class="section-heading">
<h3>{{ $t("system_status.sections.infrastructure") }}</h3>
<RouterLink class="section-link" to="/superuser/system/replication">
{{ $t("system_status.actions.open_replication") }}
</RouterLink>
</div>
<div class="status-card-grid">
<article
v-for="card in statusCards"
:key="card.id"
class="status-card"
:class="statusClass(card.status)"
:data-testid="`status-card-${card.id}`"
>
<div class="status-card__top">
<p class="status-card__title">{{ card.title }}</p>
<span class="status-pill" :class="statusClass(card.status)">{{ statusLabel(card.status) }}</span>
</div>
<strong class="status-card__primary">{{ card.primary }}</strong>
<p class="status-card__secondary">{{ card.secondary }}</p>
<small class="status-card__detail">{{ card.detail }}</small>
<small
v-if="card.replicationLabel"
class="status-card__detail status-card__replication"
:data-testid="`status-card-${card.id}-replication`"
>
{{ card.replicationLabel }}
</small>
</article>
</div>
</section>
<section
v-if="showGatewaySection"
class="system-status-section"
data-testid="system-status-gateways"
<b-tabs
v-model="activeDashboardTab"
expanded
type="is-boxed"
class="system-status-tabs"
data-testid="system-status-dashboard-tabs"
>
<div class="section-heading">
<div>
<h3>{{ $t("system_status.sections.gateways") }}</h3>
<p class="section-subtitle">{{ $t("system_status.gateways.description") }}</p>
</div>
<RouterLink class="section-link" to="/superuser/selfserve/edge-agents">
{{ $t("system_status.gateways.actions.open_fleet") }}
</RouterLink>
</div>
<b-tab-item value="infrastructure">
<template #header>
<b-icon pack="fas" icon="server" />
<span data-testid="system-status-tab-infrastructure">
{{ $t("system_status.sections.infrastructure") }}
</span>
</template>
<div class="gateway-summary-grid">
<article
v-for="card in gatewaySummaryCards"
:key="card.id"
class="summary-card gateway-summary-card"
:class="card.toneClass"
:data-testid="`gateway-summary-card-${card.id}`"
<section class="system-status-section" data-testid="system-status-panel-infrastructure">
<div class="section-heading">
<h3>{{ $t("system_status.sections.infrastructure") }}</h3>
<RouterLink class="section-link" to="/superuser/system/replication">
{{ $t("system_status.actions.open_replication") }}
</RouterLink>
</div>
<div class="status-card-grid">
<article
v-for="card in statusCards"
:key="card.id"
class="status-card"
:class="statusClass(card.status)"
:data-testid="`status-card-${card.id}`"
>
<div class="status-card__top">
<p class="status-card__title">{{ card.title }}</p>
<span class="status-pill" :class="statusClass(card.status)">{{ statusLabel(card.status) }}</span>
</div>
<strong class="status-card__primary">{{ card.primary }}</strong>
<p class="status-card__secondary">{{ card.secondary }}</p>
<small class="status-card__detail">{{ card.detail }}</small>
<small
v-if="card.replicationLabel"
class="status-card__detail status-card__replication"
:data-testid="`status-card-${card.id}-replication`"
>
{{ card.replicationLabel }}
</small>
</article>
</div>
</section>
</b-tab-item>
<b-tab-item v-if="showGatewaySection" value="gateways">
<template #header>
<b-icon pack="fas" icon="network-wired" />
<span data-testid="system-status-tab-gateways">
{{ $t("system_status.sections.gateways") }}
</span>
</template>
<section
class="system-status-section"
data-testid="system-status-gateways"
>
<p class="summary-label">{{ card.title }}</p>
<strong>{{ card.value }}</strong>
</article>
</div>
<div
v-if="gatewayError"
class="notification is-warning is-light gateway-notification"
data-testid="gateway-section-error"
>
{{ $t("system_status.gateways.error") }}
</div>
<div
v-if="gatewayLoading && !gatewayCards.length"
class="notification is-light gateway-notification"
>
{{ $t("system_status.gateways.loading") }}
</div>
<div v-if="gatewayCards.length" class="gateway-grid">
<article
v-for="gateway in gatewayCards"
:key="gateway.id"
class="gateway-card"
:class="gateway.toneClass"
:data-testid="`gateway-card-${gateway.id}`"
>
<div class="gateway-card__top">
<div class="section-heading">
<div>
<strong class="gateway-card__title">{{ gateway.displayLabel }}</strong>
<p class="gateway-card__subtitle">{{ gateway.departmentName }}</p>
<h3>{{ $t("system_status.sections.gateways") }}</h3>
<p class="section-subtitle">{{ $t("system_status.gateways.description") }}</p>
</div>
<span class="status-pill" :class="gateway.toneClass">{{ gateway.statusLabel }}</span>
<RouterLink class="section-link" to="/superuser/selfserve/edge-agents">
{{ $t("system_status.gateways.actions.open_fleet") }}
</RouterLink>
</div>
<div class="gateway-card__meta">
<span>
{{ $t("system_status.gateways.labels.discovery") }}: {{ gateway.discoveryLabel }}
</span>
<span>
{{ $t("system_status.gateways.labels.last_heartbeat") }}: {{ gateway.lastHeartbeatLabel }}
</span>
<span v-if="gateway.activeOperationLabel">
{{ $t("system_status.gateways.labels.active_operation") }}: {{ gateway.activeOperationLabel }}
</span>
<div class="gateway-summary-grid">
<article
v-for="card in gatewaySummaryCards"
:key="card.id"
class="summary-card gateway-summary-card"
:class="card.toneClass"
:data-testid="`gateway-summary-card-${card.id}`"
>
<p class="summary-label">{{ card.title }}</p>
<strong>{{ card.value }}</strong>
</article>
</div>
<p v-if="gateway.message" class="gateway-card__message">
{{ gateway.message }}
</p>
<RouterLink :to="gateway.link" class="module-card__link">
{{ $t("system_status.gateways.actions.open_gateway") }}
</RouterLink>
</article>
</div>
<div
v-else-if="!gatewayLoading && !gatewayError"
class="gateway-empty"
data-testid="gateway-empty-state"
>
{{ $t("system_status.gateways.empty") }}
</div>
</section>
<section class="system-status-section">
<div class="section-heading">
<h3>{{ $t("system_status.sections.modules") }}</h3>
</div>
<div class="module-grid">
<article
v-for="module in modules"
:key="module.key"
class="module-card"
:class="statusClass(module.status)"
:data-testid="`module-card-${module.key}`"
>
<div class="module-card__top">
<p class="module-card__title">{{ moduleLabel(module.key) }}</p>
<span class="status-pill" :class="statusClass(module.status)">{{ statusLabel(module.status) }}</span>
<div
v-if="gatewayError"
class="notification is-warning is-light gateway-notification"
data-testid="gateway-section-error"
>
{{ $t("system_status.gateways.error") }}
</div>
<p class="module-card__reason" :data-testid="`module-reason-${module.key}`">
{{ moduleReasonText(module) }}
</p>
<div class="module-card__meta">
<span>{{ $t("system_status.labels.enabled") }}: {{ module.enabled ? $t("system_status.status.yes") : $t("system_status.status.no") }}</span>
<span>{{ $t("system_status.labels.configured") }}: {{ module.configured ? $t("system_status.status.yes") : $t("system_status.status.no") }}</span>
<span>{{ $t("system_status.labels.checked_at") }}: {{ formatDate(module.checked_at) }}</span>
<div
v-if="gatewayLoading && !gatewayCards.length"
class="notification is-light gateway-notification"
>
{{ $t("system_status.gateways.loading") }}
</div>
<RouterLink v-if="modulePath(module.key)" :to="modulePath(module.key)" class="module-card__link">
{{ $t("system_status.actions.open_config") }}
</RouterLink>
</article>
</div>
</section>
<section class="system-status-section">
<div class="section-heading">
<h3>{{ $t("system_status.sections.sessions") }}</h3>
<p>{{ $t("system_status.labels.activity_window", { minutes: sessions.active_window_minutes }) }}</p>
</div>
<div v-if="gatewayCards.length" class="gateway-grid">
<article
v-for="gateway in gatewayCards"
:key="gateway.id"
class="gateway-card"
:class="gateway.toneClass"
:data-testid="`gateway-card-${gateway.id}`"
>
<div class="gateway-card__top">
<div>
<strong class="gateway-card__title">{{ gateway.displayLabel }}</strong>
<p class="gateway-card__subtitle">{{ gateway.departmentName }}</p>
</div>
<span class="status-pill" :class="gateway.toneClass">{{ gateway.statusLabel }}</span>
</div>
<div class="table-container sessions-table">
<table class="table is-fullwidth is-hoverable">
<thead>
<tr>
<th>{{ $t("system_status.table.active") }}</th>
<th>{{ $t("system_status.table.name") }}</th>
<th>{{ $t("system_status.table.type") }}</th>
<th>{{ $t("system_status.table.device") }}</th>
<th>{{ $t("system_status.table.route") }}</th>
<th>{{ $t("system_status.table.first_seen") }}</th>
<th>{{ $t("system_status.table.last_seen") }}</th>
</tr>
</thead>
<tbody>
<tr v-for="session in sessions.recent_sessions" :key="`${session.session_kind}-${session.principal_id}-${session.last_seen_at}`">
<td>
<span class="status-pill" :class="session.active ? 'is-ok' : 'is-down'">
{{ session.active ? $t("system_status.status.active") : $t("system_status.status.inactive") }}
<div class="gateway-card__meta">
<span>
{{ $t("system_status.gateways.labels.discovery") }}: {{ gateway.discoveryLabel }}
</span>
</td>
<td>
<strong>{{ sessionDisplayName(session) }}</strong>
<div class="session-context">{{ sessionContextLabel(session) }}</div>
</td>
<td>{{ formatSessionKind(session.session_kind) }}</td>
<td>{{ formatDeviceType(session.device_type) }}</td>
<td class="session-route">{{ session.last_route || "--" }}</td>
<td>{{ formatDate(session.first_seen_at) }}</td>
<td>{{ formatDate(session.last_seen_at) }}</td>
</tr>
<tr v-if="!sessions.recent_sessions?.length">
<td colspan="7">{{ $t("system_status.states.no_sessions") }}</td>
</tr>
</tbody>
</table>
</div>
</section>
<span>
{{ $t("system_status.gateways.labels.last_heartbeat") }}: {{ gateway.lastHeartbeatLabel }}
</span>
<span v-if="gateway.activeOperationLabel">
{{ $t("system_status.gateways.labels.active_operation") }}: {{ gateway.activeOperationLabel }}
</span>
</div>
<p v-if="gateway.message" class="gateway-card__message">
{{ gateway.message }}
</p>
<RouterLink :to="gateway.link" class="module-card__link">
{{ $t("system_status.gateways.actions.open_gateway") }}
</RouterLink>
</article>
</div>
<div
v-else-if="!gatewayLoading && !gatewayError"
class="gateway-empty"
data-testid="gateway-empty-state"
>
{{ $t("system_status.gateways.empty") }}
</div>
</section>
</b-tab-item>
<b-tab-item value="modules">
<template #header>
<b-icon pack="fas" icon="puzzle-piece" />
<span data-testid="system-status-tab-modules">
{{ $t("system_status.sections.modules") }}
</span>
</template>
<section class="system-status-section" data-testid="system-status-panel-modules">
<div class="section-heading">
<h3>{{ $t("system_status.sections.modules") }}</h3>
</div>
<div class="module-grid">
<article
v-for="module in modules"
:key="module.key"
class="module-card"
:class="statusClass(module.status)"
:data-testid="`module-card-${module.key}`"
>
<div class="module-card__top">
<p class="module-card__title">{{ moduleLabel(module.key) }}</p>
<span class="status-pill" :class="statusClass(module.status)">{{ statusLabel(module.status) }}</span>
</div>
<p class="module-card__reason" :data-testid="`module-reason-${module.key}`">
{{ moduleReasonText(module) }}
</p>
<div class="module-card__meta">
<span>{{ $t("system_status.labels.enabled") }}: {{ module.enabled ? $t("system_status.status.yes") : $t("system_status.status.no") }}</span>
<span>{{ $t("system_status.labels.configured") }}: {{ module.configured ? $t("system_status.status.yes") : $t("system_status.status.no") }}</span>
<span>{{ $t("system_status.labels.checked_at") }}: {{ formatDate(module.checked_at) }}</span>
</div>
<RouterLink v-if="modulePath(module.key)" :to="modulePath(module.key)" class="module-card__link">
{{ $t("system_status.actions.open_config") }}
</RouterLink>
</article>
</div>
</section>
</b-tab-item>
<b-tab-item value="sessions">
<template #header>
<b-icon pack="fas" icon="users" />
<span data-testid="system-status-tab-sessions">
{{ $t("system_status.sections.sessions") }}
</span>
</template>
<section class="system-status-section" data-testid="system-status-panel-sessions">
<div class="section-heading">
<h3>{{ $t("system_status.sections.sessions") }}</h3>
<p>{{ $t("system_status.labels.activity_window", { minutes: sessions.active_window_minutes }) }}</p>
</div>
<div class="table-container sessions-table">
<table class="table is-fullwidth is-hoverable">
<thead>
<tr>
<th>{{ $t("system_status.table.active") }}</th>
<th>{{ $t("system_status.table.name") }}</th>
<th>{{ $t("system_status.table.type") }}</th>
<th>{{ $t("system_status.table.device") }}</th>
<th>{{ $t("system_status.table.route") }}</th>
<th>{{ $t("system_status.table.first_seen") }}</th>
<th>{{ $t("system_status.table.last_seen") }}</th>
</tr>
</thead>
<tbody>
<tr v-for="session in sessions.recent_sessions" :key="`${session.session_kind}-${session.principal_id}-${session.last_seen_at}`">
<td>
<span class="status-pill" :class="session.active ? 'is-ok' : 'is-down'">
{{ session.active ? $t("system_status.status.active") : $t("system_status.status.inactive") }}
</span>
</td>
<td>
<strong>{{ sessionDisplayName(session) }}</strong>
<div class="session-context">{{ sessionContextLabel(session) }}</div>
</td>
<td>{{ formatSessionKind(session.session_kind) }}</td>
<td>{{ formatDeviceType(session.device_type) }}</td>
<td class="session-route">{{ session.last_route || "--" }}</td>
<td>{{ formatDate(session.first_seen_at) }}</td>
<td>{{ formatDate(session.last_seen_at) }}</td>
</tr>
<tr v-if="!sessions.recent_sessions?.length">
<td colspan="7">{{ $t("system_status.states.no_sessions") }}</td>
</tr>
</tbody>
</table>
</div>
</section>
</b-tab-item>
</b-tabs>
</div>
</template>
@@ -924,10 +989,10 @@ function modulePath(key) {
.module-card,
.gateway-card {
border: 1px solid #d7dde7;
border-radius: 18px;
border-radius: 8px;
padding: 1rem 1.1rem;
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
box-shadow: 0 18px 45px rgba(15, 23, 42, 0.06);
background: #ffffff;
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.05);
}
.summary-card--overall {
@@ -991,6 +1056,23 @@ function modulePath(key) {
color: #0f4c81;
}
.system-status-tabs {
min-width: 0;
}
.system-status-tabs :deep(.tab-content) {
padding: 1rem 0 0;
}
.system-status-tabs :deep(.tabs ul) {
align-items: stretch;
}
.system-status-tabs :deep(.tabs a) {
min-height: 2.75rem;
gap: 0.35rem;
}
.status-card__top,
.module-card__top {
display: flex;
@@ -1107,7 +1189,7 @@ function modulePath(key) {
.gateway-empty {
border: 1px dashed #cbd5e1;
border-radius: 18px;
border-radius: 8px;
padding: 1rem 1.1rem;
color: #475569;
background: rgba(248, 250, 252, 0.8);
@@ -1,61 +1,12 @@
<script setup>
import { useRouter} from "vue-router";
import { ref } from 'vue';
import { showAuthSignOutForm } from '@/components/forms/auth/authSignOutForm.vue';
import UserNavigation from "@/components/global/UserNavigation.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import NavigationMenuSuperUser from "@/components/models/navigation/menus/NavigationMenuSuperUser.vue";
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const router = useRouter();
const tabs = [
{ name: t('superuser_dashboard.nav.dashboard'), path: '/superuser' },
{ name: t('superuser_dashboard.nav.statistics'), path: '/superuser/statistics' },
{ name: t('common.departments'), path: '/superuser/departments' },
{ name: t('superuser_dashboard.nav.employees'), path: '/superuser/users' },
{ name: t('superuser_dashboard.nav.customers'), path: '/superuser/customers' },
{ name: t('superuser_dashboard.nav.complaints'), path: '/superuser/complaints' },
{ name: t('common.products'), path: '/superuser/products' },
{ name: t('common.invoices'), path: '/superuser/invoices' },
{ name: t('superuser_dashboard.nav.transaction_history'), path: '/superuser/orders' },
{ name: t('superuser_dashboard.nav.scanners'), path: '/superuser/scanners' },
{ name: t('superuser_dashboard.nav.roles'), path: '/superuser/roles' },
{ name: t('common.vehicles'), path: '/superuser/vehicles' },
{ name: SessionUser.objects.categories.meta.title, path: '/superuser/categories' },
{ name: t('superuser_dashboard.nav.xlvask'), path: '/superuser/xlvask' },
{ name: t('superuser_dashboard.nav.configuration'), path: '/superuser/configuration' }
];
// Get the current path
const currentPath = ref(router.currentRoute.value.path);
// Get the index of the active tab
const activeTab = tabs.findIndex(tab => tab.path === currentPath.value);
// Change the tab
const changeTab = (index) => {
router.push(tabs[index].path);
};
</script>
<template>
<div>
<UserNavigation />
<NavigationMenuSuperUser />
<!--
<div class="tabs is-right">
<ul>
<li v-for="(tab, index) in tabs" :key="index" :class="{'is-active': activeTab === index}" @click="changeTab(index)">
<a>{{ tab.name }}</a>
</li>
Sign out button
<li>
<a @click="showAuthSignOutForm">Sign out</a>
</li>
</ul>
</div> -->
</div>
</template>
@@ -784,14 +784,17 @@ test.describe("Superuser system status smoke", () => {
await expect(page).toHaveURL(/\/superuser$/);
await expect(page.getByTestId("system-status-dashboard")).toBeVisible({ timeout: PAGE_READY_TIMEOUT });
await expect(page.getByTestId("system-status-dashboard-tabs")).toBeVisible({ timeout: PAGE_READY_TIMEOUT });
await expect(page.getByTestId("status-card-database")).toContainText("truckwash", { timeout: PAGE_READY_TIMEOUT });
await expect(page.getByTestId("status-card-database-replication")).toContainText("100.0% replikering");
await expect(page.getByTestId("status-card-minio-replication")).toContainText("67.4% replikering");
await page.getByTestId("system-status-tab-sessions").click();
await expect(page.locator("body")).toContainText("Acme Logistics");
await expect(page.locator("body")).toContainText("Kunde");
await expect(page.locator("body")).toContainText("Bruger");
await expect(page.locator("body")).toContainText("Computer");
await expect(page.locator("body")).toContainText("/superuser/vehicles");
await page.getByTestId("system-status-tab-modules").click();
await expect(page.locator("body")).toContainText("Forbindelse til OpenAI API bekræftet.");
await expect(page.locator("body")).toContainText("Redis er utilgængelig; cache til modulprober omgås.");
await expect(page.locator("body")).not.toContainText("OpenAI API connectivity confirmed.");
@@ -975,6 +978,7 @@ test.describe("Superuser system status smoke", () => {
await installSystemStatusMock(page, longReasonSnapshot);
await page.goto("/superuser");
await page.getByTestId("system-status-tab-modules").click();
const card = page.getByTestId("module-card-shelly");
const title = card.locator(".module-card__title");
@@ -1004,6 +1008,7 @@ test.describe("Superuser system status smoke", () => {
await installSystemStatusMock(page, longTitleSnapshot);
await page.goto("/superuser");
await page.getByTestId("system-status-tab-modules").click();
const card = page.getByTestId("module-card-licenseplaterecognizer");
const title = card.locator(".module-card__title");
@@ -1133,6 +1138,7 @@ test.describe("Superuser system status smoke", () => {
});
await page.goto("/superuser");
await page.getByTestId("system-status-tab-gateways").click();
await expect(page.getByTestId("system-status-gateways")).toBeVisible({ timeout: PAGE_READY_TIMEOUT });
await expect(page.getByTestId("gateway-summary-card-total")).toContainText("10");
@@ -226,6 +226,11 @@ const flushDashboardLoad = async () => {
}
};
const openDashboardTab = async (wrapper, tab) => {
await wrapper.get(`[data-testid="system-status-tab-${tab}"]`).trigger("click");
await flushRendering();
};
const resetStatusStore = () => {
SuperUserSystemStatusObject.snapshot.value = null;
SuperUserSystemStatusObject.loading.value = false;
@@ -277,7 +282,7 @@ describe("superuser system status dashboard", () => {
resetSessionUser();
});
it("renders infrastructure, modules, warnings, and recent sessions from the shared snapshot", async () => {
it("renders infrastructure, modules, warnings, and recent sessions from the shared snapshot tabs", async () => {
installDashboardMocks();
const wrapper = mountWithApp(SystemStatusDashboard, {
@@ -291,10 +296,16 @@ describe("superuser system status dashboard", () => {
expect(wrapper.get('[data-testid="status-card-database"]').text()).toContain("truckwash");
expect(wrapper.get('[data-testid="status-card-database-replication"]').text()).toContain("100.0% replication");
expect(wrapper.get('a[href="/superuser/system/replication"]').exists()).toBe(true);
expect(wrapper.get('[data-testid="system-status-dashboard-tabs"]').exists()).toBe(true);
expect(wrapper.get('[data-testid="system-status-panel-infrastructure"]').isVisible()).toBe(true);
expect(wrapper.text()).toContain("Redis is unavailable; module probe caching is bypassed.");
await openDashboardTab(wrapper, "modules");
expect(wrapper.get('a[href="/superuser/configuration/openai"]').exists()).toBe(true);
await openDashboardTab(wrapper, "sessions");
expect(wrapper.text()).toContain("Acme Logistics");
expect(wrapper.text()).toContain("/superuser/vehicles");
expect(wrapper.text()).toContain("Redis is unavailable; module probe caching is bypassed.");
expect(wrapper.get('a[href="/superuser/configuration/openai"]').exists()).toBe(true);
wrapper.unmount();
});
@@ -333,6 +344,30 @@ describe("superuser system status dashboard", () => {
expect(authenticatedRequestMock).toHaveBeenCalledWith("/superuser/system/status", "GET", {});
expect(authenticatedRequestMock.mock.calls.some(([path]) => path === "/edge-gateways")).toBe(false);
expect(wrapper.find('[data-testid="system-status-gateways"]').exists()).toBe(false);
expect(wrapper.find('[data-testid="system-status-tab-gateways"]').exists()).toBe(false);
wrapper.unmount();
});
it("returns to infrastructure when the active gateway tab becomes unavailable", async () => {
installDashboardMocks({
gateways: [createGateway(201, { label: "Gateway Atlas" })],
});
const wrapper = mountWithApp(SystemStatusDashboard, {
messages: { en: enMessages },
});
await flushDashboardLoad();
await openDashboardTab(wrapper, "gateways");
expect(wrapper.get('[data-testid="system-status-gateways"]').isVisible()).toBe(true);
SessionUser.permissions.value = ["user"];
await flushRendering();
expect(wrapper.find('[data-testid="system-status-tab-gateways"]').exists()).toBe(false);
expect(wrapper.get('[data-testid="system-status-panel-infrastructure"]').isVisible()).toBe(true);
wrapper.unmount();
});
@@ -425,6 +460,7 @@ describe("superuser system status dashboard", () => {
});
await flushDashboardLoad();
await openDashboardTab(wrapper, "gateways");
expect(wrapper.get('[data-testid="gateway-summary-card-total"]').text()).toContain("10");
expect(wrapper.get('[data-testid="gateway-summary-card-online"]').text()).toContain("4");
@@ -472,6 +508,7 @@ describe("superuser system status dashboard", () => {
expect(authenticatedRequestMock.mock.calls.some(([path]) => path === "/edge-gateways")).toBe(true);
expect(wrapper.find('[data-testid="system-status-gateways"]').exists()).toBe(false);
expect(wrapper.find('[data-testid="system-status-tab-gateways"]').exists()).toBe(false);
expect(wrapper.get('[data-testid="status-card-database"]').text()).toContain("truckwash");
wrapper.unmount();
@@ -487,6 +524,7 @@ describe("superuser system status dashboard", () => {
});
await flushDashboardLoad();
await openDashboardTab(wrapper, "gateways");
expect(wrapper.get('[data-testid="system-status-gateways"]').exists()).toBe(true);
expect(wrapper.get('[data-testid="gateway-section-error"]').text()).toContain(