{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "status-monitoring",
  "title": "Status Monitoring",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/status-monitoring/status-monitoring.component.ts",
      "content": "// Uptime page. Banner derives All systems operational / Degraded performance /\n// Partial outage from the newest day of each service. Per-service rows render a\n// 90-segment day bar with hover tooltips and a right-aligned uptime percentage,\n// followed by an incident timeline with severity badges.\n// Port of the Vue block 1:1.\nimport { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core'\nimport { cn } from '@/lib/utils'\nimport { UiBadgeComponent } from '@/ui/badge/badge.component'\nimport { UiSectionCardComponent } from '@/ui/section-card/section-card.component'\n\nexport type DayStatus = 'up' | 'degraded' | 'down' | 'maintenance' | 'none'\n\nexport type IncidentSeverity = 'minor' | 'major' | 'critical'\n\nexport interface ServiceStatus {\n  name: string\n  /** One entry per day, oldest first. Expected length: 90. */\n  days: DayStatus[]\n}\n\nexport interface MonitoringIncident {\n  id: string\n  date: Date\n  title: string\n  description: string\n  duration: string\n  severity: IncidentSeverity\n  resolved?: boolean\n}\n\nexport type OverallState = 'operational' | 'degraded' | 'outage'\n\nconst DAY_COUNT = 90\n\n// Deterministic stubs (no Math.random) so SSR and client markup match exactly.\nfunction buildDays(blips: Array<{ ago: number; length: number; status: DayStatus }> = []): DayStatus[] {\n  const days: DayStatus[] = Array.from({ length: DAY_COUNT }, () => 'up')\n  for (const blip of blips) {\n    for (let k = 0; k < blip.length; k++) {\n      const idx = DAY_COUNT - 1 - blip.ago - k\n      if (idx >= 0 && idx < DAY_COUNT) days[idx] = blip.status\n    }\n  }\n  return days\n}\n\nconst now = new Date()\nconst daysAgoDate = (d: number) => new Date(now.getTime() - d * 86_400_000)\n\nconst stubServices: ServiceStatus[] = [\n  {\n    name: 'API Gateway',\n    days: buildDays([\n      { ago: 34, length: 1, status: 'down' },\n      { ago: 33, length: 1, status: 'degraded' },\n    ]),\n  },\n  { name: 'Web application', days: buildDays() },\n  { name: 'Webhooks', days: buildDays([{ ago: 61, length: 1, status: 'degraded' }]) },\n]\n\nconst stubIncidents: MonitoringIncident[] = [\n  {\n    id: 'i1',\n    date: daysAgoDate(33),\n    title: 'Elevated error rates on API Gateway',\n    description: 'A bad deploy caused intermittent 502s for roughly forty minutes. Rolled back; error rates recovered.',\n    duration: 'Lasted 42 minutes',\n    severity: 'major',\n    resolved: true,\n  },\n  {\n    id: 'i2',\n    date: daysAgoDate(34),\n    title: 'API Gateway unavailable',\n    description: 'Provider network maintenance dropped traffic between 02:14 and 02:51 UTC.',\n    duration: 'Lasted 37 minutes',\n    severity: 'critical',\n    resolved: true,\n  },\n  {\n    id: 'i3',\n    date: daysAgoDate(61),\n    title: 'Delayed webhook deliveries',\n    description: 'A queue backlog delayed deliveries by up to six minutes. The backlog drained on its own.',\n    duration: 'Lasted 1 hour, 12 minutes',\n    severity: 'minor',\n    resolved: true,\n  },\n]\n\nconst bannerMeta: Record<OverallState, { label: string; box: string; text: string; dot: string }> = {\n  operational: {\n    label: 'All systems operational',\n    box: 'border-success/25 bg-success/10',\n    text: 'text-success',\n    dot: 'bg-success',\n  },\n  degraded: {\n    label: 'Degraded performance',\n    box: 'border-warning/25 bg-warning/10',\n    text: 'text-warning',\n    dot: 'bg-warning',\n  },\n  outage: {\n    label: 'Partial outage',\n    box: 'border-destructive/25 bg-destructive/10',\n    text: 'text-destructive',\n    dot: 'bg-destructive',\n  },\n}\n\nconst dayClass: Record<DayStatus, string> = {\n  up: 'bg-success',\n  degraded: 'bg-warning',\n  down: 'bg-destructive',\n  maintenance: 'bg-info',\n  none: 'bg-muted',\n}\n\nconst dayLabel: Record<DayStatus, string> = {\n  up: 'Operational',\n  degraded: 'Degraded',\n  down: 'Outage',\n  maintenance: 'Maintenance',\n  none: 'No data',\n}\n\nconst severityVariant: Record<IncidentSeverity, 'info' | 'warning' | 'destructive'> = {\n  minor: 'info',\n  major: 'warning',\n  critical: 'destructive',\n}\n\n// Built once per module load so 90 segments × N services never rebuild Date objects during render.\nconst dayStamps = Array.from({ length: DAY_COUNT }, (_, i) =>\n  daysAgoDate(DAY_COUNT - 1 - i).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }),\n)\n\nconst updatedStamp = now.toLocaleString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' })\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-status-monitoring, [ui-status-monitoring]',\n  standalone: true,\n  // Vue renders the root <div> itself: the host stays out of layout and `class` goes to the root.\n  host: { '[attr.class]': '\"contents\"' },\n  imports: [UiBadgeComponent, UiSectionCardComponent],\n  template: `\n    <div data-slot=\"status-monitoring\" [class]=\"rootClass\">\n      <div role=\"status\" [class]=\"bannerClass\">\n        <span class=\"relative flex size-2.5 shrink-0\">\n          <span [class]=\"bannerDotPingClass\"></span>\n          <span [class]=\"bannerDotClass\"></span>\n        </span>\n        <p [class]=\"bannerTextClass\">{{ banner().label }}</p>\n        <span class=\"text-muted-foreground ml-auto text-xs whitespace-nowrap\">Updated {{ updatedStamp }}</span>\n      </div>\n\n      <ui-section-card title=\"Service status\" description=\"Daily availability over the last 90 days.\">\n        <ul slot=\"header-action\" class=\"text-muted-foreground hidden flex-wrap items-center gap-3 text-xs md:flex\">\n          @for (item of legend; track item.status) {\n            <li class=\"flex shrink-0 items-center gap-1.5 whitespace-nowrap\">\n              <span [class]=\"legendDotClass(item.status)\"></span>\n              {{ item.label }}\n            </li>\n          }\n        </ul>\n        <ul class=\"divide-y\">\n          @for (s of servicesList(); track s.name; let si = $index) {\n            <li [class]=\"serviceRowClass(si)\">\n              <div class=\"flex flex-wrap items-baseline justify-between gap-4\">\n                <p class=\"text-sm font-medium\">{{ s.name }}</p>\n                <p class=\"text-muted-foreground text-sm tabular-nums\">{{ uptimePct(s.days) }}</p>\n              </div>\n              <div\n                class=\"flex w-full gap-px overflow-hidden sm:gap-[2px]\"\n                role=\"img\"\n                [attr.aria-label]=\"s.name + ' uptime ' + uptimePct(s.days) + ' over the last 90 days'\"\n              >\n                @for (d of s.days; track $index; let di = $index) {\n                  <span [class]=\"daySegmentClass(d)\" [attr.title]=\"tooltipFor(s.days, di)\"></span>\n                }\n              </div>\n            </li>\n          }\n        </ul>\n        <p class=\"text-muted-foreground mt-4 flex justify-between text-xs\">\n          <span>90 days ago</span>\n          <span>Today</span>\n        </p>\n      </ui-section-card>\n\n      <ui-section-card title=\"Incident history\" description=\"Reported incidents over the same period.\">\n        @if (incidentList().length) {\n          <ul class=\"divide-y\">\n            @for (inc of incidentList(); track inc.id; let ii = $index) {\n              <li [class]=\"ii === 0 ? 'py-4 pt-0' : 'py-4'\">\n                <p class=\"text-muted-foreground text-xs font-medium\">{{ formatDay(inc.date) }}</p>\n                <div class=\"mt-1.5 flex flex-wrap items-center gap-2\">\n                  <span ui-badge [variant]=\"incidentBadgeVariant(inc)\" class=\"capitalize\">\n                    {{ inc.resolved ? 'Resolved' : inc.severity }}\n                  </span>\n                  <p class=\"text-sm font-medium\">{{ inc.title }}</p>\n                  <span class=\"text-muted-foreground ml-auto text-xs whitespace-nowrap\">{{ inc.duration }}</span>\n                </div>\n                <p class=\"text-muted-foreground mt-1 text-sm leading-relaxed\">{{ inc.description }}</p>\n              </li>\n            }\n          </ul>\n        } @else {\n          <div class=\"flex flex-col items-center justify-center px-6 py-10 text-center\">\n            <p class=\"text-sm font-medium\">No incidents reported</p>\n            <p class=\"text-muted-foreground mt-0.5 text-xs\">Every day in this window has been quiet.</p>\n          </div>\n        }\n      </ui-section-card>\n    </div>\n  `,\n})\nexport class UiStatusMonitoringComponent {\n  protected readonly legend = (Object.keys(dayLabel) as DayStatus[]).map((status) => ({\n    status,\n    label: dayLabel[status],\n  }))\n  protected readonly updatedStamp = updatedStamp\n\n  @Input() set services(v: ServiceStatus[] | undefined) {\n    this.servicesInput.set(v)\n  }\n  @Input() set incidents(v: MonitoringIncident[] | undefined) {\n    this.incidentsInput.set(v)\n  }\n  @Input('class') className?: string\n\n  private readonly servicesInput = signal<ServiceStatus[] | undefined>(undefined)\n  private readonly incidentsInput = signal<MonitoringIncident[] | undefined>(undefined)\n\n  readonly servicesList = computed(() => this.servicesInput() ?? stubServices)\n\n  readonly incidentList = computed(() =>\n    [...(this.incidentsInput() ?? stubIncidents)].sort((a, b) => b.date.getTime() - a.date.getTime()),\n  )\n\n  readonly overall = computed<OverallState>(() => {\n    const latest = this.servicesList().map((s) => [...s.days].reverse().find((d) => d !== 'none'))\n    if (latest.includes('down')) return 'outage'\n    if (latest.includes('degraded')) return 'degraded'\n    return 'operational'\n  })\n\n  readonly banner = computed(() => bannerMeta[this.overall()])\n\n  get rootClass(): string {\n    return cn('space-y-4', this.className)\n  }\n\n  get bannerClass(): string {\n    return cn('flex items-center gap-3 rounded-xl border px-4 py-3', this.banner().box)\n  }\n\n  get bannerTextClass(): string {\n    return cn('text-sm font-medium', this.banner().text)\n  }\n\n  get bannerDotClass(): string {\n    return cn('relative inline-flex size-2.5 rounded-full', this.banner().dot)\n  }\n\n  get bannerDotPingClass(): string {\n    return cn('absolute inline-flex h-full w-full rounded-full opacity-60', this.banner().dot)\n  }\n\n  legendDotClass(status: DayStatus): string {\n    return cn('size-2 rounded-full', dayClass[status])\n  }\n\n  daySegmentClass(status: DayStatus): string {\n    return cn('h-6 w-full min-w-px flex-1 rounded-sm transition-transform hover:scale-y-125', dayClass[status])\n  }\n\n  serviceRowClass(index: number): string {\n    return cn('space-y-2 py-4', index === 0 && 'pt-0')\n  }\n\n  tooltipFor(days: DayStatus[], i: number): string {\n    return `${dayStamps[i]} · ${dayLabel[days[i]]}`\n  }\n\n  uptimePct(days: DayStatus[]): string {\n    const counted = days.filter((d) => d !== 'none')\n    if (counted.length === 0) return '—'\n    const up = counted.filter((d) => d === 'up').length\n    return `${((up / counted.length) * 100).toFixed(2)}%`\n  }\n\n  formatDay(d: Date): string {\n    return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })\n  }\n\n  incidentBadgeVariant(inc: MonitoringIncident): 'success' | 'info' | 'warning' | 'destructive' {\n    if (inc.resolved) return 'success'\n    return severityVariant[inc.severity]\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/status-monitoring/status-monitoring.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/status-monitoring/index.ts",
      "content": "export {\n  UiStatusMonitoringComponent,\n  type DayStatus,\n  type IncidentSeverity,\n  type ServiceStatus,\n  type MonitoringIncident,\n  type OverallState,\n} from './status-monitoring.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/status-monitoring/index.ts"
    }
  ],
  "dependencies": [
    "lucide-angular"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/badge.json",
    "https://uipkge.dev/r/angular/section-card.json"
  ],
  "description": "Uptime page. Banner derives All systems operational / Degraded performance / Partial outage from the newest day of each service. Per-service rows render a 90-segment day bar (success/warning/destructive/info/muted per status) with hover tooltips and a right-aligned uptime percentage, followed by an incident timeline with severity badges. Pass `services` (name + days array of up/degraded/down/maintenance/none) and `incidents` to replace the stubs.",
  "categories": [
    "devops",
    "dashboard",
    "analytics"
  ]
}