{
  "uid": "libnovel-catalogue",
  "title": "Catalogue & Content Progress",
  "description": "Scraping progress, audio generation coverage, and catalogue health derived from runner structured logs.",
  "tags": ["libnovel", "catalogue", "content"],
  "timezone": "browser",
  "refresh": "1m",
  "time": { "from": "now-24h", "to": "now" },
  "schemaVersion": 39,
  "panels": [
    {
      "id": 1,
      "type": "stat",
      "title": "Books Scraped (last 24h)",
      "description": "Count of unique book slugs appearing in successful scrape task completions.",
      "gridPos": { "x": 0, "y": 0, "w": 4, "h": 4 },
      "options": { "reduceOptions": { "calcs": ["sum"] }, "colorMode": "value", "graphMode": "none" },
      "fieldConfig": {
        "defaults": {
          "color": { "fixedColor": "blue", "mode": "fixed" },
          "thresholds": { "mode": "absolute", "steps": [] }
        }
      },
      "targets": [
        {
          "datasource": { "type": "loki", "uid": "loki" },
          "expr": "sum_over_time({service_name=\"runner\"} | json | msg=`scrape task done` [24h])",
          "legendFormat": "books scraped"
        }
      ]
    },
    {
      "id": 2,
      "type": "stat",
      "title": "Chapters Scraped (last 24h)",
      "gridPos": { "x": 4, "y": 0, "w": 4, "h": 4 },
      "options": { "reduceOptions": { "calcs": ["sum"] }, "colorMode": "value", "graphMode": "none" },
      "fieldConfig": {
        "defaults": {
          "color": { "fixedColor": "blue", "mode": "fixed" },
          "thresholds": { "mode": "absolute", "steps": [] }
        }
      },
      "targets": [
        {
          "datasource": { "type": "loki", "uid": "loki" },
          "expr": "sum_over_time({service_name=\"runner\"} | json | unwrap scraped [24h])",
          "legendFormat": "chapters scraped"
        }
      ]
    },
    {
      "id": 3,
      "type": "stat",
      "title": "Audio Jobs Completed (last 24h)",
      "gridPos": { "x": 8, "y": 0, "w": 4, "h": 4 },
      "options": { "reduceOptions": { "calcs": ["sum"] }, "colorMode": "value", "graphMode": "none" },
      "fieldConfig": {
        "defaults": {
          "color": { "fixedColor": "green", "mode": "fixed" },
          "thresholds": { "mode": "absolute", "steps": [] }
        }
      },
      "targets": [
        {
          "datasource": { "type": "loki", "uid": "loki" },
          "expr": "sum_over_time({service_name=\"runner\"} | json | msg=`audio task done` [24h])",
          "legendFormat": "audio done"
        }
      ]
    },
    {
      "id": 4,
      "type": "stat",
      "title": "Audio Jobs Failed (last 24h)",
      "gridPos": { "x": 12, "y": 0, "w": 4, "h": 4 },
      "options": { "reduceOptions": { "calcs": ["sum"] }, "colorMode": "background", "graphMode": "none" },
      "fieldConfig": {
        "defaults": {
          "thresholds": {
            "mode": "absolute",
            "steps": [
              { "color": "green", "value": null },
              { "color": "yellow", "value": 1 },
              { "color": "red", "value": 5 }
            ]
          }
        }
      },
      "targets": [
        {
          "datasource": { "type": "loki", "uid": "loki" },
          "expr": "sum_over_time({service_name=\"runner\"} | json | msg=`audio task failed` [24h])",
          "legendFormat": "audio failed"
        }
      ]
    },
    {
      "id": 5,
      "type": "stat",
      "title": "Scrape Errors (last 24h)",
      "gridPos": { "x": 16, "y": 0, "w": 4, "h": 4 },
      "options": { "reduceOptions": { "calcs": ["sum"] }, "colorMode": "background", "graphMode": "none" },
      "fieldConfig": {
        "defaults": {
          "thresholds": {
            "mode": "absolute",
            "steps": [
              { "color": "green", "value": null },
              { "color": "yellow", "value": 1 },
              { "color": "red", "value": 10 }
            ]
          }
        }
      },
      "targets": [
        {
          "datasource": { "type": "loki", "uid": "loki" },
          "expr": "sum_over_time({service_name=\"runner\"} | json | msg=`scrape task failed` [24h])",
          "legendFormat": "scrape errors"
        }
      ]
    },
    {
      "id": 6,
      "type": "stat",
      "title": "Catalogue Refresh — Books Indexed",
      "description": "Total books indexed in the last catalogue refresh cycle (from the ok field in the summary log).",
      "gridPos": { "x": 20, "y": 0, "w": 4, "h": 4 },
      "options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "value", "graphMode": "none" },
      "fieldConfig": {
        "defaults": {
          "color": { "fixedColor": "purple", "mode": "fixed" },
          "thresholds": { "mode": "absolute", "steps": [] }
        }
      },
      "targets": [
        {
          "datasource": { "type": "loki", "uid": "loki" },
          "expr": "last_over_time({service_name=\"runner\"} | json | op=`catalogue_refresh` | msg=`catalogue refresh done` | unwrap ok [7d])",
          "legendFormat": "indexed"
        }
      ]
    },
    {
      "id": 10,
      "type": "timeseries",
      "title": "Audio Generation Rate (tasks/min)",
      "gridPos": { "x": 0, "y": 4, "w": 12, "h": 8 },
      "description": "Rate of audio task completions and failures over time.",
      "options": {
        "tooltip": { "mode": "multi" },
        "legend": { "displayMode": "list", "placement": "bottom" }
      },
      "fieldConfig": {
        "defaults": { "unit": "short", "custom": { "lineWidth": 2, "fillOpacity": 10 } },
        "overrides": [
          { "matcher": { "id": "byName", "options": "failed" }, "properties": [{ "id": "color", "value": { "fixedColor": "red", "mode": "fixed" } }] },
          { "matcher": { "id": "byName", "options": "completed" }, "properties": [{ "id": "color", "value": { "fixedColor": "green", "mode": "fixed" } }] }
        ]
      },
      "targets": [
        {
          "datasource": { "type": "prometheus", "uid": "prometheus" },
          "expr": "sum(rate(traces_spanmetrics_calls_total{service=\"runner\", span_name=\"runner.audio_task\", status_code!=\"STATUS_CODE_ERROR\"}[5m])) * 60",
          "legendFormat": "completed"
        },
        {
          "datasource": { "type": "prometheus", "uid": "prometheus" },
          "expr": "sum(rate(traces_spanmetrics_calls_total{service=\"runner\", span_name=\"runner.audio_task\", status_code=\"STATUS_CODE_ERROR\"}[5m])) * 60",
          "legendFormat": "failed"
        }
      ]
    },
    {
      "id": 11,
      "type": "timeseries",
      "title": "Scraping Rate (tasks/min)",
      "gridPos": { "x": 12, "y": 4, "w": 12, "h": 8 },
      "description": "Rate of scrape task completions and failures over time.",
      "options": {
        "tooltip": { "mode": "multi" },
        "legend": { "displayMode": "list", "placement": "bottom" }
      },
      "fieldConfig": {
        "defaults": { "unit": "short", "custom": { "lineWidth": 2, "fillOpacity": 10 } },
        "overrides": [
          { "matcher": { "id": "byName", "options": "failed" }, "properties": [{ "id": "color", "value": { "fixedColor": "red", "mode": "fixed" } }] },
          { "matcher": { "id": "byName", "options": "completed" }, "properties": [{ "id": "color", "value": { "fixedColor": "blue", "mode": "fixed" } }] }
        ]
      },
      "targets": [
        {
          "datasource": { "type": "prometheus", "uid": "prometheus" },
          "expr": "sum(rate(traces_spanmetrics_calls_total{service=\"runner\", span_name=\"runner.scrape_task\", status_code!=\"STATUS_CODE_ERROR\"}[5m])) * 60",
          "legendFormat": "completed"
        },
        {
          "datasource": { "type": "prometheus", "uid": "prometheus" },
          "expr": "sum(rate(traces_spanmetrics_calls_total{service=\"runner\", span_name=\"runner.scrape_task\", status_code=\"STATUS_CODE_ERROR\"}[5m])) * 60",
          "legendFormat": "failed"
        }
      ]
    },
    {
      "id": 20,
      "type": "logs",
      "title": "Scrape Task Events",
      "description": "One log line per completed or failed scrape task. Fields: task_id, kind, url, scraped, skipped, errors.",
      "gridPos": { "x": 0, "y": 12, "w": 24, "h": 10 },
      "options": {
        "showTime": true,
        "showLabels": false,
        "wrapLogMessage": false,
        "prettifyLogMessage": true,
        "enableLogDetails": true,
        "sortOrder": "Descending",
        "dedupStrategy": "none"
      },
      "targets": [
        {
          "datasource": { "type": "loki", "uid": "loki" },
          "expr": "{service_name=\"runner\"} | json | msg =~ `scrape task (done|failed|starting)`",
          "legendFormat": ""
        }
      ]
    },
    {
      "id": 21,
      "type": "logs",
      "title": "Audio Task Events",
      "description": "One log line per completed or failed audio task. Fields: task_id, slug, chapter, voice, key (on success), reason (on failure).",
      "gridPos": { "x": 0, "y": 22, "w": 24, "h": 10 },
      "options": {
        "showTime": true,
        "showLabels": false,
        "wrapLogMessage": false,
        "prettifyLogMessage": true,
        "enableLogDetails": true,
        "sortOrder": "Descending",
        "dedupStrategy": "none"
      },
      "targets": [
        {
          "datasource": { "type": "loki", "uid": "loki" },
          "expr": "{service_name=\"runner\"} | json | msg =~ `audio task (done|failed|starting)`",
          "legendFormat": ""
        }
      ]
    },
    {
      "id": 22,
      "type": "logs",
      "title": "Catalogue Refresh Progress",
      "description": "Progress logs from the background catalogue refresh (every 24h). Fields: op=catalogue_refresh, scraped, ok, skipped, errors.",
      "gridPos": { "x": 0, "y": 32, "w": 24, "h": 8 },
      "options": {
        "showTime": true,
        "showLabels": false,
        "wrapLogMessage": false,
        "prettifyLogMessage": true,
        "enableLogDetails": true,
        "sortOrder": "Descending",
        "dedupStrategy": "none"
      },
      "targets": [
        {
          "datasource": { "type": "loki", "uid": "loki" },
          "expr": "{service_name=\"runner\"} | json | op=`catalogue_refresh`",
          "legendFormat": ""
        }
      ]
    }
  ]
}
