{
  "dashboard_title": "Source Coverage & Gaps",
  "description": "Operational dashboard for identifying source coverage gaps, stale sources, and symbols missing expected data feeds.",
  "slug": "source-coverage-gaps",
  "position_json": {
    "HEADER_ID": {"id": "HEADER_ID", "type": "HEADER", "meta": {"text": "Source Coverage & Gaps"}},
    "ROW-1": {
      "type": "ROW",
      "children": ["CHART-coverage-matrix", "CHART-missing-types-table"]
    },
    "ROW-2": {
      "type": "ROW",
      "children": ["CHART-stale-sources-table", "CHART-failure-heatmap"]
    }
  },
  "metadata": {
    "refresh_frequency": 600,
    "default_filters": "{}",
    "color_scheme": "supersetColors"
  },
  "charts": [
    {
      "slice_name": "Source Coverage Matrix",
      "viz_type": "table",
      "description": "Per-symbol source type coverage showing active source counts",
      "datasource_type": "query",
      "query": "SELECT c.ticker, c.legal_name, c.sector, COUNT(s.id) FILTER (WHERE s.active) AS active_sources, COUNT(s.id) FILTER (WHERE s.source_type = 'market_api' AND s.active) AS market_sources, COUNT(s.id) FILTER (WHERE s.source_type = 'news_api' AND s.active) AS news_sources, COUNT(s.id) FILTER (WHERE s.source_type = 'filings_api' AND s.active) AS filings_sources, COUNT(s.id) FILTER (WHERE s.source_type = 'web_scrape' AND s.active) AS web_scrape_sources, COUNT(s.id) FILTER (WHERE s.source_type = 'broker' AND s.active) AS broker_sources FROM companies c LEFT JOIN sources s ON s.company_id = c.id WHERE c.active = TRUE GROUP BY c.ticker, c.legal_name, c.sector ORDER BY c.ticker"
    },
    {
      "slice_name": "Symbols Missing Source Types",
      "viz_type": "table",
      "description": "Companies that lack one or more expected source types (market_api, news_api, filings_api)",
      "datasource_type": "query",
      "query": "SELECT c.ticker, c.legal_name, c.sector, ARRAY_AGG(DISTINCT s.source_type) FILTER (WHERE s.active) AS active_types FROM companies c LEFT JOIN sources s ON s.company_id = c.id AND s.active = TRUE WHERE c.active = TRUE GROUP BY c.ticker, c.legal_name, c.sector HAVING NOT ARRAY['market_api', 'news_api', 'filings_api'] <@ ARRAY_AGG(DISTINCT s.source_type) FILTER (WHERE s.active) OR ARRAY_AGG(DISTINCT s.source_type) FILTER (WHERE s.active) IS NULL ORDER BY c.ticker"
    },
    {
      "slice_name": "Stale Sources (No Success in 24h)",
      "viz_type": "table",
      "description": "Active sources that have not completed a successful ingestion run in the last 24 hours",
      "datasource_type": "query",
      "query": "SELECT c.ticker, s.source_type, s.source_name, MAX(ir.started_at) FILTER (WHERE ir.status = 'completed') AS last_success, MAX(ir.started_at) AS last_attempt, COUNT(*) FILTER (WHERE ir.status = 'failed' AND ir.started_at >= NOW() - INTERVAL '24 hours') AS recent_failures FROM sources s JOIN companies c ON c.id = s.company_id LEFT JOIN ingestion_runs ir ON ir.source_id = s.id WHERE s.active = TRUE AND c.active = TRUE GROUP BY c.ticker, s.source_type, s.source_name HAVING MAX(ir.started_at) FILTER (WHERE ir.status = 'completed') < NOW() - INTERVAL '24 hours' OR MAX(ir.started_at) FILTER (WHERE ir.status = 'completed') IS NULL ORDER BY c.ticker, s.source_type"
    },
    {
      "slice_name": "Source Failure Heatmap",
      "viz_type": "heatmap",
      "description": "Failure counts by source type and ticker in the last 24h",
      "datasource_type": "query",
      "query": "SELECT c.ticker, ir.source_type, COUNT(*) FILTER (WHERE ir.status = 'failed') AS failures FROM ingestion_runs ir JOIN companies c ON c.id = ir.company_id WHERE ir.started_at >= NOW() - INTERVAL '24 hours' GROUP BY c.ticker, ir.source_type HAVING COUNT(*) FILTER (WHERE ir.status = 'failed') > 0 ORDER BY failures DESC"
    }
  ]
}
