feat: partner enrichment, mutations, settings & branding + operator quick-wins
Backend (platform-api): computed tenant health plus industry/brandColor; partner-scoped tenant update/suspend/resume guarded by assertPartnerOwnsTenant; enriched partner users (MFA + access level) with invite/remove; partner settings and whitelabel branding persistence; Authentik authenticator counting and group removal. Audit on every mutation. Frontend (portal): all five partner pages on real data — dashboard alerts, customers edit/suspend, team MFA/access with invite/remove, editable settings, branding fetch/save. Operator: dashboard and infrastructure service health driven by real liveness probes; fabricated uptime/p95/error-rate removed.
This commit is contained in:
@@ -3,7 +3,6 @@ import type { Tenant } from '~/types/tenant'
|
||||
import type { Partner } from '~/types/partner'
|
||||
import type { PlatformUser } from '~/types/user'
|
||||
import type { AuditEvent } from '~/types/audit'
|
||||
import { SERVICES, INCIDENT } from '~/data/fixtures'
|
||||
|
||||
const { data: tenants, pending: tp, refresh: rT } = await useFetch<Tenant[]>('/api/tenants', { default: () => [] })
|
||||
const { data: partners, pending: pp, refresh: rP } = await useFetch<Partner[]>('/api/partners', { default: () => [] })
|
||||
@@ -13,19 +12,30 @@ const { data: auditEvents, refresh: rA } = await useFetch<AuditEvent[]>('/api/au
|
||||
query: { limit: 8 },
|
||||
})
|
||||
|
||||
// Real service health from platform-api probes (same source as the
|
||||
// infrastructure page) — replaces the old SERVICES/INCIDENT fixtures.
|
||||
interface ProbeResult {
|
||||
id: string
|
||||
name: string
|
||||
status: 'ok' | 'warn' | 'bad'
|
||||
}
|
||||
const { data: probes, refresh: rH } = await useFetch<ProbeResult[]>('/api/health/platform', {
|
||||
default: () => [],
|
||||
})
|
||||
|
||||
function fmtClock(iso: string) {
|
||||
return new Date(iso).toLocaleTimeString('da-DK', { hour: '2-digit', minute: '2-digit' })
|
||||
}
|
||||
|
||||
const { open: openIncident } = useIncidentModal()
|
||||
|
||||
const pending = computed(() => tp.value || pp.value || up.value)
|
||||
|
||||
async function refresh() {
|
||||
await Promise.all([rT(), rP(), rU(), rA()])
|
||||
await Promise.all([rT(), rP(), rU(), rA(), rH()])
|
||||
}
|
||||
|
||||
const degradedCount = computed(() => SERVICES.filter((s) => s.status !== 'ok').length)
|
||||
const totalServices = computed(() => (probes.value ?? []).length)
|
||||
const degradedServices = computed(() => (probes.value ?? []).filter((p) => p.status !== 'ok'))
|
||||
const degradedCount = computed(() => degradedServices.value.length)
|
||||
const incidentActive = computed(() => degradedCount.value > 0)
|
||||
|
||||
const stats = computed(() => ({
|
||||
@@ -74,18 +84,18 @@ function fmtDate(d: string) {
|
||||
</PageHeader>
|
||||
|
||||
<div class="stage">
|
||||
<button v-if="incidentActive" class="incident" type="button" @click="openIncident">
|
||||
<NuxtLink v-if="incidentActive" class="incident" to="/infrastructure">
|
||||
<span class="pill">
|
||||
<span class="dot" />
|
||||
{{ INCIDENT.severity }} · ACTIVE
|
||||
DEGRADED
|
||||
</span>
|
||||
<div class="incident-body">
|
||||
<div class="incident-title">{{ INCIDENT.title }}</div>
|
||||
<div class="incident-sub">Started {{ INCIDENT.started }} · {{ INCIDENT.duration }} duration · {{ INCIDENT.affected }}</div>
|
||||
<div class="incident-title">{{ degradedCount }} service{{ degradedCount === 1 ? '' : 's' }} reporting non-OK status</div>
|
||||
<div class="incident-sub">{{ degradedServices.map((s) => s.name).join(', ') }}</div>
|
||||
</div>
|
||||
<Mono>IC: {{ INCIDENT.ic }}</Mono>
|
||||
<Mono>view infrastructure</Mono>
|
||||
<UiIcon name="chevRight" :size="14" />
|
||||
</button>
|
||||
</NuxtLink>
|
||||
|
||||
<div class="vitals">
|
||||
<NuxtLink to="/tenants" class="vital">
|
||||
@@ -102,7 +112,7 @@ function fmtDate(d: string) {
|
||||
label="Services"
|
||||
:value="incidentActive ? `${degradedCount} degraded` : 'all green'"
|
||||
:delta-tone="incidentActive ? 'down' : 'up'"
|
||||
:hint="incidentActive ? 'P2 · authentik' : `${SERVICES.length} / ${SERVICES.length} healthy`"
|
||||
:hint="`${totalServices - degradedCount} / ${totalServices} healthy`"
|
||||
/>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
@@ -249,6 +259,7 @@ function fmtDate(d: string) {
|
||||
cursor: pointer;
|
||||
color: var(--text);
|
||||
font-family: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
.pill {
|
||||
display: inline-flex;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { INCIDENT, PLANNED_SERVICES } from '~/data/fixtures'
|
||||
import { PLANNED_SERVICES } from '~/data/fixtures'
|
||||
|
||||
// Shape returned by /api/health/platform on platform-api.
|
||||
interface ProbeResult {
|
||||
@@ -72,13 +72,12 @@ function label(p: ProbeResult) {
|
||||
<div v-if="incidentActive" class="incident">
|
||||
<span class="pill">
|
||||
<span class="dot" />
|
||||
{{ INCIDENT.severity }} · ACTIVE
|
||||
DEGRADED
|
||||
</span>
|
||||
<div class="body">
|
||||
<div class="title">{{ INCIDENT.title }}</div>
|
||||
<div class="sub">{{ degradedCount }} service(s) reporting non-ok status · IC: {{ INCIDENT.ic }}</div>
|
||||
<div class="title">{{ degradedCount }} service(s) reporting non-ok status</div>
|
||||
<div class="sub">{{ (probes ?? []).filter((p) => p.status !== 'ok').map((p) => p.name).join(', ') }}</div>
|
||||
</div>
|
||||
<UiButton variant="primary" disabled>Open incident</UiButton>
|
||||
</div>
|
||||
|
||||
<Eyebrow class="section-head">Live · {{ totalCount }} services</Eyebrow>
|
||||
|
||||
Reference in New Issue
Block a user