{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "etl-pipeline-dag",
  "title": "Etl Pipeline Dag",
  "type": "registry:page",
  "files": [
    {
      "path": "packages/registry-angular/blocks/etl-pipeline-dag/etl-pipeline-dag.component.ts",
      "content": "// Airflow / Dagster style directed acyclic graph (DAG) pipeline visualizer: pipeline header\n// with trigger + export actions, four health metric cards, a five-stage execution topology\n// with selectable task nodes, and the per-node log inspector. Port of the React block 1:1.\nimport { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core'\nimport {\n  ArrowRight,\n  Check,\n  CheckCircle2,\n  Clock,\n  Cpu,\n  CreditCard,\n  Database,\n  FileDown,\n  GitBranch,\n  Globe,\n  Layers,\n  Loader2,\n  LucideAngularModule,\n  Play,\n  ShieldCheck,\n  Workflow,\n  Zap,\n  type LucideIconData,\n} from 'lucide-angular'\nimport { cn } from '@/lib/utils'\nimport { UiBadgeComponent } from '@/ui/badge/badge.component'\nimport { UiButtonComponent } from '@/ui/button/button.component'\nimport {\n  UiCardComponent,\n  UiCardContentComponent,\n  UiCardDescriptionComponent,\n  UiCardHeaderComponent,\n  UiCardTitleComponent,\n} from '@/ui/card/card.component'\nimport { UiProgressComponent } from '@/ui/progress/progress.component'\nimport { UiEtlNodeDetailComponent } from './etl-node-detail.component'\nimport { pipelineNodes, type DagNode, type NodeCategory } from './etl-pipeline-dag-data'\n\nexport type { DagNode, NodeCategory }\n\ninterface NodeCardMeta {\n  id: string\n  icon: LucideIconData\n  iconBoxClass: string\n  subtitle: string\n  footerTarget: string\n  footerDone?: boolean\n  chips: { label: string; toneClass: string }[]\n  durationLabel: string\n  centered?: boolean\n}\n\nconst NODE_CARDS: NodeCardMeta[] = [\n  {\n    id: 'extract_stripe_charges',\n    icon: CreditCard,\n    iconBoxClass: 'border-info/30 bg-info/10 text-info',\n    subtitle: '148.3k records · 42.6 MB',\n    footerTarget: 'raw_stripe.charges',\n    chips: [],\n    durationLabel: '18s',\n  },\n  {\n    id: 'extract_currency_rates',\n    icon: Globe,\n    iconBoxClass: 'border-info/30 bg-info/10 text-info',\n    subtitle: '34 FX pairs · 128 KB',\n    footerTarget: 'raw_currency.rates',\n    chips: [],\n    durationLabel: '4s',\n  },\n  {\n    id: 'transform_normalize_fx',\n    icon: Cpu,\n    iconBoxClass: 'border-chart-1/30 bg-chart-1/10 text-chart-1',\n    subtitle: '148,290 rows · PySpark 3.5',\n    footerTarget: 'stg_payments_fx',\n    chips: [\n      { label: '2 Upstreams', toneClass: 'border-border bg-muted/60 text-muted-foreground' },\n      { label: 'Snappy', toneClass: 'border-border bg-muted/60 text-muted-foreground' },\n    ],\n    durationLabel: '42s',\n    centered: true,\n  },\n  {\n    id: 'validate_schema_anomalies',\n    icon: ShieldCheck,\n    iconBoxClass: 'border-warning/30 bg-warning/10 text-warning',\n    subtitle: '18/18 checks passed (100%)',\n    footerTarget: 'anomalies_audit',\n    chips: [\n      { label: '0 Anomalies', toneClass: 'border-success/30 bg-success/10 text-success' },\n      { label: 'Z-Score 3σ', toneClass: 'border-border bg-muted/60 text-muted-foreground' },\n    ],\n    durationLabel: '14s',\n    centered: true,\n  },\n  {\n    id: 'load_snowflake_warehouse',\n    icon: Database,\n    iconBoxClass: 'border-cyan-500/30 bg-cyan-500/10 text-cyan-500',\n    subtitle: '148,290 rows merged',\n    footerTarget: 'fct_stripe_charges',\n    chips: [\n      { label: '4X-Large', toneClass: 'border-border bg-muted/60 text-muted-foreground' },\n      { label: 'Clustered', toneClass: 'border-border bg-muted/60 text-muted-foreground' },\n    ],\n    durationLabel: '1m 20s',\n    centered: true,\n  },\n  {\n    id: 'refresh_dbt_analytics_marts',\n    icon: Layers,\n    iconBoxClass: 'border-success/30 bg-success/10 text-success',\n    subtitle: '12 models · 48 tests',\n    footerTarget: 'mart_finance_revenue',\n    footerDone: true,\n    chips: [\n      { label: '8 Threads', toneClass: 'border-border bg-muted/60 text-muted-foreground' },\n      { label: 'BI Synced', toneClass: 'border-success/30 bg-success/10 text-success' },\n    ],\n    durationLabel: '1m 40s',\n    centered: true,\n  },\n]\n\ninterface DagStage {\n  heading: string\n  badge: string\n  badgeVariant: 'outline' | 'secondary'\n  nodeIds: string[]\n}\n\nconst DAG_STAGES: DagStage[] = [\n  {\n    heading: '01 · Extract (2)',\n    badge: 'Parallel',\n    badgeVariant: 'outline',\n    nodeIds: ['extract_stripe_charges', 'extract_currency_rates'],\n  },\n  { heading: '02 · Transform', badge: 'Join 2→1', badgeVariant: 'secondary', nodeIds: ['transform_normalize_fx'] },\n  { heading: '03 · Quality', badge: '18 Checks', badgeVariant: 'outline', nodeIds: ['validate_schema_anomalies'] },\n  { heading: '04 · Load', badge: 'Snowflake', badgeVariant: 'outline', nodeIds: ['load_snowflake_warehouse'] },\n  { heading: '05 · Marts', badge: 'dbt Core', badgeVariant: 'outline', nodeIds: ['refresh_dbt_analytics_marts'] },\n]\n\nfunction getNodeCategoryBadgeClass(category: NodeCategory): string {\n  switch (category) {\n    case 'ingest':\n      return 'border-info/30 bg-info/10 text-info'\n    case 'transform':\n      return 'border-chart-1/30 bg-chart-1/10 text-chart-1'\n    case 'validate':\n      return 'border-warning/30 bg-warning/10 text-warning'\n    case 'load':\n      return 'border-chart-2/30 bg-chart-2/10 text-chart-2'\n    case 'model':\n      return 'border-success/30 bg-success/10 text-success'\n    default:\n      return 'border-border bg-muted text-muted-foreground'\n  }\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-etl-pipeline-dag, [ui-etl-pipeline-dag]',\n  standalone: true,\n  // React renders the root <div> itself: the host stays out of layout and `class` goes to the root.\n  host: { '[attr.class]': '\"contents\"' },\n  imports: [\n    LucideAngularModule,\n    UiBadgeComponent,\n    UiButtonComponent,\n    UiCardComponent,\n    UiCardContentComponent,\n    UiCardDescriptionComponent,\n    UiCardHeaderComponent,\n    UiCardTitleComponent,\n    UiProgressComponent,\n    UiEtlNodeDetailComponent,\n  ],\n  template: `\n    <div data-slot=\"etl-pipeline-dag\" [class]=\"rootClass\">\n      <!-- Top Pipeline Header Card -->\n      <div ui-card class=\"border-border bg-card shadow-xs\">\n        <div ui-card-content class=\"flex flex-col gap-4 p-5 sm:flex-row sm:items-center sm:justify-between\">\n          <!-- Left Title and DAG Badges -->\n          <div class=\"flex min-w-0 items-start gap-3.5 sm:items-center\">\n            <div\n              class=\"border-primary/20 bg-primary/10 text-primary flex size-11 shrink-0 items-center justify-center rounded-xl border shadow-xs\"\n            >\n              <lucide-icon [img]=\"Workflow\" class=\"size-5\" />\n            </div>\n\n            <div class=\"min-w-0 space-y-1.5\">\n              <div class=\"flex flex-wrap items-center gap-2\">\n                <h2 class=\"text-foreground font-mono text-base font-semibold tracking-tight break-all sm:text-lg\">\n                  stripe_payments_daily_etl\n                </h2>\n                <span ui-badge variant=\"secondary\" class=\"font-mono text-xs\">v2.4.1</span>\n              </div>\n\n              <div class=\"flex flex-wrap items-center gap-2\">\n                <!-- DAG Schedule Badge -->\n                <span ui-badge variant=\"outline\" class=\"gap-1.5 font-mono text-xs shadow-xs\">\n                  <lucide-icon [img]=\"Clock\" class=\"text-muted-foreground size-3\" />\n                  <span>0 2 * * * · Daily at 02:00 UTC</span>\n                </span>\n\n                <!-- State Badge -->\n                <span ui-badge variant=\"success\" class=\"gap-1.5 font-mono text-xs shadow-xs\">\n                  <lucide-icon [img]=\"CheckCircle2\" class=\"size-3\" />\n                  <span>State: Success · Run ID: #run_90412</span>\n                </span>\n\n                <!-- Live Queue Toast -->\n                @if (triggerSuccessToast()) {\n                  <span ui-badge variant=\"info\" class=\"animate-in fade-in-0 gap-1.5 font-mono text-xs duration-300\">\n                    <lucide-icon [img]=\"Zap\" class=\"size-3\" />\n                    <span>Triggered Run #run_90413 queued!</span>\n                  </span>\n                }\n              </div>\n            </div>\n          </div>\n\n          <!-- Right Action Buttons -->\n          <div class=\"flex flex-wrap items-center gap-2.5\">\n            <button\n              ui-button\n              variant=\"outline\"\n              size=\"sm\"\n              class=\"h-8.5 gap-1.5 text-xs font-medium\"\n              [disabled]=\"copiedAllLogs()\"\n              (click)=\"exportFullRunLog()\"\n            >\n              @if (copiedAllLogs()) {\n                <lucide-icon [img]=\"Check\" class=\"size-3.5\" />\n              } @else {\n                <lucide-icon [img]=\"FileDown\" class=\"size-3.5\" />\n              }\n              <span>{{ copiedAllLogs() ? 'Run Log Copied!' : 'Export Run Log' }}</span>\n            </button>\n\n            <button\n              ui-button\n              variant=\"default\"\n              size=\"sm\"\n              class=\"h-8.5 gap-1.5 text-xs font-medium\"\n              [disabled]=\"isTriggering()\"\n              (click)=\"triggerDag()\"\n            >\n              @if (isTriggering()) {\n                <lucide-icon [img]=\"Loader2\" class=\"size-3.5 animate-spin\" />\n              } @else {\n                <lucide-icon [img]=\"Play\" class=\"size-3.5 fill-current\" />\n              }\n              <span>{{ isTriggering() ? 'Triggering DAG Run...' : 'Trigger DAG Run' }}</span>\n            </button>\n          </div>\n        </div>\n      </div>\n\n      <!-- 4 Pipeline Health Metric Cards -->\n      <div class=\"grid grid-cols-1 gap-3.5 sm:grid-cols-2 lg:grid-cols-4\">\n        <!-- Total Duration -->\n        <div ui-card class=\"border-border bg-card shadow-xs\">\n          <div ui-card-content class=\"flex flex-col justify-between p-4.5\">\n            <div class=\"flex items-center justify-between\">\n              <span class=\"text-muted-foreground text-xs font-medium\">Total Duration</span>\n              <div\n                class=\"border-border bg-muted/60 text-muted-foreground flex size-7 items-center justify-center rounded-md border\"\n              >\n                <lucide-icon [img]=\"Clock\" class=\"size-3.5\" />\n              </div>\n            </div>\n            <div class=\"mt-2 space-y-1\">\n              <div class=\"text-foreground font-mono text-2xl font-bold tracking-tight tabular-nums\">04m:18s</div>\n              <div class=\"flex items-center gap-1.5 text-xs\">\n                <span class=\"text-success font-medium\">On schedule</span>\n                <span class=\"text-muted-foreground font-mono tabular-nums\">-14s vs 7d avg</span>\n              </div>\n            </div>\n          </div>\n        </div>\n\n        <!-- Tasks Completed -->\n        <div ui-card class=\"border-border bg-card shadow-xs\">\n          <div ui-card-content class=\"flex flex-col justify-between p-4.5\">\n            <div class=\"flex items-center justify-between\">\n              <span class=\"text-muted-foreground text-xs font-medium\">Tasks Completed</span>\n              <div\n                class=\"border-success/30 bg-success/10 text-success flex size-7 items-center justify-center rounded-md border\"\n              >\n                <lucide-icon [img]=\"CheckCircle2\" class=\"size-3.5\" />\n              </div>\n            </div>\n            <div class=\"mt-2 space-y-1.5\">\n              <div class=\"text-foreground font-mono text-2xl font-bold tracking-tight tabular-nums\">\n                8 / 8 tasks succeeded\n              </div>\n              <div class=\"space-y-1\">\n                <ui-progress [value]=\"100\" class=\"h-1.5\" />\n                <p class=\"text-muted-foreground text-xs\">100% completion · 0 retries · 0 failed</p>\n              </div>\n            </div>\n          </div>\n        </div>\n\n        <!-- Records Ingested -->\n        <div ui-card class=\"border-border bg-card shadow-xs\">\n          <div ui-card-content class=\"flex flex-col justify-between p-4.5\">\n            <div class=\"flex items-center justify-between\">\n              <span class=\"text-muted-foreground text-xs font-medium\">Records Ingested</span>\n              <div\n                class=\"border-border bg-muted/60 text-muted-foreground flex size-7 items-center justify-center rounded-md border\"\n              >\n                <lucide-icon [img]=\"Database\" class=\"size-3.5\" />\n              </div>\n            </div>\n            <div class=\"mt-2 space-y-1\">\n              <div class=\"text-foreground font-mono text-2xl font-bold tracking-tight tabular-nums\">\n                148,290 records\n              </div>\n              <div class=\"flex items-center gap-1.5 text-xs\">\n                <span class=\"text-success font-medium\">100% data fidelity</span>\n                <span class=\"text-muted-foreground\">0 dropped rows</span>\n              </div>\n            </div>\n          </div>\n        </div>\n\n        <!-- Data Freshness -->\n        <div ui-card class=\"border-border bg-card shadow-xs\">\n          <div ui-card-content class=\"flex flex-col justify-between p-4.5\">\n            <div class=\"flex items-center justify-between\">\n              <span class=\"text-muted-foreground text-xs font-medium\">Data Freshness</span>\n              <div\n                class=\"border-border bg-muted/60 text-muted-foreground flex size-7 items-center justify-center rounded-md border\"\n              >\n                <lucide-icon [img]=\"Zap\" class=\"size-3.5\" />\n              </div>\n            </div>\n            <div class=\"mt-2 space-y-1\">\n              <div class=\"text-foreground font-mono text-2xl font-bold tracking-tight tabular-nums\">\n                Lag: 12m · SLA &lt; 1h\n              </div>\n              <div class=\"flex items-center gap-1.5 text-xs\">\n                <span class=\"text-success font-medium\">SLA Healthy</span>\n                <span class=\"text-muted-foreground\">Target: &lt; 60m sync</span>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n\n      <!-- Interactive DAG Node Pipeline Graph -->\n      <div ui-card class=\"border-border bg-card shadow-xs\">\n        <div ui-card-header class=\"pb-3\">\n          <div class=\"flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between\">\n            <div>\n              <div class=\"flex items-center gap-2\">\n                <h3 ui-card-title class=\"text-base font-semibold\">DAG Execution Topology</h3>\n                <span ui-badge variant=\"outline\" class=\"font-mono text-xs\">6 Connected Tasks</span>\n              </div>\n              <p ui-card-description class=\"text-xs\">\n                Directed acyclic graph with dependency streams. Select any node to inspect telemetry and execution logs.\n              </p>\n            </div>\n\n            <!-- Quick Topology Status Info -->\n            <div class=\"flex flex-wrap items-center gap-2 text-xs\">\n              <div\n                class=\"border-border bg-muted/30 text-muted-foreground flex items-center gap-1.5 rounded-md border px-2 py-1 font-mono\"\n              >\n                <span class=\"bg-success inline-block size-2 rounded-full\"></span>\n                <span>All Upstreams Resolved</span>\n              </div>\n              <div\n                class=\"border-border bg-muted/30 text-muted-foreground flex items-center gap-1.5 rounded-md border px-2 py-1 font-mono\"\n              >\n                <lucide-icon [img]=\"GitBranch\" class=\"size-3\" />\n                <span>Topology: 2 Branches → Converged</span>\n              </div>\n            </div>\n          </div>\n        </div>\n\n        <div ui-card-content class=\"p-4 pt-1 sm:p-6\">\n          <!-- DAG Canvas Box -->\n          <div class=\"border-border bg-muted/10 relative rounded-xl border p-4 sm:p-5\">\n            <!-- Desktop 5-Stage Topology Pipeline Layout -->\n            <div class=\"grid grid-cols-1 gap-5 lg:grid-cols-5\">\n              @for (stage of dagStages; track stage.heading) {\n                <div class=\"flex flex-col justify-between gap-3.5\">\n                  <div class=\"flex items-center justify-between px-1\">\n                    <span class=\"text-muted-foreground font-mono text-xs font-semibold tracking-wider uppercase\">\n                      {{ stage.heading }}\n                    </span>\n                    <span ui-badge [variant]=\"stage.badgeVariant\" class=\"font-mono text-xs\">{{ stage.badge }}</span>\n                  </div>\n\n                  @for (meta of stageMetas(stage); track meta.id) {\n                    <div\n                      role=\"button\"\n                      tabindex=\"0\"\n                      [class]=\"nodeCardClass(meta)\"\n                      (click)=\"selectedNodeId.set(meta.id)\"\n                      (keydown.enter)=\"selectedNodeId.set(meta.id)\"\n                      (keydown.space)=\"selectedNodeId.set(meta.id); $event.preventDefault()\"\n                    >\n                      <div class=\"flex items-start justify-between gap-2\">\n                        <div class=\"flex items-center gap-2\">\n                          <div\n                            [class]=\"cn('flex size-7 items-center justify-center rounded-md border', meta.iconBoxClass)\"\n                          >\n                            <lucide-icon [img]=\"meta.icon\" class=\"size-3.5\" />\n                          </div>\n                          <span ui-badge [class]=\"cn('font-mono text-xs', categoryClass(nodeOf(meta.id).category))\">\n                            {{ nodeOf(meta.id).categoryLabel }}\n                          </span>\n                        </div>\n                        <span ui-badge variant=\"success\" class=\"gap-1 font-mono text-xs\">\n                          <lucide-icon [img]=\"Check\" class=\"size-3\" />\n                          {{ meta.durationLabel }}\n                        </span>\n                      </div>\n\n                      <div class=\"mt-2.5\">\n                        <div class=\"text-foreground font-mono text-xs font-semibold break-all\">{{ meta.id }}</div>\n                        <div class=\"text-muted-foreground mt-0.5 font-mono text-xs\">{{ meta.subtitle }}</div>\n                      </div>\n\n                      @if (meta.chips.length > 0) {\n                        <div class=\"mt-2 flex flex-wrap gap-1\">\n                          @for (chip of meta.chips; track chip.label) {\n                            <span [class]=\"cn('rounded px-1.5 py-0.5 font-mono text-xs', chip.toneClass)\">\n                              {{ chip.label }}\n                            </span>\n                          }\n                        </div>\n                      }\n\n                      <div class=\"border-border/60 mt-3 flex items-center justify-between border-t pt-2 text-xs\">\n                        <span class=\"text-muted-foreground truncate font-mono\">{{ meta.footerTarget }}</span>\n                        @if (meta.footerDone) {\n                          <lucide-icon [img]=\"CheckCircle2\" class=\"text-success size-3 shrink-0\" />\n                        } @else {\n                          <lucide-icon\n                            [img]=\"ArrowRight\"\n                            class=\"text-muted-foreground group-hover:text-primary size-3 shrink-0 transition-colors\"\n                          />\n                        }\n                      </div>\n                    </div>\n                  }\n                </div>\n              }\n            </div>\n          </div>\n        </div>\n      </div>\n\n      <!-- Selected Node Log Inspector Panel -->\n      <ui-etl-node-detail\n        [node]=\"selectedNode()\"\n        [categoryBadgeClass]=\"cn('font-mono text-xs', categoryClass(selectedNode().category))\"\n        [icon]=\"nodeIcon(selectedNode().id)\"\n        (selectNode)=\"selectedNodeId.set($event)\"\n      />\n    </div>\n  `,\n})\nexport class UiEtlPipelineDagComponent {\n  protected readonly ArrowRight = ArrowRight\n  protected readonly Check = Check\n  protected readonly CheckCircle2 = CheckCircle2\n  protected readonly Clock = Clock\n  protected readonly Database = Database\n  protected readonly FileDown = FileDown\n  protected readonly GitBranch = GitBranch\n  protected readonly Loader2 = Loader2\n  protected readonly Play = Play\n  protected readonly Workflow = Workflow\n  protected readonly Zap = Zap\n  protected readonly cn = cn\n\n  @Input() set initialNodeId(v: string | undefined) {\n    this.selectedNodeId.set(v ?? 'extract_stripe_charges')\n  }\n  @Input('class') className?: string\n\n  readonly selectedNodeId = signal('extract_stripe_charges')\n  readonly copiedAllLogs = signal(false)\n  readonly isTriggering = signal(false)\n  readonly triggerSuccessToast = signal(false)\n  private copyTimer: ReturnType<typeof setTimeout> | undefined\n  private triggerTimers: ReturnType<typeof setTimeout>[] = []\n\n  protected readonly dagStages = DAG_STAGES\n  private readonly nodeCards = NODE_CARDS\n  private readonly nodes: DagNode[] = pipelineNodes\n\n  readonly selectedNode = computed<DagNode>(() => {\n    return this.nodes.find((n) => n.id === this.selectedNodeId()) ?? this.nodes[0]!\n  })\n\n  get rootClass(): string {\n    return cn('w-full space-y-6', this.className)\n  }\n\n  nodeOf(id: string): DagNode {\n    return this.nodes.find((n) => n.id === id) ?? this.nodes[0]!\n  }\n\n  stageMetas(stage: DagStage): NodeCardMeta[] {\n    return stage.nodeIds.map((id) => this.nodeCards.find((m) => m.id === id)).filter((m): m is NodeCardMeta => !!m)\n  }\n\n  categoryClass(category: NodeCategory): string {\n    return getNodeCategoryBadgeClass(category)\n  }\n\n  nodeIcon(id: string): LucideIconData {\n    switch (id) {\n      case 'extract_stripe_charges':\n        return CreditCard\n      case 'extract_currency_rates':\n        return Globe\n      case 'transform_normalize_fx':\n        return Cpu\n      case 'validate_schema_anomalies':\n        return ShieldCheck\n      case 'load_snowflake_warehouse':\n        return Database\n      case 'refresh_dbt_analytics_marts':\n        return Layers\n      default:\n        return Workflow\n    }\n  }\n\n  nodeCardClass(meta: NodeCardMeta): string {\n    return cn(\n      'group focus-visible:ring-ring relative cursor-pointer rounded-xl border p-3.5 transition-colors duration-200 focus-visible:ring-2 focus-visible:outline-none',\n      meta.centered && 'my-auto',\n      this.selectedNodeId() === meta.id\n        ? 'border-primary bg-primary/[0.04] ring-primary/30 shadow-xs ring-2'\n        : 'border-border bg-card hover:border-primary/50 hover:bg-muted/30',\n    )\n  }\n\n  triggerDag(): void {\n    if (this.isTriggering()) return\n    this.isTriggering.set(true)\n    this.triggerSuccessToast.set(false)\n    this.triggerTimers.push(\n      setTimeout(() => {\n        this.isTriggering.set(false)\n        this.triggerSuccessToast.set(true)\n        this.triggerTimers.push(\n          setTimeout(() => {\n            this.triggerSuccessToast.set(false)\n          }, 4000),\n        )\n      }, 1600),\n    )\n  }\n\n  async exportFullRunLog(): Promise<void> {\n    const fullLog = this.nodes\n      .map((node) => {\n        const header = `=== Task: ${node.name} (${node.operator}) - Duration: ${node.duration} ===`\n        const body = node.logs.map((l) => `[${l.timestamp}] [${l.level}] [${l.source}] ${l.message}`).join('\\n')\n        return `${header}\\n${body}`\n      })\n      .join('\\n\\n')\n    try {\n      await navigator.clipboard.writeText(fullLog)\n      this.copiedAllLogs.set(true)\n      clearTimeout(this.copyTimer)\n      this.copyTimer = setTimeout(() => {\n        this.copiedAllLogs.set(false)\n      }, 2500)\n    } catch {\n      // Clipboard fallback\n    }\n  }\n}\n",
      "type": "registry:page",
      "target": "~/src/app/components/blocks/etl-pipeline-dag/etl-pipeline-dag.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/etl-pipeline-dag/etl-node-detail.component.ts",
      "content": "// Per-node execution inspector: stdout log terminal with level filters + search, task\n// parameter manifest, upstream/downstream lineage with schema table, and quality assertions.\n// Port of the React EtlNodeDetail block 1:1.\nimport { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, computed, signal } from '@angular/core'\nimport {\n  Check,\n  Code2,\n  Copy,\n  CornerDownRight,\n  GitBranch,\n  GitCommit,\n  LucideAngularModule,\n  Search,\n  ShieldCheck,\n  Terminal,\n  X,\n  type LucideIconData,\n} from 'lucide-angular'\nimport { cn } from '@/lib/utils'\nimport { UiBadgeComponent } from '@/ui/badge/badge.component'\nimport { UiButtonComponent } from '@/ui/button/button.component'\nimport { UiCardComponent, UiCardContentComponent, UiCardHeaderComponent } from '@/ui/card/card.component'\nimport {\n  UiTabsComponent,\n  UiTabsContentComponent,\n  UiTabsListComponent,\n  UiTabsTriggerComponent,\n} from '@/ui/tabs/tabs.component'\nimport type { DagNode, LogLine } from './etl-pipeline-dag-data'\n\nconst LOG_LEVELS = ['ALL', 'INFO', 'WARN', 'SQL', 'SUCCESS'] as const\nexport type LogLevelFilter = (typeof LOG_LEVELS)[number]\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-etl-node-detail, [ui-etl-node-detail]',\n  standalone: true,\n  // The block renders its own root card: keep this wrapper out of the layout.\n  host: { '[attr.class]': '\"contents\"' },\n  imports: [\n    LucideAngularModule,\n    UiBadgeComponent,\n    UiButtonComponent,\n    UiCardComponent,\n    UiCardContentComponent,\n    UiCardHeaderComponent,\n    UiTabsComponent,\n    UiTabsContentComponent,\n    UiTabsListComponent,\n    UiTabsTriggerComponent,\n  ],\n  template: `\n    <div ui-card data-slot=\"etl-node-detail\" class=\"border-border bg-card shadow-xs\">\n      <div ui-card-header class=\"pb-3\">\n        <div class=\"flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between\">\n          <!-- Selected Task Metadata -->\n          <div class=\"space-y-1.5\">\n            <div class=\"flex flex-wrap items-center gap-2\">\n              <div\n                class=\"border-primary/30 bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg border\"\n              >\n                <lucide-icon [img]=\"icon\" class=\"size-4\" />\n              </div>\n              <span class=\"text-foreground font-mono text-base font-semibold\">{{ node.name }}</span>\n              <span ui-badge [class]=\"cn('font-mono text-xs', categoryBadgeClass)\">{{ node.categoryLabel }}</span>\n              <span ui-badge variant=\"success\" class=\"gap-1 font-mono text-xs\">\n                <lucide-icon [img]=\"Check\" class=\"size-3\" />\n                Success\n              </span>\n            </div>\n\n            <!-- Task Telemetry Chips -->\n            <div class=\"text-muted-foreground flex flex-wrap items-center gap-2 font-mono text-xs\">\n              <span>Operator: {{ node.operator }}</span>\n              <span>•</span>\n              <span class=\"tabular-nums\">Duration: {{ node.duration }}</span>\n              <span>•</span>\n              <span class=\"tabular-nums\">Retries: {{ node.retries }}</span>\n              <span>•</span>\n              <span class=\"tabular-nums\">RAM: {{ node.memoryPeak }}</span>\n              <span>•</span>\n              <span class=\"tabular-nums\">CPU: {{ node.cpuPeak }}</span>\n            </div>\n          </div>\n\n          <!-- Log Controls & Filters -->\n          <div class=\"flex flex-wrap items-center gap-2\">\n            <!-- Search Logs Input -->\n            <div class=\"relative w-44 sm:w-56\">\n              <lucide-icon\n                [img]=\"Search\"\n                class=\"text-muted-foreground absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2\"\n              />\n              <input\n                [value]=\"logSearchQuery()\"\n                (input)=\"logSearchQuery.set($any($event.target).value)\"\n                type=\"text\"\n                placeholder=\"Filter logs...\"\n                class=\"border-border bg-muted/40 text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 h-8 w-full rounded-md border pr-7 pl-8 text-xs focus-visible:ring-[2px] focus-visible:outline-none\"\n              />\n              @if (logSearchQuery()) {\n                <button\n                  type=\"button\"\n                  class=\"text-muted-foreground hover:text-foreground absolute top-1/2 right-2 -translate-y-1/2\"\n                  aria-label=\"Clear log search\"\n                  (click)=\"logSearchQuery.set('')\"\n                >\n                  <lucide-icon [img]=\"X\" class=\"size-3.5\" />\n                </button>\n              }\n            </div>\n\n            <!-- Wrap Toggle -->\n            <button ui-button variant=\"outline\" size=\"sm\" class=\"h-8 gap-1 text-xs\" (click)=\"toggleWrap()\">\n              <span>{{ isWrapped() ? 'Unwrap Lines' : 'Wrap Lines' }}</span>\n            </button>\n\n            <!-- Copy Logs Button -->\n            <button\n              ui-button\n              variant=\"outline\"\n              size=\"sm\"\n              class=\"h-8 gap-1.5 text-xs font-medium\"\n              [disabled]=\"copiedLogs()\"\n              (click)=\"copyNodeLogs()\"\n            >\n              @if (copiedLogs()) {\n                <lucide-icon [img]=\"Check\" class=\"size-3.5\" />\n              } @else {\n                <lucide-icon [img]=\"Copy\" class=\"size-3.5\" />\n              }\n              <span>{{ copiedLogs() ? 'Logs Copied' : 'Copy Logs' }}</span>\n            </button>\n          </div>\n        </div>\n      </div>\n\n      <div ui-card-content class=\"p-6 pt-1\">\n        <ui-tabs [value]=\"activeTab()\" (valueChange)=\"activeTab.set($event)\" class=\"w-full\">\n          <ui-tabs-list class=\"mb-4\">\n            <button ui-tabs-trigger value=\"logs\" class=\"gap-1.5 font-mono text-xs\">\n              <lucide-icon [img]=\"Terminal\" class=\"size-3.5\" />\n              <span>Stdout Logs ({{ node.logs.length }})</span>\n            </button>\n            <button ui-tabs-trigger value=\"params\" class=\"gap-1.5 font-mono text-xs\">\n              <lucide-icon [img]=\"Code2\" class=\"size-3.5\" />\n              <span>Task Parameters</span>\n            </button>\n            <button ui-tabs-trigger value=\"lineage\" class=\"gap-1.5 font-mono text-xs\">\n              <lucide-icon [img]=\"GitBranch\" class=\"size-3.5\" />\n              <span>Lineage & Schema</span>\n            </button>\n            <button ui-tabs-trigger value=\"quality\" class=\"gap-1.5 font-mono text-xs\">\n              <lucide-icon [img]=\"ShieldCheck\" class=\"size-3.5\" />\n              <span>Quality Assertions ({{ node.qualityChecks.length }})</span>\n            </button>\n          </ui-tabs-list>\n\n          <!-- Tab 1: Stdout Execution Logs -->\n          <ui-tabs-content value=\"logs\" class=\"space-y-2\">\n            <!-- Filter Bar for Log Levels -->\n            <div class=\"flex flex-wrap items-center justify-between gap-2\">\n              <div class=\"flex items-center gap-1\">\n                <span class=\"text-muted-foreground mr-1 text-xs font-medium\">Level:</span>\n                @for (lvl of logLevels; track lvl) {\n                  <button type=\"button\" [class]=\"levelButtonClass(lvl)\" (click)=\"selectedLogLevel.set(lvl)\">\n                    {{ lvl }}\n                  </button>\n                }\n              </div>\n\n              <div class=\"text-muted-foreground font-mono text-xs\">\n                Showing {{ filteredLogs().length }} of {{ node.logs.length }} lines\n              </div>\n            </div>\n\n            <!-- Dark Terminal Window Container -->\n            <div class=\"overflow-hidden rounded-xl border border-zinc-800 bg-zinc-950 text-zinc-100 shadow-md\">\n              <!-- Terminal Header Bar -->\n              <div class=\"flex items-center justify-between border-b border-zinc-800 bg-zinc-900/90 px-4 py-2 text-xs\">\n                <div class=\"flex items-center gap-2\">\n                  <div class=\"flex items-center gap-1.5\">\n                    <span class=\"bg-destructive/80 inline-block size-2.5 rounded-full\"></span>\n                    <span class=\"bg-warning/80 inline-block size-2.5 rounded-full\"></span>\n                    <span class=\"bg-success/80 inline-block size-2.5 rounded-full\"></span>\n                  </div>\n                  <span class=\"text-muted-foreground font-mono text-xs\">stdout · {{ node.operator }}</span>\n                </div>\n\n                <div class=\"text-muted-foreground flex items-center gap-2 font-mono text-xs\">\n                  <span>host: runner-us-east-1a</span>\n                  <span>·</span>\n                  <span>exit: 0 (success)</span>\n                </div>\n              </div>\n\n              <!-- Terminal Body Viewport -->\n              <div class=\"max-h-[380px] overflow-y-auto p-4 font-mono text-xs leading-relaxed\">\n                @if (filteredLogs().length === 0) {\n                  <div class=\"text-muted-foreground py-8 text-center\">\n                    No log entries matching current search or level filters.\n                  </div>\n                } @else {\n                  @for (log of filteredLogs(); track $index) {\n                    <div [class]=\"logLineClass()\">\n                      <!-- Line Number -->\n                      <span class=\"mr-3 w-7 shrink-0 text-right text-zinc-600 tabular-nums select-none\">\n                        {{ $index + 1 }}\n                      </span>\n\n                      <!-- Timestamp -->\n                      <span class=\"text-success/90 mr-2 shrink-0 tabular-nums select-none\">[{{ log.timestamp }}]</span>\n\n                      <!-- Level Tag -->\n                      <span [class]=\"logLevelClass(log)\">[{{ log.level }}]</span>\n\n                      <!-- Source Tag -->\n                      <span class=\"text-muted-foreground mr-2 shrink-0 select-none\">[{{ log.source }}]</span>\n\n                      <!-- Message -->\n                      <span class=\"flex-1 text-zinc-200\">{{ log.message }}</span>\n                    </div>\n                  }\n                }\n              </div>\n            </div>\n          </ui-tabs-content>\n\n          <!-- Tab 2: Task Parameters & Config -->\n          <ui-tabs-content value=\"params\" class=\"space-y-4\">\n            <div class=\"border-border bg-card overflow-hidden rounded-xl border\">\n              <div class=\"border-border bg-muted/40 px-4 py-3 text-xs font-semibold\">\n                Task Configuration Manifest ({{ node.operator }})\n              </div>\n              <div class=\"divide-border divide-y text-xs\">\n                @for (entry of configEntries(); track entry[0]) {\n                  <div class=\"flex flex-col gap-1 p-3.5 sm:flex-row sm:items-center sm:justify-between\">\n                    <span class=\"text-muted-foreground font-mono font-medium sm:w-1/3\">{{ entry[0] }}</span>\n                    <span\n                      class=\"text-foreground border-border bg-muted/30 rounded border px-2 py-1 font-mono break-all sm:w-2/3\"\n                    >\n                      {{ entry[1] }}\n                    </span>\n                  </div>\n                }\n              </div>\n            </div>\n          </ui-tabs-content>\n\n          <!-- Tab 3: Lineage & Schema -->\n          <ui-tabs-content value=\"lineage\" class=\"space-y-4\">\n            <div class=\"grid grid-cols-1 gap-4 lg:grid-cols-2\">\n              <!-- Upstream Dependencies -->\n              <div class=\"border-border bg-card rounded-xl border p-4\">\n                <div class=\"text-foreground flex items-center gap-2 text-xs font-semibold\">\n                  <lucide-icon [img]=\"GitCommit\" class=\"text-muted-foreground size-3.5\" />\n                  <span>Upstream Dependencies ({{ node.upstream.length }})</span>\n                </div>\n                @if (node.upstream.length === 0) {\n                  <div class=\"text-muted-foreground mt-3 text-xs\">\n                    Root extraction node. No upstream parent dependencies.\n                  </div>\n                } @else {\n                  <div class=\"mt-3 space-y-2\">\n                    @for (up of node.upstream; track up) {\n                      <div\n                        role=\"button\"\n                        tabindex=\"0\"\n                        class=\"border-border bg-muted/20 hover:border-primary/40 focus-visible:ring-ring flex cursor-pointer items-center justify-between rounded-lg border p-2.5 transition-colors focus-visible:ring-2 focus-visible:outline-none\"\n                        (click)=\"selectNode.emit(up)\"\n                        (keydown.enter)=\"selectNode.emit(up)\"\n                        (keydown.space)=\"selectNode.emit(up); $event.preventDefault()\"\n                      >\n                        <span class=\"text-foreground font-mono text-xs font-medium\">{{ up }}</span>\n                        <span ui-badge variant=\"success\" class=\"text-xs\">Resolved</span>\n                      </div>\n                    }\n                  </div>\n                }\n              </div>\n\n              <!-- Downstream Dependents -->\n              <div class=\"border-border bg-card rounded-xl border p-4\">\n                <div class=\"text-foreground flex items-center gap-2 text-xs font-semibold\">\n                  <lucide-icon [img]=\"CornerDownRight\" class=\"text-muted-foreground size-3.5\" />\n                  <span>Downstream Consumers ({{ node.downstream.length }})</span>\n                </div>\n                @if (node.downstream.length === 0) {\n                  <div class=\"text-muted-foreground mt-3 text-xs\">\n                    Terminal node. Emits finalized reporting dataset.\n                  </div>\n                } @else {\n                  <div class=\"mt-3 space-y-2\">\n                    @for (down of node.downstream; track down) {\n                      <div\n                        role=\"button\"\n                        tabindex=\"0\"\n                        class=\"border-border bg-muted/20 hover:border-primary/40 focus-visible:ring-ring flex cursor-pointer items-center justify-between rounded-lg border p-2.5 transition-colors focus-visible:ring-2 focus-visible:outline-none\"\n                        (click)=\"selectNode.emit(down)\"\n                        (keydown.enter)=\"selectNode.emit(down)\"\n                        (keydown.space)=\"selectNode.emit(down); $event.preventDefault()\"\n                      >\n                        <span class=\"text-foreground font-mono text-xs font-medium\">{{ down }}</span>\n                        <span ui-badge variant=\"outline\" class=\"text-xs\">Triggered</span>\n                      </div>\n                    }\n                  </div>\n                }\n              </div>\n            </div>\n\n            <!-- Target Schema Column Table -->\n            <div class=\"border-border bg-card overflow-hidden rounded-xl border\">\n              <div class=\"border-border bg-muted/40 px-4 py-3 text-xs font-semibold\">\n                Target Dataset Schema ({{ node.target }})\n              </div>\n              <div class=\"overflow-x-auto\">\n                <table class=\"w-full text-left text-xs\">\n                  <thead class=\"border-border bg-muted/20 text-muted-foreground border-b font-mono font-medium\">\n                    <tr>\n                      <th class=\"px-4 py-2.5\">Column Name</th>\n                      <th class=\"px-4 py-2.5\">Data Type</th>\n                      <th class=\"px-4 py-2.5\">Nullable</th>\n                      <th class=\"px-4 py-2.5\">Description</th>\n                    </tr>\n                  </thead>\n                  <tbody class=\"divide-border divide-y font-mono\">\n                    @for (col of node.schemaColumns; track col.name) {\n                      <tr class=\"hover:bg-muted/30\">\n                        <td class=\"text-foreground px-4 py-2.5 font-semibold\">{{ col.name }}</td>\n                        <td class=\"text-info px-4 py-2.5\">{{ col.type }}</td>\n                        <td class=\"text-muted-foreground px-4 py-2.5\">{{ col.nullable ? 'YES' : 'NO' }}</td>\n                        <td class=\"text-muted-foreground px-4 py-2.5 font-sans\">{{ col.description }}</td>\n                      </tr>\n                    }\n                  </tbody>\n                </table>\n              </div>\n            </div>\n          </ui-tabs-content>\n\n          <!-- Tab 4: Quality Assertions -->\n          <ui-tabs-content value=\"quality\" class=\"space-y-4\">\n            <div class=\"border-border bg-card overflow-hidden rounded-xl border\">\n              <div class=\"border-border bg-muted/40 flex items-center justify-between px-4 py-3 text-xs font-semibold\">\n                <span>Great Expectations / Assertion Test Suite</span>\n                <span class=\"text-success font-mono\"\n                  >{{ passedChecks() }} / {{ node.qualityChecks.length }} PASSED</span\n                >\n              </div>\n              <div class=\"divide-border divide-y text-xs\">\n                @for (check of node.qualityChecks; track check.id) {\n                  <div class=\"flex flex-col gap-2 p-4 sm:flex-row sm:items-center sm:justify-between\">\n                    <div class=\"space-y-1\">\n                      <div class=\"flex items-center gap-2\">\n                        <span ui-badge variant=\"success\" class=\"text-xs\">PASSED</span>\n                        <span class=\"text-foreground font-mono font-semibold\">{{ check.assertion }}</span>\n                      </div>\n                      <p class=\"text-muted-foreground text-xs\">\n                        Target column: <code class=\"text-foreground font-mono\">{{ check.column }}</code>\n                      </p>\n                    </div>\n\n                    <div class=\"text-muted-foreground flex items-center gap-4 font-mono text-xs\">\n                      <div>\n                        <span class=\"text-muted-foreground/80\">Observed:</span>\n                        <span class=\"text-foreground ml-1 font-semibold\">{{ check.observed }}</span>\n                      </div>\n                      <div>\n                        <span class=\"text-muted-foreground/80\">Threshold:</span>\n                        <span class=\"text-muted-foreground ml-1\">{{ check.threshold }}</span>\n                      </div>\n                    </div>\n                  </div>\n                }\n              </div>\n            </div>\n          </ui-tabs-content>\n        </ui-tabs>\n      </div>\n    </div>\n  `,\n})\nexport class UiEtlNodeDetailComponent {\n  protected readonly Check = Check\n  protected readonly Code2 = Code2\n  protected readonly Copy = Copy\n  protected readonly CornerDownRight = CornerDownRight\n  protected readonly GitBranch = GitBranch\n  protected readonly GitCommit = GitCommit\n  protected readonly Search = Search\n  protected readonly ShieldCheck = ShieldCheck\n  protected readonly Terminal = Terminal\n  protected readonly X = X\n  protected readonly cn = cn\n  protected readonly logLevels = LOG_LEVELS\n\n  @Input({ required: true }) set node(v: DagNode) {\n    this.nodeState.set(v)\n  }\n  get node(): DagNode {\n    return this.nodeState()!\n  }\n  @Input() categoryBadgeClass = ''\n  @Input() icon: LucideIconData | null = null\n  @Output() selectNode = new EventEmitter<string>()\n\n  readonly nodeState = signal<DagNode | null>(null)\n\n  readonly activeTab = signal('logs')\n  readonly logSearchQuery = signal('')\n  readonly selectedLogLevel = signal<LogLevelFilter>('ALL')\n  readonly isWrapped = signal(false)\n  readonly copiedLogs = signal(false)\n  private copyTimer: ReturnType<typeof setTimeout> | undefined\n\n  readonly filteredLogs = computed<LogLine[]>(() => {\n    const node = this.nodeState()\n    if (!node) return []\n    const query = this.logSearchQuery().trim().toLowerCase()\n    const level = this.selectedLogLevel()\n    return node.logs.filter((log) => {\n      const matchesLevel = level === 'ALL' || log.level === level\n      const matchesQuery =\n        query === '' ||\n        log.message.toLowerCase().includes(query) ||\n        log.source.toLowerCase().includes(query) ||\n        log.timestamp.toLowerCase().includes(query)\n      return matchesLevel && matchesQuery\n    })\n  })\n\n  readonly passedChecks = computed(\n    () => (this.nodeState()?.qualityChecks ?? []).filter((c) => c.status === 'passed').length,\n  )\n\n  readonly configEntries = computed<[string, string][]>(() => Object.entries(this.nodeState()?.configParams ?? {}))\n\n  levelButtonClass(lvl: LogLevelFilter): string {\n    return cn(\n      'min-h-6 rounded-md px-2 py-0.5 font-mono text-xs transition-colors',\n      this.selectedLogLevel() === lvl\n        ? 'bg-primary text-primary-foreground font-semibold'\n        : 'border-border bg-muted/30 text-muted-foreground hover:text-foreground border',\n    )\n  }\n\n  logLineClass(): string {\n    return cn(\n      '-mx-1 flex items-start rounded px-1 py-0.5 hover:bg-zinc-900/60',\n      this.isWrapped() ? 'break-all whitespace-pre-wrap' : 'overflow-x-auto whitespace-pre',\n    )\n  }\n\n  logLevelClass(log: LogLine): string {\n    return cn(\n      'mr-2 shrink-0 font-semibold',\n      log.level === 'INFO' && 'text-info',\n      log.level === 'WARN' && 'text-warning',\n      log.level === 'SQL' && 'text-chart-1',\n      log.level === 'SUCCESS' && 'text-success font-bold',\n      log.level === 'ERROR' && 'text-destructive font-bold',\n    )\n  }\n\n  toggleWrap(): void {\n    this.isWrapped.update((v) => !v)\n  }\n\n  async copyNodeLogs(): Promise<void> {\n    const node = this.nodeState()\n    if (!node) return\n    const text = node.logs.map((l) => `[${l.timestamp}] [${l.level}] [${l.source}] ${l.message}`).join('\\n')\n    try {\n      await navigator.clipboard.writeText(text)\n      this.copiedLogs.set(true)\n      clearTimeout(this.copyTimer)\n      this.copyTimer = setTimeout(() => this.copiedLogs.set(false), 2000)\n    } catch {\n      // Clipboard fallback\n    }\n  }\n}\n",
      "type": "registry:page",
      "target": "~/src/app/components/blocks/etl-pipeline-dag/etl-node-detail.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/etl-pipeline-dag/etl-pipeline-dag-data.ts",
      "content": "// Shared DAG types + sample pipeline telemetry. Port of the React block data 1:1:\n// same six tasks, operators, durations, quality checks, schemas, and stdout logs.\nexport type NodeStatus = 'success' | 'running' | 'failed' | 'queued'\nexport type NodeCategory = 'ingest' | 'transform' | 'validate' | 'load' | 'model'\n\nexport interface QualityCheck {\n  id: string\n  assertion: string\n  column: string\n  status: 'passed' | 'warning' | 'failed'\n  observed: string\n  threshold: string\n}\n\nexport interface SchemaColumn {\n  name: string\n  type: string\n  nullable: boolean\n  description: string\n}\n\nexport interface LogLine {\n  timestamp: string\n  level: 'INFO' | 'WARN' | 'SQL' | 'SUCCESS' | 'ERROR'\n  source: string\n  message: string\n}\n\nexport interface DagNode {\n  id: string\n  name: string\n  category: NodeCategory\n  categoryLabel: string\n  stageNumber: number\n  stageTitle: string\n  operator: string\n  status: NodeStatus\n  duration: string\n  durationSec: number\n  records: string\n  bytes: string\n  startedAt: string\n  finishedAt: string\n  target: string\n  engine: string\n  upstream: string[]\n  downstream: string[]\n  retries: string\n  memoryPeak: string\n  cpuPeak: string\n  configParams: Record<string, string>\n  qualityChecks: QualityCheck[]\n  schemaColumns: SchemaColumn[]\n  logs: LogLine[]\n}\n\nexport const pipelineNodes: DagNode[] = [\n  {\n    id: 'extract_stripe_charges',\n    name: 'extract_stripe_charges',\n    category: 'ingest',\n    categoryLabel: 'INGEST',\n    stageNumber: 1,\n    stageTitle: 'Extract & Ingest',\n    operator: 'StripeApiConnector',\n    status: 'success',\n    duration: '18s',\n    durationSec: 18.2,\n    records: '148,290 records',\n    bytes: '42.6 MB',\n    startedAt: '02:00:02 UTC',\n    finishedAt: '02:00:20 UTC',\n    target: 'raw_stripe.charges',\n    engine: 'Airbyte / Stripe REST API v2024-06',\n    upstream: [],\n    downstream: ['transform_normalize_fx'],\n    retries: '0 / 3',\n    memoryPeak: '342 MB',\n    cpuPeak: '18%',\n    configParams: {\n      endpoint: 'https://api.stripe.com/v1/charges',\n      batch_size: '10,000',\n      pagination_mode: 'starting_after cursor',\n      target_s3_bucket: 's3://lakehouse-raw/stripe/charges/dt=2026-02-21/',\n      compression_codec: 'snappy (parquet)',\n      iam_role_arn: 'arn:aws:iam::12498214:role/etl-stripe-ingestion',\n      secret_vault_ref: 'vault://production/credentials/stripe_api_key',\n    },\n    qualityChecks: [\n      {\n        id: 'q1',\n        assertion: 'HTTP 200 Response Ratio',\n        column: 'api_response',\n        status: 'passed',\n        observed: '100% (15/15 chunks)',\n        threshold: '100%',\n      },\n      {\n        id: 'q2',\n        assertion: 'Payload Schema Checksum',\n        column: 'raw_payload',\n        status: 'passed',\n        observed: 'sha256:8f4c91...',\n        threshold: 'Valid JSON',\n      },\n      {\n        id: 'q3',\n        assertion: 'Record Count Lower Bound',\n        column: 'id',\n        status: 'passed',\n        observed: '148,290 records',\n        threshold: '> 50,000',\n      },\n    ],\n    schemaColumns: [\n      {\n        name: 'id',\n        type: 'VARCHAR(64)',\n        nullable: false,\n        description: 'Stripe unique charge identifier (ch_xxx)',\n      },\n      {\n        name: 'amount',\n        type: 'BIGINT',\n        nullable: false,\n        description: 'Charge amount in smallest currency unit (cents)',\n      },\n      {\n        name: 'currency',\n        type: 'VARCHAR(3)',\n        nullable: false,\n        description: 'Three-letter ISO currency code (USD, EUR, GBP)',\n      },\n      {\n        name: 'customer_id',\n        type: 'VARCHAR(64)',\n        nullable: true,\n        description: 'Associated customer reference (cus_xxx)',\n      },\n      {\n        name: 'status',\n        type: 'VARCHAR(32)',\n        nullable: false,\n        description: 'Charge status (succeeded, pending, failed)',\n      },\n      {\n        name: 'created_at',\n        type: 'TIMESTAMP_TZ',\n        nullable: false,\n        description: 'Stripe transaction creation epoch timestamp',\n      },\n    ],\n    logs: [\n      {\n        timestamp: '02:00:02.104',\n        level: 'INFO',\n        source: 'worker_4',\n        message: 'Initializing Stripe extraction worker on task runner runner-us-east-1a',\n      },\n      {\n        timestamp: '02:00:02.482',\n        level: 'INFO',\n        source: 'auth',\n        message: 'Authenticated with KMS secret token: sec_live_stripe_vault_****',\n      },\n      {\n        timestamp: '02:00:03.119',\n        level: 'INFO',\n        source: 'query',\n        message: 'Requesting charges filter: created >= 1708473600 (2026-02-21 00:00:00 UTC)',\n      },\n      {\n        timestamp: '02:00:05.892',\n        level: 'INFO',\n        source: 'stream',\n        message: 'Chunk 1/15 fetched: 10,000 records (HTTP 200 OK - 2.4s latency)',\n      },\n      {\n        timestamp: '02:00:09.140',\n        level: 'INFO',\n        source: 'stream',\n        message: 'Chunk 5/15 fetched: 50,000 records (HTTP 200 OK - 1.8s latency)',\n      },\n      {\n        timestamp: '02:00:14.730',\n        level: 'INFO',\n        source: 'stream',\n        message: 'Chunk 11/15 fetched: 110,000 records (HTTP 200 OK - 1.9s latency)',\n      },\n      {\n        timestamp: '02:00:18.915',\n        level: 'INFO',\n        source: 'stream',\n        message: 'Chunk 15/15 fetched: 148,290 records (HTTP 200 OK - 1.1s latency)',\n      },\n      {\n        timestamp: '02:00:19.420',\n        level: 'INFO',\n        source: 'parquet',\n        message: 'Staged parquet buffer to s3://lakehouse-raw/stripe/charges/dt=2026-02-21/part-001.parquet',\n      },\n      {\n        timestamp: '02:00:20.312',\n        level: 'SUCCESS',\n        source: 'task',\n        message: 'Task extract_stripe_charges completed successfully in 18.208s. Checksum verified: sha256:8f4c91',\n      },\n    ],\n  },\n  {\n    id: 'extract_currency_rates',\n    name: 'extract_currency_rates',\n    category: 'ingest',\n    categoryLabel: 'INGEST',\n    stageNumber: 1,\n    stageTitle: 'Extract & Ingest',\n    operator: 'EcbRatesConnector',\n    status: 'success',\n    duration: '4s',\n    durationSec: 4.1,\n    records: '34 FX pairs',\n    bytes: '128 KB',\n    startedAt: '02:00:02 UTC',\n    finishedAt: '02:00:06 UTC',\n    target: 'raw_currency.rates',\n    engine: 'ECB SDMX API / FastHTTP',\n    upstream: [],\n    downstream: ['transform_normalize_fx'],\n    retries: '0 / 3',\n    memoryPeak: '84 MB',\n    cpuPeak: '6%',\n    configParams: {\n      endpoint: 'https://data-api.ecb.europa.eu/service/data/EXR/D..EUR.SP00.A',\n      benchmark_currency: 'USD',\n      feed_type: 'Daily Official Reference Spot',\n      output_format: 'JSON / Arrow Table',\n      target_s3_bucket: 's3://lakehouse-raw/rates/dt=2026-02-21/',\n    },\n    qualityChecks: [\n      {\n        id: 'q1',\n        assertion: 'Base Currency Coverage',\n        column: 'currency_code',\n        status: 'passed',\n        observed: '34/34 major currencies',\n        threshold: '>= 30',\n      },\n      {\n        id: 'q2',\n        assertion: 'EUR/USD Rate Sanity Check',\n        column: 'spot_rate',\n        status: 'passed',\n        observed: '1.0842 USD',\n        threshold: '0.80 - 1.40',\n      },\n      {\n        id: 'q3',\n        assertion: 'Zero Value Absence',\n        column: 'rate_multiplier',\n        status: 'passed',\n        observed: '0 non-zero values',\n        threshold: '0 zeros',\n      },\n    ],\n    schemaColumns: [\n      {\n        name: 'currency_code',\n        type: 'VARCHAR(3)',\n        nullable: false,\n        description: 'Three letter ISO code (EUR, GBP, JPY, CAD)',\n      },\n      {\n        name: 'rate_to_usd',\n        type: 'DECIMAL(12, 6)',\n        nullable: false,\n        description: 'Standardized multiplier to convert 1 unit to USD',\n      },\n      {\n        name: 'published_date',\n        type: 'DATE',\n        nullable: false,\n        description: 'Official publication date by Central Bank',\n      },\n      {\n        name: 'ingested_at',\n        type: 'TIMESTAMP_TZ',\n        nullable: false,\n        description: 'Ingestion pipeline timestamp',\n      },\n    ],\n    logs: [\n      {\n        timestamp: '02:00:02.108',\n        level: 'INFO',\n        source: 'worker_1',\n        message: 'Starting currency spot exchange rate ingestion from European Central Bank API',\n      },\n      {\n        timestamp: '02:00:02.740',\n        level: 'INFO',\n        source: 'http',\n        message: 'GET https://data-api.ecb.europa.eu/service/data/EXR/D..EUR.SP00.A?startPeriod=2026-02-20',\n      },\n      {\n        timestamp: '02:00:03.921',\n        level: 'INFO',\n        source: 'parser',\n        message: 'SDMX-ML XML payload decoded (34 reference base currencies parsed)',\n      },\n      {\n        timestamp: '02:00:04.450',\n        level: 'INFO',\n        source: 'transform',\n        message:\n          'Inverted base currency rates against USD benchmark (EUR/USD: 1.0842, GBP/USD: 1.2610, JPY/USD: 154.20)',\n      },\n      {\n        timestamp: '02:00:05.620',\n        level: 'INFO',\n        source: 's3',\n        message: 'Writing FX snapshots to s3://lakehouse-raw/rates/fx_daily_20260221.json',\n      },\n      {\n        timestamp: '02:00:06.210',\n        level: 'SUCCESS',\n        source: 'task',\n        message: 'Task extract_currency_rates completed in 4.102s. 34/34 currency pairs ingested.',\n      },\n    ],\n  },\n  {\n    id: 'transform_normalize_fx',\n    name: 'transform_normalize_fx',\n    category: 'transform',\n    categoryLabel: 'TRANSFORM',\n    stageNumber: 2,\n    stageTitle: 'Transform & Normalize',\n    operator: 'PySparkBatchOperator',\n    status: 'success',\n    duration: '42s',\n    durationSec: 42.6,\n    records: '148,290 rows',\n    bytes: '58.2 MB',\n    startedAt: '02:00:21 UTC',\n    finishedAt: '02:01:03 UTC',\n    target: 'stg_payments_fx',\n    engine: 'PySpark 3.5.1 / Apache Arrow 15.0',\n    upstream: ['extract_stripe_charges', 'extract_currency_rates'],\n    downstream: ['validate_schema_anomalies'],\n    retries: '0 / 3',\n    memoryPeak: '1,280 MB',\n    cpuPeak: '74%',\n    configParams: {\n      spark_driver_memory: '4g',\n      spark_executor_memory: '8g',\n      executor_instances: '4',\n      join_strategy: 'BroadcastHashJoin(stripe.currency = fx.currency_code)',\n      precision_mode: 'DECIMAL(18, 4)',\n      output_staging_path: 's3://lakehouse-stage/payments_fx/dt=2026-02-21/',\n    },\n    qualityChecks: [\n      {\n        id: 'q1',\n        assertion: 'FX Broadcast Join Match Rate',\n        column: 'currency',\n        status: 'passed',\n        observed: '100% matched (0 orphaned FX)',\n        threshold: '100%',\n      },\n      {\n        id: 'q2',\n        assertion: 'Non-negative Amount USD',\n        column: 'amount_usd',\n        status: 'passed',\n        observed: '0 negative rows',\n        threshold: '0 negative',\n      },\n      {\n        id: 'q3',\n        assertion: 'Timestamp Timezone Normalization',\n        column: 'created_at_utc',\n        status: 'passed',\n        observed: 'All UTC ISO-8601',\n        threshold: 'Valid UTC',\n      },\n    ],\n    schemaColumns: [\n      {\n        name: 'charge_id',\n        type: 'VARCHAR(64)',\n        nullable: false,\n        description: 'Normalized unique charge identifier',\n      },\n      {\n        name: 'customer_id',\n        type: 'VARCHAR(64)',\n        nullable: true,\n        description: 'Customer identifier reference',\n      },\n      {\n        name: 'original_amount',\n        type: 'DECIMAL(18,2)',\n        nullable: false,\n        description: 'Amount in transaction original currency',\n      },\n      {\n        name: 'original_currency',\n        type: 'VARCHAR(3)',\n        nullable: false,\n        description: 'Original ISO transaction currency',\n      },\n      {\n        name: 'fx_rate_applied',\n        type: 'DECIMAL(12,6)',\n        nullable: false,\n        description: 'Exchange rate multiplier to USD',\n      },\n      {\n        name: 'amount_usd',\n        type: 'DECIMAL(18,4)',\n        nullable: false,\n        description: 'Standardized gross revenue in USD',\n      },\n      {\n        name: 'net_interchange_fee_usd',\n        type: 'DECIMAL(18,4)',\n        nullable: false,\n        description: 'Estimated payment processing fees in USD',\n      },\n      {\n        name: 'created_at_utc',\n        type: 'TIMESTAMP_TZ',\n        nullable: false,\n        description: 'UTC converted timestamp',\n      },\n    ],\n    logs: [\n      {\n        timestamp: '02:00:21.050',\n        level: 'INFO',\n        source: 'spark',\n        message: 'Initialized PySpark cluster (executor_count: 4, cores_per_exec: 4, memory: 8GB)',\n      },\n      {\n        timestamp: '02:00:22.410',\n        level: 'INFO',\n        source: 'upstream',\n        message: 'Joined dependencies: extract_stripe_charges (OK), extract_currency_rates (OK)',\n      },\n      {\n        timestamp: '02:00:24.980',\n        level: 'INFO',\n        source: 'dag.plan',\n        message: 'Logical plan: BroadcastHashJoin(stripe_charges.currency = fx_rates.currency_code)',\n      },\n      {\n        timestamp: '02:00:30.120',\n        level: 'INFO',\n        source: 'spark.exec',\n        message: 'Partition 0-7 completed: Normalized cents to standardized float amounts',\n      },\n      {\n        timestamp: '02:00:37.450',\n        level: 'INFO',\n        source: 'spark.exec',\n        message: 'Calculated converted_amount_usd, net_interchange_fee_usd, and tax_withheld_usd',\n      },\n      {\n        timestamp: '02:00:48.890',\n        level: 'INFO',\n        source: 'spark.exec',\n        message: 'Partition 8-15 completed: Applied ISO-8601 UTC timestamp timezone adjustments',\n      },\n      {\n        timestamp: '02:00:59.320',\n        level: 'INFO',\n        source: 'io.write',\n        message: 'Emitted staging dataset to s3://lakehouse-stage/payments_fx/dt=2026-02-21/ (148,290 records)',\n      },\n      {\n        timestamp: '02:01:03.650',\n        level: 'SUCCESS',\n        source: 'task',\n        message: 'Task transform_normalize_fx finished in 42.600s. 0 join drops, 100% matched FX rates.',\n      },\n    ],\n  },\n  {\n    id: 'validate_schema_anomalies',\n    name: 'validate_schema_anomalies',\n    category: 'validate',\n    categoryLabel: 'VALIDATE',\n    stageNumber: 3,\n    stageTitle: 'Quality & Audit',\n    operator: 'GreatExpectationsOperator',\n    status: 'success',\n    duration: '14s',\n    durationSec: 14.3,\n    records: '18/18 checks',\n    bytes: '1.2 MB',\n    startedAt: '02:01:04 UTC',\n    finishedAt: '02:01:18 UTC',\n    target: 'anomalies_audit',\n    engine: 'Great Expectations 0.18 + Monte Carlo',\n    upstream: ['transform_normalize_fx'],\n    downstream: ['load_snowflake_warehouse'],\n    retries: '0 / 3',\n    memoryPeak: '412 MB',\n    cpuPeak: '28%',\n    configParams: {\n      suite_name: 'suite_stripe_payments_strict_v3',\n      assertion_count: '18',\n      anomaly_detection_model: 'Z-Score 3-Sigma Rolling Window (30-day baseline)',\n      halt_pipeline_on_error: 'True',\n      alerting_webhook: 'slack://#data-pipeline-alerts',\n    },\n    qualityChecks: [\n      {\n        id: 'q1',\n        assertion: 'expect_column_values_to_not_be_null',\n        column: 'charge_id',\n        status: 'passed',\n        observed: '148,290 / 148,290 (100%)',\n        threshold: '100% non-null',\n      },\n      {\n        id: 'q2',\n        assertion: 'expect_column_values_to_be_unique',\n        column: 'charge_id',\n        status: 'passed',\n        observed: '0 duplicate keys',\n        threshold: '0 duplicates',\n      },\n      {\n        id: 'q3',\n        assertion: 'expect_column_values_to_be_in_set',\n        column: 'status',\n        status: 'passed',\n        observed: 'succeeded, failed, refunded',\n        threshold: 'Allowed set',\n      },\n      {\n        id: 'q4',\n        assertion: 'expect_column_values_to_be_between',\n        column: 'amount_usd',\n        status: 'passed',\n        observed: '$0.50 - $24,900.00',\n        threshold: '$0.50 - $250k',\n      },\n      {\n        id: 'q5',\n        assertion: 'expect_table_row_count_to_be_between',\n        column: '*',\n        status: 'passed',\n        observed: '148,290 rows',\n        threshold: '100,000 - 200,000',\n      },\n      {\n        id: 'q6',\n        assertion: 'expect_volume_drift_z_score_within_bound',\n        column: 'daily_volume',\n        status: 'passed',\n        observed: 'z = +0.42 (Normal)',\n        threshold: '|z| < 3.0',\n      },\n    ],\n    schemaColumns: [\n      {\n        name: 'test_id',\n        type: 'VARCHAR(64)',\n        nullable: false,\n        description: 'Unique assertion execution ID',\n      },\n      {\n        name: 'expectation_type',\n        type: 'VARCHAR(128)',\n        nullable: false,\n        description: 'Great Expectations rule name',\n      },\n      {\n        name: 'target_column',\n        type: 'VARCHAR(64)',\n        nullable: true,\n        description: 'Target column evaluated',\n      },\n      {\n        name: 'result_status',\n        type: 'VARCHAR(16)',\n        nullable: false,\n        description: 'PASS or FAIL status',\n      },\n      {\n        name: 'observed_metrics',\n        type: 'JSON',\n        nullable: false,\n        description: 'Computed profiling metrics summary',\n      },\n    ],\n    logs: [\n      {\n        timestamp: '02:01:04.110',\n        level: 'INFO',\n        source: 'gx.suite',\n        message: 'Loading expectation suite: suite_stripe_payments_strict_v3',\n      },\n      {\n        timestamp: '02:01:05.420',\n        level: 'INFO',\n        source: 'assertion',\n        message: '[1/18] expect_column_values_to_not_be_null(column=charge_id) -> PASSED (148,290/148,290 valid)',\n      },\n      {\n        timestamp: '02:01:07.190',\n        level: 'INFO',\n        source: 'assertion',\n        message: '[2/18] expect_column_values_to_be_unique(column=charge_id) -> PASSED (0 duplicate keys)',\n      },\n      {\n        timestamp: '02:01:09.650',\n        level: 'INFO',\n        source: 'assertion',\n        message: '[5/18] expect_column_values_to_be_in_set(column=status, set=[succeeded, failed, refunded]) -> PASSED',\n      },\n      {\n        timestamp: '02:01:12.330',\n        level: 'INFO',\n        source: 'assertion',\n        message:\n          '[11/18] expect_column_values_to_be_between(column=amount_usd, min_value=0.50, max_value=250000.00) -> PASSED',\n      },\n      {\n        timestamp: '02:01:15.840',\n        level: 'INFO',\n        source: 'assertion',\n        message:\n          '[18/18] expect_table_row_count_to_be_between(min_value=100000, max_value=200000) -> PASSED (148,290 rows)',\n      },\n      {\n        timestamp: '02:01:17.200',\n        level: 'INFO',\n        source: 'anomaly',\n        message:\n          'Statistical z-score drift test: Revenue mean +1.4% vs 30d rolling window (within normal range: ±3.0σ)',\n      },\n      {\n        timestamp: '02:01:18.410',\n        level: 'SUCCESS',\n        source: 'task',\n        message: 'Quality suite passed with 100% score (18/18 expectations verified, 0 anomalies detected).',\n      },\n    ],\n  },\n  {\n    id: 'load_snowflake_warehouse',\n    name: 'load_snowflake_warehouse',\n    category: 'load',\n    categoryLabel: 'LOAD',\n    stageNumber: 4,\n    stageTitle: 'Warehouse Load',\n    operator: 'SnowflakeMergeOperator',\n    status: 'success',\n    duration: '1m 20s',\n    durationSec: 80.0,\n    records: '148,290 rows',\n    bytes: '74.8 MB',\n    startedAt: '02:01:19 UTC',\n    finishedAt: '02:02:39 UTC',\n    target: 'fct_stripe_charges',\n    engine: 'Snowflake / TRANSFORMING_XL',\n    upstream: ['validate_schema_anomalies'],\n    downstream: ['refresh_dbt_analytics_marts'],\n    retries: '0 / 3',\n    memoryPeak: '520 MB',\n    cpuPeak: '15%',\n    configParams: {\n      database: 'PROD_ANALYTICS',\n      schema: 'PAYMENTS',\n      target_table: 'ANALYTICS.PAYMENTS.FCT_STRIPE_CHARGES',\n      warehouse_name: 'TRANSFORMING_XL (4X-Large)',\n      merge_primary_key: 'charge_id',\n      clustering_keys: '(created_at_utc::date, original_currency)',\n      auto_suspend_seconds: '60',\n    },\n    qualityChecks: [\n      {\n        id: 'q1',\n        assertion: 'Snowflake Transaction ACID Commit',\n        column: 'txn_id',\n        status: 'passed',\n        observed: 'Committed #TXN-89021',\n        threshold: 'Committed',\n      },\n      {\n        id: 'q2',\n        assertion: 'Merge Mutation Parity',\n        column: 'rows_merged',\n        status: 'passed',\n        observed: '142.4k ins / 5.8k upd',\n        threshold: '148,290 total',\n      },\n      {\n        id: 'q3',\n        assertion: 'Clustering Depth Efficiency',\n        column: 'clustering_depth',\n        status: 'passed',\n        observed: 'Average depth: 1.12',\n        threshold: '< 2.0',\n      },\n    ],\n    schemaColumns: [\n      {\n        name: 'charge_id',\n        type: 'VARCHAR(64)',\n        nullable: false,\n        description: 'Primary key charge hash',\n      },\n      {\n        name: 'customer_id',\n        type: 'VARCHAR(64)',\n        nullable: true,\n        description: 'Foreign key to DIM_CUSTOMERS',\n      },\n      {\n        name: 'amount_usd',\n        type: 'NUMBER(18,4)',\n        nullable: false,\n        description: 'Standardized gross charge amount in USD',\n      },\n      {\n        name: 'fee_usd',\n        type: 'NUMBER(18,4)',\n        nullable: false,\n        description: 'Interchange and payment gateway fees',\n      },\n      {\n        name: 'net_usd',\n        type: 'NUMBER(18,4)',\n        nullable: false,\n        description: 'Net deposited funds (amount_usd - fee_usd)',\n      },\n      {\n        name: 'status',\n        type: 'VARCHAR(32)',\n        nullable: false,\n        description: 'Current transactional settlement state',\n      },\n      {\n        name: 'created_at_utc',\n        type: 'TIMESTAMP_NTZ',\n        nullable: false,\n        description: 'Event timestamp',\n      },\n      {\n        name: '_dbt_loaded_at',\n        type: 'TIMESTAMP_NTZ',\n        nullable: false,\n        description: 'Warehouse load audit timestamp',\n      },\n    ],\n    logs: [\n      {\n        timestamp: '02:01:19.040',\n        level: 'INFO',\n        source: 'snowflake',\n        message: 'Establishing TLS 1.3 session to acct_finance.snowflakecomputing.com',\n      },\n      {\n        timestamp: '02:01:21.320',\n        level: 'INFO',\n        source: 'warehouse',\n        message: 'Resumed warehouse TRANSFORMING_XL (cluster_size: 4X-Large, auto_suspend: 60s)',\n      },\n      {\n        timestamp: '02:01:24.890',\n        level: 'SQL',\n        source: 'query',\n        message:\n          'MERGE INTO ANALYTICS.PAYMENTS.FCT_STRIPE_CHARGES AS target USING @STAGE_S3_PAYMENTS AS stage ON target.charge_id = stage.charge_id WHEN MATCHED THEN UPDATE SET target.status = stage.status WHEN NOT MATCHED THEN INSERT (charge_id, customer_id, amount_usd, fee_usd, net_usd, status, created_at_utc, _dbt_loaded_at) VALUES (stage.charge_id, stage.customer_id, stage.amount_usd, stage.net_interchange_fee_usd, stage.amount_usd - stage.net_interchange_fee_usd, stage.status, stage.created_at_utc, CURRENT_TIMESTAMP())',\n      },\n      {\n        timestamp: '02:01:45.120',\n        level: 'INFO',\n        source: 'pruner',\n        message: 'Partition pruning: 42 micro-partitions scanned out of 1,280 total (96.7% pruned)',\n      },\n      {\n        timestamp: '02:02:12.780',\n        level: 'INFO',\n        source: 'stats',\n        message: 'Merge stats: 142,400 rows inserted, 5,890 existing rows updated (disputes/refund status sync)',\n      },\n      {\n        timestamp: '02:02:32.410',\n        level: 'INFO',\n        source: 'cluster',\n        message: 'Automatic clustering key (created_at_utc::date, original_currency) re-balanced in background',\n      },\n      {\n        timestamp: '02:02:38.100',\n        level: 'INFO',\n        source: 'warehouse',\n        message: 'Auto-suspend timer armed for warehouse TRANSFORMING_XL',\n      },\n      {\n        timestamp: '02:02:39.050',\n        level: 'SUCCESS',\n        source: 'task',\n        message: 'Snowflake warehouse load completed successfully in 80.010s. Target table committed.',\n      },\n    ],\n  },\n  {\n    id: 'refresh_dbt_analytics_marts',\n    name: 'refresh_dbt_analytics_marts',\n    category: 'model',\n    categoryLabel: 'MODEL',\n    stageNumber: 5,\n    stageTitle: 'Analytics Marts',\n    operator: 'DbtCloudRunOperator',\n    status: 'success',\n    duration: '1m 40s',\n    durationSec: 100.0,\n    records: '12 models',\n    bytes: '112.4 MB',\n    startedAt: '02:02:40 UTC',\n    finishedAt: '02:04:20 UTC',\n    target: 'mart_finance_revenue',\n    engine: 'dbt Core 1.8.2 / Jinja',\n    upstream: ['load_snowflake_warehouse'],\n    downstream: [],\n    retries: '0 / 3',\n    memoryPeak: '890 MB',\n    cpuPeak: '62%',\n    configParams: {\n      dbt_project: 'bi_marts',\n      target_environment: 'prod_snowflake',\n      threads: '8',\n      models_selection: 'tag:finance_daily+ tag:revenue_marts+',\n      manifest_version: 'dbt-core v1.8.2 (schema v12)',\n      downstream_webhooks: 'Looker Cache Flush, Hex App Refresh, Metabase Sync',\n    },\n    qualityChecks: [\n      {\n        id: 'q1',\n        assertion: 'dbt Schema & Custom Tests',\n        column: 'all_marts',\n        status: 'passed',\n        observed: '48/48 tests passed (0 failures)',\n        threshold: '100% pass',\n      },\n      {\n        id: 'q2',\n        assertion: 'Incremental Model Freshness',\n        column: 'fct_daily_gross_revenue',\n        status: 'passed',\n        observed: 'Watermark: 2026-02-21 02:00 UTC',\n        threshold: '< 3h',\n      },\n      {\n        id: 'q3',\n        assertion: 'BI Semantic Layer Sync',\n        column: 'Looker/Hex',\n        status: 'passed',\n        observed: '3/3 endpoints acknowledged',\n        threshold: 'All ACK',\n      },\n    ],\n    schemaColumns: [\n      {\n        name: 'report_date',\n        type: 'DATE',\n        nullable: false,\n        description: 'Reporting dimension day',\n      },\n      {\n        name: 'gross_revenue_usd',\n        type: 'DECIMAL(20,2)',\n        nullable: false,\n        description: 'Total daily revenue converted to USD',\n      },\n      {\n        name: 'net_revenue_usd',\n        type: 'DECIMAL(20,2)',\n        nullable: false,\n        description: 'Gross revenue minus fees and refunds',\n      },\n      {\n        name: 'active_paying_customers',\n        type: 'INTEGER',\n        nullable: false,\n        description: 'Distinct customer count for the day',\n      },\n      {\n        name: 'dispute_rate_pct',\n        type: 'DECIMAL(6,4)',\n        nullable: false,\n        description: 'Daily payment dispute percentage',\n      },\n      {\n        name: 'mrr_impact_usd',\n        type: 'DECIMAL(18,2)',\n        nullable: false,\n        description: 'MRR movement delta attribution',\n      },\n    ],\n    logs: [\n      {\n        timestamp: '02:02:40.090',\n        level: 'INFO',\n        source: 'dbt',\n        message: 'Found 12 models, 48 tests, 6 snapshots, 4 semantic metrics in project bi_marts',\n      },\n      {\n        timestamp: '02:02:42.510',\n        level: 'INFO',\n        source: 'dbt',\n        message: 'Concurrency: 8 threads across target database PROD_ANALYTICS',\n      },\n      {\n        timestamp: '02:02:48.330',\n        level: 'INFO',\n        source: 'dbt',\n        message: '1 of 12 START incremental model marts.fct_daily_gross_revenue ................ [RUN]',\n      },\n      {\n        timestamp: '02:02:59.880',\n        level: 'INFO',\n        source: 'dbt',\n        message: '1 of 12 OK created incremental model marts.fct_daily_gross_revenue ........... [SUCCESS 11.55s]',\n      },\n      {\n        timestamp: '02:03:00.120',\n        level: 'INFO',\n        source: 'dbt',\n        message: '2 of 12 START table model marts.dim_customer_ltv .............................. [RUN]',\n      },\n      {\n        timestamp: '02:03:18.420',\n        level: 'INFO',\n        source: 'dbt',\n        message: '2 of 12 OK created table model marts.dim_customer_ltv ......................... [SUCCESS 18.30s]',\n      },\n      {\n        timestamp: '02:03:19.050',\n        level: 'INFO',\n        source: 'dbt',\n        message: '3 of 12 START view model marts.finance_mrr_arr_summary ....................... [RUN]',\n      },\n      {\n        timestamp: '02:03:26.700',\n        level: 'INFO',\n        source: 'dbt',\n        message: '3 of 12 OK created view model marts.finance_mrr_arr_summary ................... [SUCCESS 7.65s]',\n      },\n      {\n        timestamp: '02:03:27.100',\n        level: 'INFO',\n        source: 'dbt',\n        message: '4 of 12 START incremental model marts.fct_payment_dispute_rates .............. [RUN]',\n      },\n      {\n        timestamp: '02:03:45.920',\n        level: 'INFO',\n        source: 'dbt',\n        message: '4 of 12 OK created incremental model marts.fct_payment_dispute_rates ........ [SUCCESS 18.82s]',\n      },\n      {\n        timestamp: '02:03:46.300',\n        level: 'INFO',\n        source: 'dbt',\n        message: '5 to 12 START remaining downstream aggregate semantic marts .................. [RUN]',\n      },\n      {\n        timestamp: '02:04:12.180',\n        level: 'INFO',\n        source: 'dbt',\n        message: 'Running 48 data integrity tests on rebuilt marts ............................ [PASS 48/48]',\n      },\n      {\n        timestamp: '02:04:18.500',\n        level: 'INFO',\n        source: 'dbt',\n        message: 'Generated docs manifest and catalog metadata to target/manifest.json',\n      },\n      {\n        timestamp: '02:04:20.120',\n        level: 'SUCCESS',\n        source: 'dbt',\n        message:\n          'Finished running 12 models, 48 tests in 100.03s. All marts synchronized with Looker/Hex BI semantic layer.',\n      },\n    ],\n  },\n]\n",
      "type": "registry:page",
      "target": "~/src/app/components/blocks/etl-pipeline-dag/etl-pipeline-dag-data.ts"
    },
    {
      "path": "packages/registry-angular/blocks/etl-pipeline-dag/index.ts",
      "content": "export { UiEtlPipelineDagComponent } from './etl-pipeline-dag.component'\nexport { UiEtlNodeDetailComponent } from './etl-node-detail.component'\nexport {\n  pipelineNodes,\n  type DagNode,\n  type LogLine,\n  type NodeCategory,\n  type NodeStatus,\n  type QualityCheck,\n  type SchemaColumn,\n} from './etl-pipeline-dag-data'\n",
      "type": "registry:page",
      "target": "~/src/app/components/blocks/etl-pipeline-dag/index.ts"
    }
  ],
  "dependencies": [
    "lucide-angular"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/badge.json",
    "https://uipkge.dev/r/angular/button.json",
    "https://uipkge.dev/r/angular/card.json",
    "https://uipkge.dev/r/angular/progress.json",
    "https://uipkge.dev/r/angular/tabs.json"
  ],
  "description": "Airflow and Dagster style directed acyclic graph (DAG) pipeline visualizer with interactive node execution topologies, health metrics, and terminal stdout log inspector.",
  "categories": [
    "devops",
    "app",
    "analytics"
  ]
}