Menu

Change language

GitHub repo

Service Health Matrix

blockdevops

Statuspage and BetterStack style global region latency & infrastructure health matrix with overall operational status banner, 6-region latency grid with sparklines, 90-day core service uptime bars, and incident history timeline.

Installation

$npx uipkge-ng@latest add service-health-matrix
Manifest:https://uipkge.dev/r/angular/service-health-matrix.jsonInstalls to:src/app/components/blocks/service-health-matrix/

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

RegionHealth
interface RegionHealth {
  id: string
  name: string
  location: string
  code: string
  latency: number
  status: RegionStatus
  uptime: number
  p95Latency?: number
  packetLoss?: number
  latencyHistory?: number[]
}
ServiceHealthItem
interface ServiceHealthItem {
  id: string
  name: string
  description?: string
  icon?: 'auth' | 'cdn' | 'database' | 'webhook' | 'ai'
  uptime: number
  status: RegionStatus
  days?: DayStatus[]
}
IncidentUpdate
interface IncidentUpdate {
  time: string
  status: string
  description: string
}
IncidentRecord
interface IncidentRecord {
  id: string
  title: string
  date: string
  severity: IncidentSeverity
  resolved: boolean
  duration: string
  updates: IncidentUpdate[]
}
BannerConfig
interface BannerConfig {
  title: string
  badge: string
  badgeVariant: 'success' | 'warning' | 'destructive' | 'info'
  containerClass: string
  dotClass: string
  textClass: string
}
Sparkline
interface Sparkline {
  path: string
  area: string
  lastPoint: { x: number; y: number }
}

npm dependencies

Files installed (2)

  • src/app/components/blocks/service-health-matrix/service-health-matrix.component.ts32.8 kB
    // Statuspage / BetterStack style global region latency & infrastructure health matrix:
    // overall operational status banner, 6-region latency grid with SVG sparklines, 90-day
    // core service uptime bars, and an incident history timeline. Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, DestroyRef, Input, computed, inject, signal } from '@angular/core'
    import {
      Activity,
      AlertTriangle,
      Bell,
      Check,
      CheckCircle2,
      Clock,
      Cpu,
      Database,
      Globe,
      LucideAngularModule,
      RefreshCw,
      Server,
      ShieldCheck,
      Zap,
    } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import { UiBadgeComponent, type BadgeVariant } from '@/ui/badge/badge.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import {
      UiCardComponent,
      UiCardContentComponent,
      UiCardDescriptionComponent,
      UiCardHeaderComponent,
      UiCardTitleComponent,
    } from '@/ui/card/card.component'
    
    export type RegionStatus = 'operational' | 'degraded' | 'outage' | 'maintenance'
    export type DayStatus = 'up' | 'degraded' | 'down' | 'maintenance'
    export type IncidentSeverity = 'minor' | 'major' | 'critical' | 'maintenance'
    
    export interface RegionHealth {
      id: string
      name: string
      location: string
      code: string
      latency: number
      status: RegionStatus
      uptime: number
      p95Latency?: number
      packetLoss?: number
      latencyHistory?: number[]
    }
    
    export interface ServiceHealthItem {
      id: string
      name: string
      description?: string
      icon?: 'auth' | 'cdn' | 'database' | 'webhook' | 'ai'
      uptime: number
      status: RegionStatus
      days?: DayStatus[]
    }
    
    export interface IncidentUpdate {
      time: string
      status: string
      description: string
    }
    
    export interface IncidentRecord {
      id: string
      title: string
      date: string
      severity: IncidentSeverity
      resolved: boolean
      duration: string
      updates: IncidentUpdate[]
    }
    
    function generate90Days(blips: Array<{ dayAgo: number; status: DayStatus }> = []): DayStatus[] {
      const days: DayStatus[] = Array.from({ length: 90 }, () => 'up')
      for (const blip of blips) {
        const idx = 89 - blip.dayAgo
        if (idx >= 0 && idx < 90) {
          days[idx] = blip.status
        }
      }
      return days
    }
    
    const defaultRegions: RegionHealth[] = [
      {
        id: 'us-east',
        name: 'US East',
        location: 'N. Virginia',
        code: 'us-east-1',
        latency: 24,
        status: 'operational',
        uptime: 100,
        p95Latency: 28,
        packetLoss: 0.0,
        latencyHistory: [26, 25, 24, 25, 23, 24, 25, 24, 23, 24, 25, 24],
      },
      {
        id: 'us-west',
        name: 'US West',
        location: 'Oregon',
        code: 'us-west-2',
        latency: 38,
        status: 'operational',
        uptime: 99.98,
        p95Latency: 42,
        packetLoss: 0.0,
        latencyHistory: [40, 39, 38, 37, 38, 39, 38, 38, 37, 38, 39, 38],
      },
      {
        id: 'eu-central',
        name: 'EU Central',
        location: 'Frankfurt',
        code: 'eu-central-1',
        latency: 18,
        status: 'operational',
        uptime: 100,
        p95Latency: 21,
        packetLoss: 0.0,
        latencyHistory: [19, 18, 18, 17, 18, 19, 18, 18, 17, 18, 18, 18],
      },
      {
        id: 'ap-south',
        name: 'AP South',
        location: 'Mumbai',
        code: 'ap-south-1',
        latency: 45,
        status: 'operational',
        uptime: 99.95,
        p95Latency: 52,
        packetLoss: 0.01,
        latencyHistory: [47, 46, 45, 44, 45, 48, 46, 45, 44, 45, 46, 45],
      },
      {
        id: 'ap-east',
        name: 'AP East',
        location: 'Tokyo',
        code: 'ap-northeast-1',
        latency: 32,
        status: 'operational',
        uptime: 99.99,
        p95Latency: 36,
        packetLoss: 0.0,
        latencyHistory: [34, 33, 32, 31, 32, 33, 32, 32, 31, 32, 33, 32],
      },
      {
        id: 'sa-east',
        name: 'SA East',
        location: 'São Paulo',
        code: 'sa-east-1',
        latency: 58,
        status: 'operational',
        uptime: 99.91,
        p95Latency: 65,
        packetLoss: 0.02,
        latencyHistory: [62, 60, 59, 58, 61, 58, 59, 58, 57, 58, 60, 58],
      },
    ]
    
    const defaultServices: ServiceHealthItem[] = [
      {
        id: 'auth-api',
        name: 'Authentication API',
        description: 'OAuth2 / SAML SSO · Session Tokens · JWT Validation',
        icon: 'auth',
        uptime: 99.99,
        status: 'operational',
        days: generate90Days(),
      },
      {
        id: 'edge-cdn',
        name: 'Edge CDN & DNS',
        description: 'Anycast Global Routing · Edge Cache · SSL Termination',
        icon: 'cdn',
        uptime: 100.0,
        status: 'operational',
        days: generate90Days(),
      },
      {
        id: 'postgres-cluster',
        name: 'Postgres Database Cluster',
        description: 'Primary Write Node · Regional Read Replicas · Pooler',
        icon: 'database',
        uptime: 99.97,
        status: 'operational',
        days: generate90Days([{ dayAgo: 42, status: 'degraded' }]),
      },
      {
        id: 'webhook-dispatcher',
        name: 'Webhook Dispatcher',
        description: 'Event Streaming Engine · Exponential Backoff Retries',
        icon: 'webhook',
        uptime: 99.98,
        status: 'operational',
        days: generate90Days([{ dayAgo: 1, status: 'degraded' }]),
      },
      {
        id: 'ai-gateway',
        name: 'AI Inference Gateway',
        description: 'Model Routing · Streaming Token Buffers · Semantic Cache',
        icon: 'ai',
        uptime: 99.95,
        status: 'operational',
        days: generate90Days([{ dayAgo: 18, status: 'degraded' }]),
      },
    ]
    
    const defaultIncidents: IncidentRecord[] = [
      {
        id: 'inc-1',
        title: 'Elevated latency on Webhook Dispatcher',
        date: 'Yesterday · Aug 20, 2026',
        severity: 'minor',
        resolved: true,
        duration: '18 minutes',
        updates: [
          {
            time: '14:40 UTC',
            status: 'Resolved',
            description:
              'The backlog of outgoing webhook payloads has fully drained. All queued webhook events were delivered with zero packet drop. Worker autoscaling thresholds have been adjusted.',
          },
          {
            time: '14:22 UTC',
            status: 'Investigating',
            description:
              'We identified a localized queue lock in the dispatch workers following a Redis connection pool saturation. Workers were scaled up and connection pool limits raised.',
          },
        ],
      },
      {
        id: 'inc-2',
        title: 'Scheduled Maintenance: Postgres Cluster Engine Upgrade',
        date: 'Aug 14, 2026',
        severity: 'maintenance',
        resolved: true,
        duration: '12 minutes',
        updates: [
          {
            time: '02:12 UTC',
            status: 'Completed',
            description:
              'Primary instance switchover completed smoothly. Read replicas synchronized without data lag. All database endpoints are fully operational.',
          },
          {
            time: '02:00 UTC',
            status: 'In Progress',
            description:
              'Scheduled rolling restart of secondary read replicas initiated while applying minor database engine updates.',
          },
        ],
      },
    ]
    
    interface BannerConfig {
      title: string
      badge: string
      badgeVariant: 'success' | 'warning' | 'destructive' | 'info'
      containerClass: string
      dotClass: string
      textClass: string
    }
    
    const statusBannerConfig: Record<RegionStatus, BannerConfig> = {
      operational: {
        title: 'All Systems Operational',
        badge: '99.99% 90-day uptime',
        badgeVariant: 'success',
        containerClass: 'border-success/30 bg-success/10 text-success',
        dotClass: 'bg-success',
        textClass: 'text-success',
      },
      degraded: {
        title: 'Active Service Degradation',
        badge: 'Elevated Latency Detected',
        badgeVariant: 'warning',
        containerClass: 'border-warning/30 bg-warning/10 text-warning',
        dotClass: 'bg-warning',
        textClass: 'text-warning',
      },
      outage: {
        title: 'Partial System Outage',
        badge: 'Incident Under Investigation',
        badgeVariant: 'destructive',
        containerClass: 'border-destructive/30 bg-destructive/10 text-destructive',
        dotClass: 'bg-destructive',
        textClass: 'text-destructive',
      },
      maintenance: {
        title: 'Scheduled Maintenance in Progress',
        badge: 'Planned Upgrades',
        badgeVariant: 'info',
        containerClass: 'border-info/30 bg-info/10 text-info',
        dotClass: 'bg-info',
        textClass: 'text-info',
      },
    }
    
    const dayClassMap: Record<DayStatus, string> = {
      up: 'bg-success/80 hover:bg-success bg-success/70 dark:hover:bg-success',
      degraded: 'bg-warning/90 hover:bg-warning',
      down: 'bg-destructive hover:bg-destructive',
      maintenance: 'bg-info hover:bg-info',
    }
    
    const dayLabelMap: Record<DayStatus, string> = {
      up: 'Operational (100%)',
      degraded: 'Degraded Performance',
      down: 'Major Outage',
      maintenance: 'Scheduled Maintenance',
    }
    
    const severityBadgeMap: Record<IncidentSeverity, { variant: BadgeVariant; label: string }> = {
      minor: { variant: 'warning', label: 'Minor' },
      major: { variant: 'destructive', label: 'Major' },
      critical: { variant: 'destructive', label: 'Critical' },
      maintenance: { variant: 'secondary', label: 'Maintenance' },
    }
    
    interface Sparkline {
      path: string
      area: string
      lastPoint: { x: number; y: number }
    }
    
    function buildSparkline(history: number[] = [24, 24, 24, 24]): Sparkline {
      const min = Math.min(...history)
      const max = Math.max(...history)
      const range = max - min || 1
      const height = 26
      const padding = 3
      const points = history.map((val, idx) => {
        const x = (idx / (history.length - 1)) * 100
        const y = height - ((val - min) / range) * (height - padding * 2) - padding
        return { x, y }
      })
      let path = `M ${points[0].x},${points[0].y}`
      for (let i = 1; i < points.length; i++) {
        const prev = points[i - 1]
        const curr = points[i]
        const cx = (prev.x + curr.x) / 2
        path += ` C ${cx},${prev.y} ${cx},${curr.y} ${curr.x},${curr.y}`
      }
      const lastPoint = points[points.length - 1]
      const area = `${path} L 100,32 L 0,32 Z`
      return { path, area, lastPoint }
    }
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-service-health-matrix, [ui-service-health-matrix]',
      standalone: true,
      // React renders the root <div> itself: the host stays out of layout and `class` goes to the root.
      host: { '[attr.class]': '"contents"' },
      imports: [
        LucideAngularModule,
        UiBadgeComponent,
        UiButtonComponent,
        UiCardComponent,
        UiCardContentComponent,
        UiCardDescriptionComponent,
        UiCardHeaderComponent,
        UiCardTitleComponent,
      ],
      template: `
        <div data-slot="service-health-matrix" [class]="rootClass">
          <!-- Header Banner -->
          <div
            role="status"
            [class]="
              cn(
                'flex flex-col gap-4 rounded-xl border p-4 shadow-xs transition-colors sm:p-5 md:flex-row md:items-center md:justify-between',
                currentBanner().containerClass
              )
            "
          >
            <div class="flex items-start gap-3.5 sm:items-center">
              <span class="relative mt-1 flex size-3 shrink-0 sm:mt-0">
                <span
                  [class]="cn('absolute inline-flex h-full w-full rounded-full opacity-75', currentBanner().dotClass)"
                ></span>
                <span [class]="cn('relative inline-flex size-3 rounded-full', currentBanner().dotClass)"></span>
              </span>
              <div class="space-y-1">
                <div class="flex flex-wrap items-center gap-2">
                  <h1 class="text-foreground text-base leading-none font-semibold tracking-tight sm:text-lg">
                    {{ currentBanner().title }}
                  </h1>
                  <span [class]="cn('flex items-center gap-2 text-xs font-medium sm:text-sm', currentBanner().textClass)">
                    {{ systemUptime() }} 90-day uptime
                  </span>
                </div>
                <p class="text-muted-foreground text-xs">
                  Continuous synthetic telemetry and edge probe latency across all global endpoints.
                </p>
              </div>
            </div>
    
            <div class="flex flex-wrap items-center gap-2 self-end sm:gap-3 md:self-center">
              <div class="text-muted-foreground flex items-center gap-1.5 text-xs">
                <lucide-icon [img]="Clock" class="size-3.5" />
                <span>{{ currentUpdatedText() }}</span>
                <button
                  ui-button
                  variant="ghost"
                  size="icon-sm"
                  class="size-7 rounded-md"
                  aria-label="Refresh status telemetry"
                  (click)="handleRefresh()"
                >
                  <lucide-icon [img]="RefreshCw" [class]="cn('size-3.5', isRefreshing() && 'animate-spin')" />
                </button>
              </div>
    
              <div class="relative">
                @if (!isSubscribed()) {
                  <button
                    ui-button
                    variant="outline"
                    size="sm"
                    class="h-8 gap-1.5 text-xs font-medium shadow-xs"
                    (click)="showSubscribeInput.set(!showSubscribeInput())"
                  >
                    <lucide-icon [img]="Bell" class="size-3.5" />
                    <span>Subscribe to Updates</span>
                  </button>
                } @else {
                  <span ui-badge variant="success" class="h-8 gap-1.5 px-3 text-xs font-medium">
                    <lucide-icon [img]="Check" class="size-3.5" />
                    Subscribed
                  </span>
                }
              </div>
            </div>
          </div>
    
          <!-- Collapsible Subscribe Input Form -->
          @if (showSubscribeInput() && !isSubscribed()) {
            <div
              class="border-border bg-card text-card-foreground flex flex-col items-stretch gap-2 rounded-lg border p-3 shadow-xs sm:flex-row sm:items-center"
            >
              <div class="flex-1">
                <p class="text-foreground text-xs font-medium">Get instant outage alerts via Email</p>
                <p class="text-muted-foreground text-xs">
                  We will only notify you for major severity incidents and maintenance.
                </p>
              </div>
              <div class="flex items-center gap-2">
                <input
                  type="email"
                  [value]="emailInput()"
                  (input)="emailInput.set($any($event.target).value)"
                  placeholder="admin@company.com"
                  class="border-border bg-background placeholder:text-muted-foreground focus-visible:ring-ring h-8 w-full rounded-md border px-2.5 text-xs focus-visible:ring-2 focus-visible:outline-none sm:w-64"
                  (keydown.enter)="handleSubscribeSubmit()"
                />
                <button ui-button size="sm" class="h-8 shrink-0 px-3 text-xs" (click)="handleSubscribeSubmit()">
                  Subscribe
                </button>
              </div>
            </div>
          }
    
          <!-- Section 1: Global Region Latency Grid -->
          <div class="space-y-3">
            <div class="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
              <div>
                <h2 class="text-foreground text-sm font-semibold tracking-tight">Global Region Latency Grid</h2>
                <p class="text-muted-foreground text-xs">
                  Real-time edge response round-trip time and packet loss metrics.
                </p>
              </div>
              <div class="text-muted-foreground flex items-center gap-2 text-xs">
                <span class="bg-success inline-flex size-2 rounded-full"></span>
                <span>All 6 Edge Zones Live</span>
              </div>
            </div>
    
            <div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
              @for (region of regions(); track region.id) {
                <div
                  ui-card
                  class="border-border/80 bg-card text-card-foreground hover:border-border shadow-xs transition-colors"
                >
                  <div ui-card-header class="p-4 pb-2">
                    <div class="flex items-start justify-between gap-2">
                      <div class="flex items-center gap-2.5">
                        <div
                          class="bg-muted/70 text-muted-foreground border-border/50 flex size-8 shrink-0 items-center justify-center rounded-md border"
                        >
                          <lucide-icon [img]="Globe" class="size-4" />
                        </div>
                        <div>
                          <h3 ui-card-title class="text-sm leading-snug font-semibold">{{ region.name }}</h3>
                          <p ui-card-description class="text-xs">
                            {{ region.location }} · <span class="text-muted-foreground font-mono">{{ region.code }}</span>
                          </p>
                        </div>
                      </div>
    
                      <span
                        ui-badge
                        [variant]="regionBadgeVariant(region.status)"
                        class="shrink-0 text-xs font-medium capitalize"
                      >
                        <span [class]="cn('mr-1 size-1.5 rounded-full', regionDotClass(region.status))"></span>
                        {{ region.status }}
                      </span>
                    </div>
                  </div>
    
                  <div ui-card-content class="space-y-3 p-4 pt-1">
                    <div class="flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5 pt-1">
                      <div class="flex items-baseline gap-1.5">
                        <span class="text-foreground font-mono text-2xl font-bold tracking-tight"
                          >{{ region.latency }}ms</span
                        >
                        <span class="text-muted-foreground text-xs">ping</span>
                      </div>
                      <div [class]="cn('flex items-center gap-1 text-xs font-medium', latencyToneClass(region.latency))">
                        <lucide-icon [img]="Activity" class="size-3" />
                        <span>{{ latencyLabel(region.latency) }}</span>
                      </div>
                    </div>
    
                    <div class="h-9 w-full overflow-hidden pt-1" [attr.aria-label]="region.name + ' latency sparkline'">
                      <svg class="h-full w-full overflow-visible" viewBox="0 0 100 32" preserveAspectRatio="none">
                        <defs>
                          <linearGradient [id]="'grad-ng-' + region.id" x1="0" y1="0" x2="0" y2="1">
                            <stop offset="0%" [attr.stop-color]="sparkColor(region.status)" stop-opacity="0.25" />
                            <stop offset="100%" [attr.stop-color]="sparkColor(region.status)" stop-opacity="0.0" />
                          </linearGradient>
                        </defs>
                        <path [attr.d]="spark(region).area" [attr.fill]="'url(#grad-ng-' + region.id + ')'" />
                        <path
                          [attr.d]="spark(region).path"
                          fill="none"
                          [attr.stroke]="sparkColor(region.status)"
                          stroke-width="2"
                          stroke-linecap="round"
                          stroke-linejoin="round"
                        />
                        <circle
                          [attr.cx]="spark(region).lastPoint.x"
                          [attr.cy]="spark(region).lastPoint.y"
                          r="2.5"
                          [class]="region.status === 'operational' ? 'fill-success' : 'fill-warning'"
                        />
                      </svg>
                    </div>
    
                    <div
                      class="border-border/50 text-muted-foreground flex items-center justify-between border-t pt-2.5 text-xs"
                    >
                      <div class="text-foreground flex items-center gap-1 font-medium">
                        <lucide-icon [img]="CheckCircle2" class="text-success size-3.5 shrink-0" />
                        <span>{{ region.uptime }}% uptime</span>
                      </div>
                      <div class="flex items-center gap-2 font-mono">
                        <span>P95: {{ region.p95Latency ?? 28 }}ms</span>
                        <span>·</span>
                        <span>{{ region.packetLoss ?? 0 }}% loss</span>
                      </div>
                    </div>
                  </div>
                </div>
              }
            </div>
          </div>
    
          <!-- Section 2: Core Services Component Health List -->
          <div class="space-y-3">
            <div class="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
              <div>
                <h2 class="text-foreground text-sm font-semibold tracking-tight">Core Services Component Health</h2>
                <p class="text-muted-foreground text-xs">90-day daily uptime history and service availability breakdown.</p>
              </div>
    
              <div class="text-muted-foreground flex flex-wrap items-center gap-3 text-xs">
                <div class="flex items-center gap-1.5">
                  <span class="bg-success size-2 rounded-full"></span>
                  <span>Operational</span>
                </div>
                <div class="flex items-center gap-1.5">
                  <span class="bg-warning size-2 rounded-full"></span>
                  <span>Degraded</span>
                </div>
                <div class="flex items-center gap-1.5">
                  <span class="bg-destructive size-2 rounded-full"></span>
                  <span>Outage</span>
                </div>
                <div class="flex items-center gap-1.5">
                  <span class="bg-info size-2 rounded-full"></span>
                  <span>Maintenance</span>
                </div>
              </div>
            </div>
    
            <div ui-card class="border-border/80 bg-card text-card-foreground overflow-hidden shadow-xs">
              <div class="divide-border/60 divide-y">
                @for (service of services(); track service.id) {
                  <div class="hover:bg-muted/15 space-y-3 p-4 transition-colors sm:p-5">
                    <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
                      <div class="flex items-center gap-3">
                        <div
                          class="bg-muted text-muted-foreground border-border/50 flex size-8 shrink-0 items-center justify-center rounded-lg border"
                        >
                          @switch (service.icon) {
                            @case ('auth') {
                              <lucide-icon [img]="ShieldCheck" class="size-4" />
                            }
                            @case ('cdn') {
                              <lucide-icon [img]="Globe" class="size-4" />
                            }
                            @case ('database') {
                              <lucide-icon [img]="Database" class="size-4" />
                            }
                            @case ('webhook') {
                              <lucide-icon [img]="Zap" class="size-4" />
                            }
                            @case ('ai') {
                              <lucide-icon [img]="Cpu" class="size-4" />
                            }
                            @default {
                              <lucide-icon [img]="Server" class="size-4" />
                            }
                          }
                        </div>
                        <div>
                          <p class="text-foreground text-sm leading-tight font-semibold">{{ service.name }}</p>
                          @if (service.description) {
                            <p class="text-muted-foreground mt-0.5 text-xs">{{ service.description }}</p>
                          }
                        </div>
                      </div>
    
                      <div class="flex items-center gap-3 self-start sm:self-auto">
                        <span class="text-foreground font-mono text-xs font-semibold tabular-nums">
                          {{ service.uptime }}% uptime
                        </span>
                        <span
                          ui-badge
                          [variant]="serviceBadgeVariant(service.status)"
                          class="text-xs font-medium capitalize"
                        >
                          <span [class]="cn('mr-1 size-1.5 rounded-full', regionDotClass(service.status))"></span>
                          {{ service.status }}
                        </span>
                      </div>
                    </div>
    
                    <div class="space-y-1.5">
                      <div
                        class="flex w-full gap-px overflow-hidden sm:gap-[2px]"
                        role="img"
                        [attr.aria-label]="service.name + ' 90-day availability history (' + service.uptime + '%)'"
                      >
                        @for (day of daysFor(service); track $index) {
                          <span
                            [class]="
                              cn(
                                'h-6 min-w-px flex-1 cursor-pointer rounded-xs transition-transform hover:scale-y-125',
                                dayClass(day)
                              )
                            "
                            [attr.title]="'Day ' + (90 - $index) + ' ago: ' + dayLabel(day)"
                          ></span>
                        }
                      </div>
    
                      <div class="text-muted-foreground flex items-center justify-between pt-0.5 text-xs">
                        <span>90 days ago</span>
                        <span class="hidden sm:inline">100% daily health check baseline</span>
                        <span>Today</span>
                      </div>
                    </div>
                  </div>
                }
              </div>
            </div>
          </div>
    
          <!-- Section 3: Past Incidents Timeline -->
          <div class="space-y-3">
            <div>
              <h2 class="text-foreground text-sm font-semibold tracking-tight">Past Incidents & Maintenance</h2>
              <p class="text-muted-foreground text-xs">Detailed post-mortems and scheduled infrastructure updates.</p>
            </div>
    
            <div ui-card class="border-border/80 bg-card text-card-foreground p-5 shadow-xs sm:p-6">
              @if (incidents().length > 0) {
                <div class="space-y-8">
                  @for (incident of incidents(); track incident.id) {
                    <div class="border-border/80 relative space-y-3 border-l-2 pb-2 pl-6 last:border-l-transparent sm:pl-8">
                      <div
                        [class]="
                          cn(
                            'bg-background absolute top-0 -left-[11px] flex size-5 items-center justify-center rounded-full border',
                            incident.resolved ? 'border-success text-success' : 'border-warning text-warning'
                          )
                        "
                      >
                        @if (incident.resolved) {
                          <lucide-icon [img]="Check" class="size-3" />
                        } @else {
                          <lucide-icon [img]="AlertTriangle" class="size-3" />
                        }
                      </div>
    
                      <div class="flex flex-col gap-1.5 sm:flex-row sm:items-center sm:justify-between">
                        <div class="space-y-1">
                          <div class="flex flex-wrap items-center gap-2">
                            <h3 class="text-foreground text-sm font-semibold">{{ incident.title }}</h3>
                            <span ui-badge [variant]="incident.resolved ? 'success' : 'warning'" class="text-xs">
                              {{ incident.resolved ? 'Resolved' : 'Ongoing' }}
                            </span>
                            <span ui-badge [variant]="severityBadge(incident.severity).variant" class="text-xs">
                              {{ severityBadge(incident.severity).label }}
                            </span>
                          </div>
                          <p class="text-muted-foreground text-xs">{{ incident.date }}</p>
                        </div>
    
                        <span class="text-muted-foreground font-mono text-xs whitespace-nowrap">
                          Duration: {{ incident.duration }}
                        </span>
                      </div>
    
                      <div class="bg-muted/40 border-border/60 space-y-3 rounded-lg border p-3.5 text-xs sm:p-4">
                        @for (update of incident.updates; track $index) {
                          <div class="border-border/40 space-y-1 border-b pb-2.5 last:border-b-0 last:pb-0">
                            <div class="text-foreground flex items-center gap-2 font-medium">
                              <span class="text-muted-foreground font-mono">{{ update.time }}</span>
                              <span>·</span>
                              <span
                                [class]="
                                  update.status === 'Resolved' || update.status === 'Completed'
                                    ? 'text-success'
                                    : 'text-foreground'
                                "
                              >
                                {{ update.status }}
                              </span>
                            </div>
                            <p class="text-muted-foreground pl-0 leading-relaxed sm:pl-2">
                              {{ update.description }}
                            </p>
                          </div>
                        }
                      </div>
                    </div>
                  }
                </div>
              } @else {
                <div class="flex flex-col items-center justify-center py-8 text-center">
                  <lucide-icon [img]="CheckCircle2" class="text-success mb-2 size-8" />
                  <p class="text-foreground text-sm font-medium">No incidents reported</p>
                  <p class="text-muted-foreground mt-0.5 text-xs">
                    All services have maintained 100% operational integrity.
                  </p>
                </div>
              }
            </div>
          </div>
        </div>
      `,
    })
    export class UiServiceHealthMatrixComponent {
      protected readonly cn = cn
      protected readonly Activity = Activity
      protected readonly AlertTriangle = AlertTriangle
      protected readonly Bell = Bell
      protected readonly Check = Check
      protected readonly CheckCircle2 = CheckCircle2
      protected readonly Clock = Clock
      protected readonly Cpu = Cpu
      protected readonly Database = Database
      protected readonly Globe = Globe
      protected readonly RefreshCw = RefreshCw
      protected readonly Server = Server
      protected readonly ShieldCheck = ShieldCheck
      protected readonly Zap = Zap
    
      @Input('systemStatus') set systemStatusInput(v: RegionStatus | undefined) {
        if (v) this.systemStatus.set(v)
      }
      @Input('systemUptime') set systemUptimeInput(v: string | undefined) {
        if (v !== undefined) this.systemUptime.set(v)
      }
      @Input('lastUpdated') set lastUpdatedInput(v: string | undefined) {
        if (v !== undefined) {
          this.lastUpdated.set(v)
          this.currentUpdatedText.set(v)
        }
      }
      @Input('regions') set regionsInput(v: RegionHealth[] | undefined) {
        if (v)
          this.regions.set(v.map((r) => ({ ...r, latencyHistory: r.latencyHistory ? [...r.latencyHistory] : undefined })))
      }
      @Input('services') set servicesInput(v: ServiceHealthItem[] | undefined) {
        if (v) this.services.set(v.map((s) => ({ ...s, days: s.days ? [...s.days] : undefined })))
      }
      @Input('incidents') set incidentsInput(v: IncidentRecord[] | undefined) {
        if (v) this.incidents.set(v.map((i) => ({ ...i, updates: i.updates.map((u) => ({ ...u })) })))
      }
      @Input('class') className?: string
    
      readonly systemStatus = signal<RegionStatus>('operational')
      readonly systemUptime = signal('99.99%')
      readonly lastUpdated = signal('Updated 30s ago')
      readonly regions = signal<RegionHealth[]>(
        defaultRegions.map((r) => ({ ...r, latencyHistory: [...(r.latencyHistory ?? [])] })),
      )
      readonly services = signal<ServiceHealthItem[]>(defaultServices.map((s) => ({ ...s, days: [...(s.days ?? [])] })))
      readonly incidents = signal<IncidentRecord[]>(
        defaultIncidents.map((i) => ({ ...i, updates: i.updates.map((u) => ({ ...u })) })),
      )
    
      readonly isRefreshing = signal(false)
      readonly isSubscribed = signal(false)
      readonly showSubscribeInput = signal(false)
      readonly emailInput = signal('')
      readonly currentUpdatedText = signal('Updated 30s ago')
    
      private refreshTimer: ReturnType<typeof setTimeout> | undefined
    
      constructor() {
        inject(DestroyRef).onDestroy(() => clearTimeout(this.refreshTimer))
      }
    
      readonly currentBanner = computed<BannerConfig>(
        () => statusBannerConfig[this.systemStatus()] ?? statusBannerConfig.operational,
      )
    
      get rootClass(): string {
        return cn('w-full space-y-6', this.className)
      }
    
      handleRefresh(): void {
        this.isRefreshing.set(true)
        clearTimeout(this.refreshTimer)
        this.refreshTimer = setTimeout(() => {
          this.isRefreshing.set(false)
          this.currentUpdatedText.set('Updated just now')
        }, 400)
      }
    
      handleSubscribeSubmit(): void {
        if (this.emailInput().trim().length > 0) {
          this.isSubscribed.set(true)
          this.showSubscribeInput.set(false)
          this.emailInput.set('')
        }
      }
    
      daysFor(service: ServiceHealthItem): DayStatus[] {
        return service.days ?? generate90Days()
      }
    
      dayClass(day: DayStatus): string {
        return dayClassMap[day]
      }
    
      dayLabel(day: DayStatus): string {
        return dayLabelMap[day]
      }
    
      severityBadge(severity: IncidentSeverity): { variant: BadgeVariant; label: string } {
        return severityBadgeMap[severity]
      }
    
      spark(region: RegionHealth): Sparkline {
        return buildSparkline(region.latencyHistory)
      }
    
      sparkColor(status: RegionStatus): string {
        return status === 'operational' ? 'var(--color-emerald-500, #10b981)' : 'var(--color-amber-500, #f59e0b)'
      }
    
      regionBadgeVariant(status: RegionStatus): BadgeVariant {
        return status === 'operational' ? 'success' : status === 'degraded' ? 'warning' : 'destructive'
      }
    
      serviceBadgeVariant(status: RegionStatus): BadgeVariant {
        return status === 'operational' ? 'success' : status === 'degraded' ? 'warning' : 'destructive'
      }
    
      regionDotClass(status: RegionStatus): string {
        return status === 'operational' ? 'bg-success' : status === 'degraded' ? 'bg-warning' : 'bg-destructive'
      }
    
      latencyToneClass(latency: number): string {
        return latency < 30 ? 'text-success' : latency < 50 ? 'text-info' : 'text-warning'
      }
    
      latencyLabel(latency: number): string {
        return latency < 30 ? 'Optimal' : latency < 50 ? 'Normal' : 'Elevated'
      }
    }
    
  • src/app/components/blocks/service-health-matrix/index.ts0.2 kB

Raw manifest:https://uipkge.dev/r/angular/service-health-matrix.json