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:
Ronni Baslund
2026-05-30 08:03:07 +02:00
parent a51dc9a732
commit 89691626f4
33 changed files with 1753 additions and 198 deletions
@@ -3,17 +3,54 @@
// preview of how the partner topbar/header will look with the picked
// primary color + display name.
defineProps<{ open: boolean }>()
const emit = defineEmits<{ close: [] }>()
export interface BrandIdentity {
displayName?: string
logoUrl?: string
markUrl?: string
faviconUrl?: string
primaryColor?: string
supportEmail?: string
supportPhone?: string
website?: string
replyTo?: string
}
const name = ref('NordicMSP')
const props = defineProps<{ open: boolean; identity?: BrandIdentity }>()
const emit = defineEmits<{ close: []; save: [payload: BrandIdentity] }>()
const name = ref('')
const color = ref('#3F6BFF')
const supportEmail = ref('support@nordicmsp.dk')
const supportPhone = ref('+45 70 70 12 34')
const website = ref('nordicmsp.dk')
const replyTo = ref('no-reply@nordicmsp.dk')
const supportEmail = ref('')
const supportPhone = ref('')
const website = ref('')
const replyTo = ref('')
// Seed the form from the current identity each time the modal opens.
function seed() {
const i = props.identity ?? {}
name.value = i.displayName ?? ''
color.value = i.primaryColor ?? '#3F6BFF'
supportEmail.value = i.supportEmail ?? ''
supportPhone.value = i.supportPhone ?? ''
website.value = i.website ?? ''
replyTo.value = i.replyTo ?? ''
}
watch(() => props.open, (o) => { if (o) seed() }, { immediate: true })
const SWATCHES = ['#3F6BFF', '#0A2540', '#0066CC', '#5B8C5A', '#D4FF3A']
function save() {
// Emit only `save` — the parent closes the modal after the async save
// succeeds, so a failed save keeps the form open instead of losing edits.
emit('save', {
displayName: name.value,
primaryColor: color.value,
supportEmail: supportEmail.value,
supportPhone: supportPhone.value,
website: website.value,
replyTo: replyTo.value,
})
}
</script>
<template>
@@ -96,7 +133,7 @@ const SWATCHES = ['#3F6BFF', '#0A2540', '#0066CC', '#5B8C5A', '#D4FF3A']
<template #footer>
<UiButton variant="ghost" @click="emit('close')">Cancel</UiButton>
<UiButton variant="primary" @click="emit('close')">
<UiButton variant="primary" @click="save">
<template #leading><UiIcon name="check" :size="14" /></template>
Save identity
</UiButton>