Files
dezky/apps/operator/pages/infrastructure.vue
T
Ronni Baslund c71e782dc0 feat(operator): command palette, impersonation, incident, tweaks (O.8)
- CommandPalette + useCommandPalette: ⌘K opens a search-and-jump panel over
  real tenants/partners + fixture flags + nav + actions. Arrow keys + Enter
  navigate, Escape/backdrop close. Recents are intentionally omitted for now;
  add when there's something to recent over.
- Impersonation stub: useImpersonation + ImpersonationModal + ImpersonationBanner.
  Modal opens from tenant detail and from the palette. Banner stays at the top
  of the shell until exited. No real OBO token is minted — wiring OAuth Token
  Exchange is tracked as a follow-up.
- IncidentModal + useIncidentModal: opened from the Overview and Infrastructure
  incident banners, renders the mock INCIDENT data with metrics, timeline and
  draft composer.
- TweaksPanel + useTweaks: floating bottom-right panel for theme (dark/light),
  density (comfy/compact), env badge (prod/staging/dev). Saved to localStorage.
- Theme/density apply via [data-theme] + [data-density] overrides in
  tokens.css. Topbar env badge now reads from useTweaks instead of a prop.
- Layout wires ⌘K + ⌘[ at the document level and mounts the palette + modals
  + banner + tweaks panel once for all pages.
2026-05-24 08:34:34 +02:00

132 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)
const { open: openIncident } = useIncidentModal()
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" @click="openIncident">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>