{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "etl-pipeline-dag",
  "title": "Etl Pipeline Dag",
  "type": "registry:page",
  "files": [
    {
      "path": "packages/registry-vue/blocks/etl-pipeline-dag/EtlPipelineDag.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport type { HTMLAttributes } from 'vue'\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  Play,\n  ShieldCheck,\n  Workflow,\n  Zap,\n} from 'lucide-vue-next'\nimport { cn } from '@/lib/utils'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'\nimport { Progress } from '@/components/ui/progress'\nimport EtlNodeDetail from './EtlNodeDetail.vue'\n\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\ninterface Props {\n  initialNodeId?: string\n  class?: HTMLAttributes['class']\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  initialNodeId: 'extract_stripe_charges',\n})\n\nconst selectedNodeId = ref<string>(props.initialNodeId)\nconst copiedAllLogs = ref<boolean>(false)\nconst isTriggering = ref<boolean>(false)\nconst triggerSuccessToast = ref<boolean>(false)\n\nconst 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\nconst selectedNode = computed(() => {\n  return pipelineNodes.find((n) => n.id === selectedNodeId.value) ?? pipelineNodes[0]\n})\n\nfunction selectNode(id: string) {\n  selectedNodeId.value = id\n}\n\nfunction handleTriggerDag() {\n  if (isTriggering.value) return\n  isTriggering.value = true\n  triggerSuccessToast.value = false\n\n  setTimeout(() => {\n    isTriggering.value = false\n    triggerSuccessToast.value = true\n    setTimeout(() => {\n      triggerSuccessToast.value = false\n    }, 4000)\n  }, 1600)\n}\n\nasync function handleExportFullRunLog() {\n  const fullLog = pipelineNodes\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\n  try {\n    await navigator.clipboard.writeText(fullLog)\n    copiedAllLogs.value = true\n    setTimeout(() => {\n      copiedAllLogs.value = false\n    }, 2500)\n  } catch {\n    // Fallback\n  }\n}\n\nfunction getNodeCategoryBadgeClass(category: NodeCategory) {\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\nfunction getNodeIcon(id: string) {\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</script>\n\n<template>\n  <div data-slot=\"etl-pipeline-dag\" :class=\"cn('w-full space-y-6', props.class)\">\n    <!-- Top Pipeline Header Card -->\n    <Card class=\"border-border bg-card shadow-xs\">\n      <CardContent 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            <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              <Badge variant=\"secondary\" class=\"font-mono text-xs\">v2.4.1</Badge>\n            </div>\n\n            <div class=\"flex flex-wrap items-center gap-2\">\n              <!-- DAG Schedule Badge -->\n              <Badge variant=\"outline\" class=\"gap-1.5 font-mono text-xs shadow-xs\">\n                <Clock class=\"text-muted-foreground size-3\" />\n                <span>0 2 * * * · Daily at 02:00 UTC</span>\n              </Badge>\n\n              <!-- State Badge -->\n              <Badge variant=\"success\" class=\"gap-1.5 font-mono text-xs shadow-xs\">\n                <CheckCircle2 class=\"size-3\" />\n                <span>State: Success · Run ID: #run_90412</span>\n              </Badge>\n\n              <!-- Live Queue Toast -->\n              <Badge\n                v-if=\"triggerSuccessToast\"\n                variant=\"info\"\n                class=\"animate-in fade-in-0 gap-1.5 font-mono text-xs duration-300\"\n              >\n                <Zap class=\"size-3\" />\n                <span>Triggered Run #run_90413 queued!</span>\n              </Badge>\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            variant=\"outline\"\n            size=\"sm\"\n            class=\"h-8.5 gap-1.5 text-xs font-medium\"\n            :disabled=\"copiedAllLogs\"\n            @click=\"handleExportFullRunLog\"\n          >\n            <component :is=\"copiedAllLogs ? Check : FileDown\" class=\"size-3.5\" />\n            <span>{{ copiedAllLogs ? 'Run Log Copied!' : 'Export Run Log' }}</span>\n          </Button>\n\n          <Button\n            variant=\"default\"\n            size=\"sm\"\n            class=\"h-8.5 gap-1.5 text-xs font-medium\"\n            :disabled=\"isTriggering\"\n            @click=\"handleTriggerDag\"\n          >\n            <Loader2 v-if=\"isTriggering\" class=\"size-3.5 animate-spin\" />\n            <Play v-else class=\"size-3.5 fill-current\" />\n            <span>{{ isTriggering ? 'Triggering DAG Run...' : 'Trigger DAG Run' }}</span>\n          </Button>\n        </div>\n      </CardContent>\n    </Card>\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      <Card class=\"border-border bg-card shadow-xs\">\n        <CardContent 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              <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        </CardContent>\n      </Card>\n\n      <!-- Tasks Completed -->\n      <Card class=\"border-border bg-card shadow-xs\">\n        <CardContent 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              <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              <Progress :model-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        </CardContent>\n      </Card>\n\n      <!-- Records Ingested -->\n      <Card class=\"border-border bg-card shadow-xs\">\n        <CardContent 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              <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\">148,290 records</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        </CardContent>\n      </Card>\n\n      <!-- Data Freshness -->\n      <Card class=\"border-border bg-card shadow-xs\">\n        <CardContent 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              <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        </CardContent>\n      </Card>\n    </div>\n\n    <!-- Interactive DAG Node Pipeline Graph (Connected DAG Flow) -->\n    <Card class=\"border-border bg-card shadow-xs\">\n      <CardHeader 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              <CardTitle class=\"text-base font-semibold\">DAG Execution Topology</CardTitle>\n              <Badge variant=\"outline\" class=\"font-mono text-xs\">6 Connected Tasks</Badge>\n            </div>\n            <CardDescription class=\"text-xs\">\n              Directed acyclic graph with dependency streams. Select any node to inspect telemetry and execution logs.\n            </CardDescription>\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\" />\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              <GitBranch class=\"size-3\" />\n              <span>Topology: 2 Branches &rarr; Converged</span>\n            </div>\n          </div>\n        </div>\n      </CardHeader>\n\n      <CardContent 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            <!-- Stage 1: Extract / Ingest (2 Parallel Sources) -->\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                  01 · Extract (2)\n                </span>\n                <Badge variant=\"outline\" class=\"text-xs\">Parallel</Badge>\n              </div>\n\n              <!-- Node 1: extract_stripe_charges -->\n              <div\n                role=\"button\"\n                tabindex=\"0\"\n                :class=\"\n                  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                    selectedNodeId === 'extract_stripe_charges'\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                @click=\"selectNode('extract_stripe_charges')\"\n                @keydown.enter=\"selectNode('extract_stripe_charges')\"\n                @keydown.space.prevent=\"selectNode('extract_stripe_charges')\"\n              >\n                <div class=\"flex items-start justify-between gap-2\">\n                  <div class=\"flex items-center gap-2\">\n                    <div\n                      class=\"border-info/30 bg-info/10 text-info flex size-7 items-center justify-center rounded-md border\"\n                    >\n                      <CreditCard class=\"size-3.5\" />\n                    </div>\n                    <Badge :class=\"cn('font-mono text-xs', getNodeCategoryBadgeClass('ingest'))\"> INGEST </Badge>\n                  </div>\n                  <Badge variant=\"success\" class=\"gap-1 font-mono text-xs\">\n                    <Check class=\"size-3\" />\n                    18s\n                  </Badge>\n                </div>\n\n                <div class=\"mt-2.5\">\n                  <div class=\"text-foreground font-mono text-xs font-semibold break-all\">extract_stripe_charges</div>\n                  <div class=\"text-muted-foreground mt-0.5 font-mono text-xs\">148.3k records · 42.6 MB</div>\n                </div>\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\">raw_stripe.charges</span>\n                  <ArrowRight\n                    class=\"text-muted-foreground group-hover:text-primary size-3 shrink-0 transition-colors\"\n                  />\n                </div>\n              </div>\n\n              <!-- Node 2: extract_currency_rates -->\n              <div\n                role=\"button\"\n                tabindex=\"0\"\n                :class=\"\n                  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                    selectedNodeId === 'extract_currency_rates'\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                @click=\"selectNode('extract_currency_rates')\"\n                @keydown.enter=\"selectNode('extract_currency_rates')\"\n                @keydown.space.prevent=\"selectNode('extract_currency_rates')\"\n              >\n                <div class=\"flex items-start justify-between gap-2\">\n                  <div class=\"flex items-center gap-2\">\n                    <div\n                      class=\"border-info/30 bg-info/10 text-info flex size-7 items-center justify-center rounded-md border\"\n                    >\n                      <Globe class=\"size-3.5\" />\n                    </div>\n                    <Badge :class=\"cn('font-mono text-xs', getNodeCategoryBadgeClass('ingest'))\"> INGEST </Badge>\n                  </div>\n                  <Badge variant=\"success\" class=\"gap-1 font-mono text-xs\">\n                    <Check class=\"size-3\" />\n                    4s\n                  </Badge>\n                </div>\n\n                <div class=\"mt-2.5\">\n                  <div class=\"text-foreground font-mono text-xs font-semibold break-all\">extract_currency_rates</div>\n                  <div class=\"text-muted-foreground mt-0.5 font-mono text-xs\">34 FX pairs · 128 KB</div>\n                </div>\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\">raw_currency.rates</span>\n                  <ArrowRight\n                    class=\"text-muted-foreground group-hover:text-primary size-3 shrink-0 transition-colors\"\n                  />\n                </div>\n              </div>\n            </div>\n\n            <!-- Stage 2: Transform (PySpark Normalization) -->\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                  02 · Transform\n                </span>\n                <Badge variant=\"secondary\" class=\"font-mono text-xs\">Join 2&rarr;1</Badge>\n              </div>\n\n              <!-- Node 3: transform_normalize_fx -->\n              <div\n                role=\"button\"\n                tabindex=\"0\"\n                :class=\"\n                  cn(\n                    'group focus-visible:ring-ring relative my-auto cursor-pointer rounded-xl border p-3.5 transition-colors duration-200 focus-visible:ring-2 focus-visible:outline-none',\n                    selectedNodeId === 'transform_normalize_fx'\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                @click=\"selectNode('transform_normalize_fx')\"\n                @keydown.enter=\"selectNode('transform_normalize_fx')\"\n                @keydown.space.prevent=\"selectNode('transform_normalize_fx')\"\n              >\n                <div class=\"flex items-start justify-between gap-2\">\n                  <div class=\"flex items-center gap-2\">\n                    <div\n                      class=\"border-chart-1/30 bg-chart-1/10 text-chart-1 flex size-7 items-center justify-center rounded-md border\"\n                    >\n                      <Cpu class=\"size-3.5\" />\n                    </div>\n                    <Badge :class=\"cn('font-mono text-xs', getNodeCategoryBadgeClass('transform'))\"> TRANSFORM </Badge>\n                  </div>\n                  <Badge variant=\"success\" class=\"gap-1 font-mono text-xs\">\n                    <Check class=\"size-3\" />\n                    42s\n                  </Badge>\n                </div>\n\n                <div class=\"mt-2.5\">\n                  <div class=\"text-foreground font-mono text-xs font-semibold break-all\">transform_normalize_fx</div>\n                  <div class=\"text-muted-foreground mt-0.5 font-mono text-xs\">148,290 rows · PySpark 3.5</div>\n                </div>\n\n                <div class=\"mt-2 flex flex-wrap gap-1\">\n                  <span class=\"border-border bg-muted/60 text-muted-foreground rounded px-1.5 py-0.5 font-mono text-xs\">\n                    2 Upstreams\n                  </span>\n                  <span class=\"border-border bg-muted/60 text-muted-foreground rounded px-1.5 py-0.5 font-mono text-xs\">\n                    Snappy\n                  </span>\n                </div>\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\">stg_payments_fx</span>\n                  <ArrowRight\n                    class=\"text-muted-foreground group-hover:text-primary size-3 shrink-0 transition-colors\"\n                  />\n                </div>\n              </div>\n            </div>\n\n            <!-- Stage 3: Quality & Validation (Great Expectations) -->\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                  03 · Quality\n                </span>\n                <Badge variant=\"outline\" class=\"font-mono text-xs\">18 Checks</Badge>\n              </div>\n\n              <!-- Node 4: validate_schema_anomalies -->\n              <div\n                role=\"button\"\n                tabindex=\"0\"\n                :class=\"\n                  cn(\n                    'group focus-visible:ring-ring relative my-auto cursor-pointer rounded-xl border p-3.5 transition-colors duration-200 focus-visible:ring-2 focus-visible:outline-none',\n                    selectedNodeId === 'validate_schema_anomalies'\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                @click=\"selectNode('validate_schema_anomalies')\"\n                @keydown.enter=\"selectNode('validate_schema_anomalies')\"\n                @keydown.space.prevent=\"selectNode('validate_schema_anomalies')\"\n              >\n                <div class=\"flex items-start justify-between gap-2\">\n                  <div class=\"flex items-center gap-2\">\n                    <div\n                      class=\"border-warning/30 bg-warning/10 text-warning flex size-7 items-center justify-center rounded-md border\"\n                    >\n                      <ShieldCheck class=\"size-3.5\" />\n                    </div>\n                    <Badge :class=\"cn('font-mono text-xs', getNodeCategoryBadgeClass('validate'))\"> VALIDATE </Badge>\n                  </div>\n                  <Badge variant=\"success\" class=\"gap-1 font-mono text-xs\">\n                    <Check class=\"size-3\" />\n                    14s\n                  </Badge>\n                </div>\n\n                <div class=\"mt-2.5\">\n                  <div class=\"text-foreground font-mono text-xs font-semibold break-all\">validate_schema_anomalies</div>\n                  <div class=\"text-muted-foreground mt-0.5 font-mono text-xs\">18/18 checks passed (100%)</div>\n                </div>\n\n                <div class=\"mt-2 flex flex-wrap gap-1\">\n                  <span class=\"border-success/30 bg-success/10 text-success rounded px-1.5 py-0.5 font-mono text-xs\">\n                    0 Anomalies\n                  </span>\n                  <span class=\"border-border bg-muted/60 text-muted-foreground rounded px-1.5 py-0.5 font-mono text-xs\">\n                    Z-Score 3σ\n                  </span>\n                </div>\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\">anomalies_audit</span>\n                  <ArrowRight\n                    class=\"text-muted-foreground group-hover:text-primary size-3 shrink-0 transition-colors\"\n                  />\n                </div>\n              </div>\n            </div>\n\n            <!-- Stage 4: Warehouse Load (Snowflake) -->\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                  04 · Load\n                </span>\n                <Badge variant=\"outline\" class=\"font-mono text-xs\">Snowflake</Badge>\n              </div>\n\n              <!-- Node 5: load_snowflake_warehouse -->\n              <div\n                role=\"button\"\n                tabindex=\"0\"\n                :class=\"\n                  cn(\n                    'group focus-visible:ring-ring relative my-auto cursor-pointer rounded-xl border p-3.5 transition-colors duration-200 focus-visible:ring-2 focus-visible:outline-none',\n                    selectedNodeId === 'load_snowflake_warehouse'\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                @click=\"selectNode('load_snowflake_warehouse')\"\n                @keydown.enter=\"selectNode('load_snowflake_warehouse')\"\n                @keydown.space.prevent=\"selectNode('load_snowflake_warehouse')\"\n              >\n                <div class=\"flex items-start justify-between gap-2\">\n                  <div class=\"flex items-center gap-2\">\n                    <div\n                      class=\"flex size-7 items-center justify-center rounded-md border border-cyan-500/30 bg-cyan-500/10 text-cyan-500\"\n                    >\n                      <Database class=\"size-3.5\" />\n                    </div>\n                    <Badge :class=\"cn('font-mono text-xs', getNodeCategoryBadgeClass('load'))\"> LOAD </Badge>\n                  </div>\n                  <Badge variant=\"success\" class=\"gap-1 font-mono text-xs\">\n                    <Check class=\"size-3\" />\n                    1m 20s\n                  </Badge>\n                </div>\n\n                <div class=\"mt-2.5\">\n                  <div class=\"text-foreground font-mono text-xs font-semibold break-all\">load_snowflake_warehouse</div>\n                  <div class=\"text-muted-foreground mt-0.5 font-mono text-xs\">148,290 rows merged</div>\n                </div>\n\n                <div class=\"mt-2 flex flex-wrap gap-1\">\n                  <span class=\"border-border bg-muted/60 text-muted-foreground rounded px-1.5 py-0.5 font-mono text-xs\">\n                    4X-Large\n                  </span>\n                  <span class=\"border-border bg-muted/60 text-muted-foreground rounded px-1.5 py-0.5 font-mono text-xs\">\n                    Clustered\n                  </span>\n                </div>\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\">fct_stripe_charges</span>\n                  <ArrowRight\n                    class=\"text-muted-foreground group-hover:text-primary size-3 shrink-0 transition-colors\"\n                  />\n                </div>\n              </div>\n            </div>\n\n            <!-- Stage 5: Analytics Modeling (dbt Core) -->\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                  05 · Marts\n                </span>\n                <Badge variant=\"outline\" class=\"font-mono text-xs\">dbt Core</Badge>\n              </div>\n\n              <!-- Node 6: refresh_dbt_analytics_marts -->\n              <div\n                role=\"button\"\n                tabindex=\"0\"\n                :class=\"\n                  cn(\n                    'group focus-visible:ring-ring relative my-auto cursor-pointer rounded-xl border p-3.5 transition-colors duration-200 focus-visible:ring-2 focus-visible:outline-none',\n                    selectedNodeId === 'refresh_dbt_analytics_marts'\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                @click=\"selectNode('refresh_dbt_analytics_marts')\"\n                @keydown.enter=\"selectNode('refresh_dbt_analytics_marts')\"\n                @keydown.space.prevent=\"selectNode('refresh_dbt_analytics_marts')\"\n              >\n                <div class=\"flex items-start justify-between gap-2\">\n                  <div class=\"flex items-center gap-2\">\n                    <div\n                      class=\"border-success/30 bg-success/10 text-success flex size-7 items-center justify-center rounded-md border\"\n                    >\n                      <Layers class=\"size-3.5\" />\n                    </div>\n                    <Badge :class=\"cn('font-mono text-xs', getNodeCategoryBadgeClass('model'))\"> MODEL </Badge>\n                  </div>\n                  <Badge variant=\"success\" class=\"gap-1 font-mono text-xs\">\n                    <Check class=\"size-3\" />\n                    1m 40s\n                  </Badge>\n                </div>\n\n                <div class=\"mt-2.5\">\n                  <div class=\"text-foreground font-mono text-xs font-semibold break-all\">\n                    refresh_dbt_analytics_marts\n                  </div>\n                  <div class=\"text-muted-foreground mt-0.5 font-mono text-xs\">12 models · 48 tests</div>\n                </div>\n\n                <div class=\"mt-2 flex flex-wrap gap-1\">\n                  <span class=\"border-border bg-muted/60 text-muted-foreground rounded px-1.5 py-0.5 font-mono text-xs\">\n                    8 Threads\n                  </span>\n                  <span class=\"border-success/30 bg-success/10 text-success rounded px-1.5 py-0.5 font-mono text-xs\">\n                    BI Synced\n                  </span>\n                </div>\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\">mart_finance_revenue</span>\n                  <CheckCircle2 class=\"text-success size-3 shrink-0\" />\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </CardContent>\n    </Card>\n\n    <!-- Selected Node Log Inspector Panel -->\n    <EtlNodeDetail\n      :node=\"selectedNode\"\n      :category-badge-class=\"cn('font-mono text-xs', getNodeCategoryBadgeClass(selectedNode.category))\"\n      :node-icon=\"getNodeIcon(selectedNode.id)\"\n      @select=\"selectNode\"\n    />\n  </div>\n</template>\n",
      "type": "registry:page",
      "target": "~/app/components/blocks/EtlPipelineDag.vue"
    },
    {
      "path": "packages/registry-vue/blocks/etl-pipeline-dag/EtlNodeDetail.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport type { Component } from 'vue'\nimport {\n  Check,\n  Code2,\n  Copy,\n  CornerDownRight,\n  GitBranch,\n  GitCommit,\n  Search,\n  ShieldCheck,\n  Terminal,\n  X,\n} from 'lucide-vue-next'\nimport { cn } from '@/lib/utils'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent, CardHeader } from '@/components/ui/card'\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'\nimport type { DagNode } from './EtlPipelineDag.vue'\n\ninterface Props {\n  node: DagNode\n  categoryBadgeClass: string\n  nodeIcon: Component\n}\n\nconst props = defineProps<Props>()\n\nconst emit = defineEmits<{\n  select: [id: string]\n}>()\n\nconst activeTab = ref<string>('logs')\nconst logSearchQuery = ref<string>('')\nconst selectedLogLevel = ref<string>('ALL')\nconst isWrapped = ref<boolean>(false)\nconst copiedLogs = ref<boolean>(false)\n\nconst filteredLogs = computed(() => {\n  const query = logSearchQuery.value.trim().toLowerCase()\n  const level = selectedLogLevel.value\n\n  return props.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\n    return matchesLevel && matchesQuery\n  })\n})\n\nasync function handleCopyNodeLogs() {\n  const text = props.node.logs.map((l) => `[${l.timestamp}] [${l.level}] [${l.source}] ${l.message}`).join('\\n')\n  try {\n    await navigator.clipboard.writeText(text)\n    copiedLogs.value = true\n    setTimeout(() => {\n      copiedLogs.value = false\n    }, 2000)\n  } catch {\n    // Clipboard fallback\n  }\n}\n</script>\n\n<template>\n  <Card class=\"border-border bg-card shadow-xs\">\n    <CardHeader 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              <component :is=\"nodeIcon\" class=\"size-4\" />\n            </div>\n            <span class=\"text-foreground font-mono text-base font-semibold\">\n              {{ node.name }}\n            </span>\n            <Badge :class=\"cn('font-mono text-xs', categoryBadgeClass)\">\n              {{ node.categoryLabel }}\n            </Badge>\n            <Badge variant=\"success\" class=\"gap-1 font-mono text-xs\">\n              <Check class=\"size-3\" />\n              Success\n            </Badge>\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            <Search class=\"text-muted-foreground absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2\" />\n            <input\n              v-model=\"logSearchQuery\"\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            <button\n              aria-label=\"Clear log search\"\n              v-if=\"logSearchQuery\"\n              type=\"button\"\n              class=\"text-muted-foreground hover:text-foreground absolute top-1/2 right-2 -translate-y-1/2\"\n              @click=\"logSearchQuery = ''\"\n            >\n              <X class=\"size-3.5\" />\n            </button>\n          </div>\n\n          <!-- Wrap Toggle -->\n          <Button variant=\"outline\" size=\"sm\" class=\"h-8 gap-1 text-xs\" @click=\"isWrapped = !isWrapped\">\n            <span>{{ isWrapped ? 'Unwrap Lines' : 'Wrap Lines' }}</span>\n          </Button>\n\n          <!-- Copy Logs Button -->\n          <Button\n            variant=\"outline\"\n            size=\"sm\"\n            class=\"h-8 gap-1.5 text-xs font-medium\"\n            :disabled=\"copiedLogs\"\n            @click=\"handleCopyNodeLogs\"\n          >\n            <component :is=\"copiedLogs ? Check : Copy\" class=\"size-3.5\" />\n            <span>{{ copiedLogs ? 'Logs Copied' : 'Copy Logs' }}</span>\n          </Button>\n        </div>\n      </div>\n    </CardHeader>\n\n    <CardContent class=\"p-6 pt-1\">\n      <Tabs v-model=\"activeTab\" default-value=\"logs\" class=\"w-full\">\n        <TabsList class=\"mb-4\">\n          <TabsTrigger value=\"logs\" class=\"gap-1.5 font-mono text-xs\">\n            <Terminal class=\"size-3.5\" />\n            <span>Stdout Logs ({{ node.logs.length }})</span>\n          </TabsTrigger>\n          <TabsTrigger value=\"params\" class=\"gap-1.5 font-mono text-xs\">\n            <Code2 class=\"size-3.5\" />\n            <span>Task Parameters</span>\n          </TabsTrigger>\n          <TabsTrigger value=\"lineage\" class=\"gap-1.5 font-mono text-xs\">\n            <GitBranch class=\"size-3.5\" />\n            <span>Lineage & Schema</span>\n          </TabsTrigger>\n          <TabsTrigger value=\"quality\" class=\"gap-1.5 font-mono text-xs\">\n            <ShieldCheck class=\"size-3.5\" />\n            <span>Quality Assertions ({{ node.qualityChecks.length }})</span>\n          </TabsTrigger>\n        </TabsList>\n\n        <!-- Tab 1: Stdout Execution Logs -->\n        <TabsContent 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              <button\n                v-for=\"lvl in ['ALL', 'INFO', 'WARN', 'SQL', 'SUCCESS']\"\n                :key=\"lvl\"\n                type=\"button\"\n                :class=\"\n                  cn(\n                    'min-h-6 rounded-md px-2 py-0.5 font-mono text-xs transition-colors',\n                    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                @click=\"selectedLogLevel = lvl\"\n              >\n                {{ lvl }}\n              </button>\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\" />\n                  <span class=\"bg-warning/80 inline-block size-2.5 rounded-full\" />\n                  <span class=\"bg-success/80 inline-block size-2.5 rounded-full\" />\n                </div>\n                <span class=\"text-muted-foreground font-mono text-xs\"> stdout &middot; {{ 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>&middot;</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              <div v-if=\"filteredLogs.length === 0\" class=\"text-muted-foreground py-8 text-center\">\n                No log entries matching current search or level filters.\n              </div>\n\n              <div\n                v-for=\"(log, idx) in filteredLogs\"\n                :key=\"idx\"\n                :class=\"\n                  cn(\n                    '-mx-1 flex items-start rounded px-1 py-0.5 hover:bg-zinc-900/60',\n                    isWrapped ? 'break-all whitespace-pre-wrap' : 'overflow-x-auto whitespace-pre',\n                  )\n                \"\n              >\n                <!-- Line Number -->\n                <span class=\"mr-3 w-7 shrink-0 text-right text-zinc-600 tabular-nums select-none\">\n                  {{ idx + 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\n                  :class=\"\n                    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                  [{{ log.level }}]\n                </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\">\n                  {{ log.message }}\n                </span>\n              </div>\n            </div>\n          </div>\n        </TabsContent>\n\n        <!-- Tab 2: Task Parameters & Config -->\n        <TabsContent 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              <div\n                v-for=\"(val, key) in node.configParams\"\n                :key=\"key\"\n                class=\"flex flex-col gap-1 p-3.5 sm:flex-row sm:items-center sm:justify-between\"\n              >\n                <span class=\"text-muted-foreground font-mono font-medium sm:w-1/3\">{{ key }}</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                  {{ val }}\n                </span>\n              </div>\n            </div>\n          </div>\n        </TabsContent>\n\n        <!-- Tab 3: Lineage & Schema -->\n        <TabsContent 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                <GitCommit class=\"text-muted-foreground size-3.5\" />\n                <span>Upstream Dependencies ({{ node.upstream.length }})</span>\n              </div>\n              <div v-if=\"node.upstream.length === 0\" class=\"text-muted-foreground mt-3 text-xs\">\n                Root extraction node. No upstream parent dependencies.\n              </div>\n              <div v-else class=\"mt-3 space-y-2\">\n                <div\n                  v-for=\"up in node.upstream\"\n                  :key=\"up\"\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=\"emit('select', up)\"\n                  @keydown.enter=\"emit('select', up)\"\n                >\n                  <span class=\"text-foreground font-mono text-xs font-medium\">{{ up }}</span>\n                  <Badge variant=\"success\" class=\"text-xs\">Resolved</Badge>\n                </div>\n              </div>\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                <CornerDownRight class=\"text-muted-foreground size-3.5\" />\n                <span>Downstream Consumers ({{ node.downstream.length }})</span>\n              </div>\n              <div v-if=\"node.downstream.length === 0\" class=\"text-muted-foreground mt-3 text-xs\">\n                Terminal node. Emits finalized reporting dataset.\n              </div>\n              <div v-else class=\"mt-3 space-y-2\">\n                <div\n                  v-for=\"down in node.downstream\"\n                  :key=\"down\"\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=\"emit('select', down)\"\n                  @keydown.enter=\"emit('select', down)\"\n                >\n                  <span class=\"text-foreground font-mono text-xs font-medium\">{{ down }}</span>\n                  <Badge variant=\"outline\" class=\"text-xs\">Triggered</Badge>\n                </div>\n              </div>\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                  <tr v-for=\"col in node.schemaColumns\" :key=\"col.name\" 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                </tbody>\n              </table>\n            </div>\n          </div>\n        </TabsContent>\n\n        <!-- Tab 4: Quality Assertions -->\n        <TabsContent 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                {{ node.qualityChecks.filter((c) => c.status === 'passed').length }} /\n                {{ node.qualityChecks.length }} PASSED\n              </span>\n            </div>\n            <div class=\"divide-border divide-y text-xs\">\n              <div\n                v-for=\"check in node.qualityChecks\"\n                :key=\"check.id\"\n                class=\"flex flex-col gap-2 p-4 sm:flex-row sm:items-center sm:justify-between\"\n              >\n                <div class=\"space-y-1\">\n                  <div class=\"flex items-center gap-2\">\n                    <Badge variant=\"success\" class=\"text-xs\">PASSED</Badge>\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            </div>\n          </div>\n        </TabsContent>\n      </Tabs>\n    </CardContent>\n  </Card>\n</template>\n",
      "type": "registry:page",
      "target": "~/app/components/blocks/EtlNodeDetail.vue"
    }
  ],
  "dependencies": [
    "lucide-vue-next"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/vue/badge.json",
    "https://uipkge.dev/r/vue/button.json",
    "https://uipkge.dev/r/vue/card.json",
    "https://uipkge.dev/r/vue/progress.json",
    "https://uipkge.dev/r/vue/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"
  ]
}