e0ac643e80
- Overview (pages/index.vue): KPIs from real /tenants /partners /users, status meter, recent + needs-follow-up tables. Mock activity stream and incident banner overlay come from data/fixtures.ts. - Operator team: real GET /users filtered to platformAdmin === true, with last-seen + tenant counts. - Users (global): real read with All/Admins/Inactive views and search. - Infrastructure / Feature flags / Audit: mock fixtures only — wiring to real backends (Prometheus, OpenFeature, append-only audit) is tracked as follow-ups in OPERATOR-PLAN.md. - Placeholder pages (support/billing/reports/settings) via OpPlaceholder. - Shared: Stat, MetricCell, OpPlaceholder components, /api/users proxy, PlatformUser type. - .gitignore: scope the docker volumes data/ rule so apps/*/data/ is tracked again (operator carries mock fixtures there).
131 lines
3.9 KiB
Vue
131 lines
3.9 KiB
Vue
<script setup lang="ts">
|
|
import { SERVICES, INCIDENT, type PlatformService } from '~/data/fixtures'
|
|
|
|
const degradedCount = computed(() => SERVICES.filter((s) => s.status !== 'ok').length)
|
|
const incidentActive = computed(() => degradedCount.value > 0)
|
|
|
|
function tone(s: PlatformService): 'ok' | 'warn' | 'bad' {
|
|
return s.status
|
|
}
|
|
function label(s: PlatformService) {
|
|
return s.status === 'ok' ? 'operational' : s.status === 'warn' ? 'degraded' : 'down'
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<PageHeader
|
|
eyebrow="Operations"
|
|
title="Infrastructure"
|
|
subtitle="Health of every service that makes up the Dezky platform."
|
|
>
|
|
<template #actions>
|
|
<UiButton variant="secondary" disabled>
|
|
<template #leading><UiIcon name="chevDown" :size="13" /></template>
|
|
Refresh
|
|
</UiButton>
|
|
<UiButton variant="secondary" disabled>
|
|
<template #leading><UiIcon name="calendar" :size="13" /></template>
|
|
Schedule maintenance
|
|
</UiButton>
|
|
</template>
|
|
</PageHeader>
|
|
|
|
<div class="stage">
|
|
<div v-if="incidentActive" class="incident">
|
|
<span class="pill">
|
|
<span class="dot" />
|
|
{{ INCIDENT.severity }} · ACTIVE
|
|
</span>
|
|
<div class="body">
|
|
<div class="title">{{ INCIDENT.title }}</div>
|
|
<div class="sub">Started {{ INCIDENT.started }} · IC: {{ INCIDENT.ic }}</div>
|
|
</div>
|
|
<UiButton variant="primary" disabled>Open incident</UiButton>
|
|
</div>
|
|
|
|
<div class="grid">
|
|
<Card v-for="s in SERVICES" :key="s.id" :pad="0">
|
|
<div class="head">
|
|
<div>
|
|
<div class="name">{{ s.name }}</div>
|
|
<Mono dim>{{ s.role }}</Mono>
|
|
</div>
|
|
<Badge :tone="tone(s)" dot>{{ label(s) }}</Badge>
|
|
</div>
|
|
<div class="metrics">
|
|
<MetricCell label="uptime · 30d" :value="`${s.uptime.toFixed(2)}%`" />
|
|
<MetricCell label="p95 latency" :value="`${s.p95}ms`" :tone="s.p95 > 300 ? 'warn' : undefined" />
|
|
<MetricCell label="error rate" :value="`${s.err.toFixed(3)}%`" :tone="s.err > 0.04 ? 'warn' : undefined" />
|
|
</div>
|
|
<div class="foot">
|
|
<Mono dim>last incident · {{ s.last }}</Mono>
|
|
<Mono dim>details →</Mono>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<Mono dim class="note">// mock fixtures — wire up to Docker healthchecks + Prometheus in a follow-up</Mono>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.stage { padding: 24px 40px 64px 40px; display: flex; flex-direction: column; gap: 16px; }
|
|
|
|
.incident {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
padding: 14px 18px;
|
|
background: rgba(226, 48, 48, 0.06);
|
|
border: 1px solid rgba(226, 48, 48, 0.3);
|
|
border-left: 3px solid var(--bad);
|
|
border-radius: 8px;
|
|
}
|
|
.pill {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 4px 10px;
|
|
background: var(--bad);
|
|
color: #fff;
|
|
border-radius: 4px;
|
|
font-family: var(--font-mono);
|
|
font-weight: 700;
|
|
font-size: 11px;
|
|
letter-spacing: 0.08em;
|
|
}
|
|
.pill .dot { width: 6px; height: 6px; border-radius: 999px; background: #fff; }
|
|
.body { flex: 1; min-width: 0; }
|
|
.title { font-size: 14px; font-weight: 600; }
|
|
.sub { font-size: 12px; color: var(--text-mute); margin-top: 2px; }
|
|
|
|
.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
|
|
.head {
|
|
padding: 16px 18px 12px 18px;
|
|
border-bottom: 1px solid var(--border);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
gap: 8px;
|
|
}
|
|
.name { font-family: var(--font-display); font-weight: 600; font-size: 17px; }
|
|
|
|
.metrics {
|
|
padding: 12px 18px;
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 8px;
|
|
}
|
|
|
|
.foot {
|
|
padding: 10px 18px;
|
|
border-top: 1px solid var(--border);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.note { display: block; padding: 4px 4px 0 4px; }
|
|
</style>
|