# K8s Monitoring Widget Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add a Quickshell bar pill + popout that shows pod health and namespace resource quota for the `tenant-5` Kubernetes cluster.

**Architecture:** A `Kubernetes.qml` Singleton runs two shell scripts on independent timers — `k8s-status.sh` (pod readiness, 30s) and `k8s-metrics.sh` (kubectl top + resourcequota, 15s). A `KubernetesPill` in the bar shows ready/total pods colored by health; clicking opens `KubernetesPopout` with a per-pod table and quota bars.

**Tech Stack:** Quickshell QML (Singleton, Process, StdioCollector, BarPill), bash + jq, kubectl

> **Working directory for all git commands:** `~/.local/share/chezmoi/dot_config/quickshell/`
> `Kubernetes.qml` needs no `qmldir` registration — Quickshell auto-discovers Singletons in the same directory (same mechanism as `Weather.qml`, `Config.qml`, etc.).

---

## File Map

| Action | Path | Responsibility |
|---|---|---|
| Create | `scripts/executable_k8s-status.sh` | Pod list + ready status → JSON |
| Create | `scripts/executable_k8s-metrics.sh` | kubectl top + resourcequota → JSON |
| Create | `shared/Kubernetes.qml` | Singleton data layer |
| Create | `bar/KubernetesPill.qml` | Bar pill UI |
| Create | `bar/popouts/KubernetesPopout.qml` | Popout UI |
| Modify | `shared/Config.qml.tmpl` | Add kube config properties |
| Modify | `Bar.qml` | Wire pill + popout |

---

## Task 1: Config properties

**Files:**
- Modify: `shared/Config.qml.tmpl`

- [ ] **Step 1: Add kube properties to Config**

Open `shared/Config.qml.tmpl`. After the `gpuScript` line (line 36), add:

```qml
    // Kubernetes
    readonly property string kubeNamespace: "tenant-5"
    readonly property int kubeStatusRefreshMs: 30000
    readonly property int kubeMetricsRefreshMs: 15000
```

- [ ] **Step 2: Apply and verify Config is valid**

```bash
chezmoi apply ~/.config/quickshell/shared/Config.qml
# No output = success. If quickshell is running, it hot-reloads — check for errors in:
quickshell --version  # just confirm tool works
```

- [ ] **Step 3: Commit**

```bash
git add shared/Config.qml.tmpl
git commit -m "k8s-widget: add kube config properties"
```

---

## Task 2: k8s-status.sh

**Files:**
- Create: `scripts/executable_k8s-status.sh`

- [ ] **Step 1: Write the script**

Create `scripts/executable_k8s-status.sh`:

```bash
#!/usr/bin/env bash
# Outputs pod ready status for a namespace as JSON.
# Usage: k8s-status.sh <namespace>
set -euo pipefail

NS="${1:-tenant-5}"

pods_json=$(kubectl get pods -n "$NS" -o json 2>/dev/null) || exit 0

echo "$pods_json" | jq -c '
  [.items[] | {
    app: (.metadata.labels["app.kubernetes.io/instance"] // .metadata.name),
    ready: ((.status.conditions // [])
            | map(select(.type == "Ready"))
            | if length > 0 then .[0].status == "True" else false end)
  }] as $pods |
  {
    pods: $pods,
    readyCount: ($pods | map(select(.ready)) | length),
    totalCount: ($pods | length)
  }
'
```

- [ ] **Step 2: Test standalone**

```bash
chezmoi apply ~/.config/quickshell/scripts/k8s-status.sh
~/.config/quickshell/scripts/k8s-status.sh tenant-5 | jq .
```

Expected output:
```json
{
  "pods": [
    {"app": "dnddrugs", "ready": true},
    {"app": "joplin", "ready": true},
    {"app": "n8n", "ready": true},
    {"app": "shared-postgres", "ready": true}
  ],
  "readyCount": 4,
  "totalCount": 4
}
```

- [ ] **Step 3: Commit**

```bash
git add scripts/executable_k8s-status.sh
git commit -m "k8s-widget: add k8s-status.sh"
```

---

## Task 3: k8s-metrics.sh

**Files:**
- Create: `scripts/executable_k8s-metrics.sh`

- [ ] **Step 1: Write the script**

Create `scripts/executable_k8s-metrics.sh`:

```bash
#!/usr/bin/env bash
# Outputs per-pod CPU/MEM metrics and namespace quota as JSON.
# Usage: k8s-metrics.sh <namespace>
set -euo pipefail

NS="${1:-tenant-5}"

# ── Unit normalization ───────────────────────────────────────────────────────

# Normalize CPU string to millicores integer
normalize_cpu() {
    local val="$1"
    if [[ "$val" =~ ^([0-9]+)m$ ]]; then
        echo "${BASH_REMATCH[1]}"
    elif [[ "$val" =~ ^([0-9]+(\.[0-9]+)?)$ ]]; then
        awk "BEGIN { printf \"%d\", ${BASH_REMATCH[1]} * 1000 }"
    else
        echo 0
    fi
}

# Normalize memory string to MiB integer
normalize_mem() {
    local val="$1"
    if [[ "$val" =~ ^([0-9]+)Gi$ ]]; then
        echo $(( ${BASH_REMATCH[1]} * 1024 ))
    elif [[ "$val" =~ ^([0-9]+)Mi$ ]]; then
        echo "${BASH_REMATCH[1]}"
    elif [[ "$val" =~ ^([0-9]+)Ki$ ]]; then
        echo $(( ${BASH_REMATCH[1]} / 1024 ))
    elif [[ "$val" =~ ^([0-9]+)$ ]]; then
        echo $(( ${BASH_REMATCH[1]} / 1048576 ))
    else
        echo 0
    fi
}

# Format millicores for display ("420m" or "1.5c")
fmt_cpu() {
    local m="$1"
    if [[ $m -lt 1000 ]]; then
        echo "${m}m"
    else
        awk "BEGIN { printf \"%.1fc\", $m / 1000 }"
    fi
}

# Format MiB for display ("343Mi" or "1.50Gi")
fmt_mem() {
    local mib="$1"
    if [[ $mib -lt 1024 ]]; then
        echo "${mib}Mi"
    else
        awk "BEGIN { printf \"%.2fGi\", $mib / 1024 }"
    fi
}

# ── Fetch data ───────────────────────────────────────────────────────────────

pods_json=$(kubectl get pods -n "$NS" -o json 2>/dev/null)         || exit 0
top_output=$(kubectl top pods -n "$NS" --no-headers 2>/dev/null)   || top_output=""
quota_json=$(kubectl get resourcequota -n "$NS" -o json 2>/dev/null) || exit 0

# ── Build podName → app map ──────────────────────────────────────────────────

declare -A name_to_app
while IFS= read -r line; do
    pod_name=$(echo "$line" | jq -r '.name')
    app=$(echo "$line" | jq -r '.app')
    name_to_app["$pod_name"]="$app"
done < <(echo "$pods_json" | jq -c '.items[] | {name: .metadata.name, app: (.metadata.labels["app.kubernetes.io/instance"] // .metadata.name)}')

# ── Parse kubectl top ────────────────────────────────────────────────────────

declare -A top_cpu  # app → cpuM
declare -A top_mem  # app → memMi
cpu_actual=0
mem_actual=0

while IFS= read -r line; do
    [[ -z "$line" ]] && continue
    read -r pod_name cpu_str mem_str <<< "$line"
    app="${name_to_app[$pod_name]:-}"
    [[ -z "$app" ]] && continue
    cpu_m=$(normalize_cpu "$cpu_str")
    mem_mib=$(normalize_mem "$mem_str")
    top_cpu["$app"]=$cpu_m
    top_mem["$app"]=$mem_mib
    cpu_actual=$(( cpu_actual + cpu_m ))
    mem_actual=$(( mem_actual + mem_mib ))
done <<< "$top_output"

# ── Build podMetrics JSON array ──────────────────────────────────────────────

pod_metrics="["
first=1
while IFS= read -r line; do
    app=$(echo "$line" | jq -r '.app')
    cpu_m="${top_cpu[$app]:-"-1"}"
    mem_mib="${top_mem[$app]:-"-1"}"
    [[ $first -eq 0 ]] && pod_metrics+=","
    pod_metrics+=$(jq -nc --arg app "$app" --argjson cpu "$cpu_m" --argjson mem "$mem_mib" \
        '{app: $app, cpuM: $cpu, memMi: $mem}')
    first=0
done < <(echo "$pods_json" | jq -c '.items[] | {app: (.metadata.labels["app.kubernetes.io/instance"] // .metadata.name)}')
pod_metrics+="]"

# ── Parse resourcequota ──────────────────────────────────────────────────────

q_cpu_req_used=$(echo "$quota_json" | jq -r '.items[0].status.used["requests.cpu"]  // "0"')
q_cpu_req_hard=$(echo "$quota_json" | jq -r '.items[0].status.hard["requests.cpu"]  // "0"')
q_cpu_lim_used=$(echo "$quota_json" | jq -r '.items[0].status.used["limits.cpu"]    // "0"')
q_cpu_lim_hard=$(echo "$quota_json" | jq -r '.items[0].status.hard["limits.cpu"]    // "0"')
q_mem_req_used=$(echo "$quota_json" | jq -r '.items[0].status.used["requests.memory"] // "0"')
q_mem_req_hard=$(echo "$quota_json" | jq -r '.items[0].status.hard["requests.memory"] // "0"')
q_mem_lim_used=$(echo "$quota_json" | jq -r '.items[0].status.used["limits.memory"]   // "0"')
q_mem_lim_hard=$(echo "$quota_json" | jq -r '.items[0].status.hard["limits.memory"]   // "0"')

cpu_req_used_m=$(normalize_cpu "$q_cpu_req_used")
cpu_req_hard_m=$(normalize_cpu "$q_cpu_req_hard")
cpu_lim_used_m=$(normalize_cpu "$q_cpu_lim_used")
cpu_lim_hard_m=$(normalize_cpu "$q_cpu_lim_hard")
mem_req_used_mib=$(normalize_mem "$q_mem_req_used")
mem_req_hard_mib=$(normalize_mem "$q_mem_req_hard")
mem_lim_used_mib=$(normalize_mem "$q_mem_lim_used")
mem_lim_hard_mib=$(normalize_mem "$q_mem_lim_hard")

cpu_req_pct=$(awk "BEGIN { printf \"%.4f\", $cpu_req_used_m / ($cpu_req_hard_m > 0 ? $cpu_req_hard_m : 1) }")
cpu_lim_pct=$(awk "BEGIN { printf \"%.4f\", $cpu_lim_used_m / ($cpu_lim_hard_m > 0 ? $cpu_lim_hard_m : 1) }")
mem_req_pct=$(awk "BEGIN { printf \"%.4f\", $mem_req_used_mib / ($mem_req_hard_mib > 0 ? $mem_req_hard_mib : 1) }")
mem_lim_pct=$(awk "BEGIN { printf \"%.4f\", $mem_lim_used_mib / ($mem_lim_hard_mib > 0 ? $mem_lim_hard_mib : 1) }")

cpu_req_label="$(fmt_cpu $cpu_req_used_m) / $(fmt_cpu $cpu_req_hard_m)"
cpu_lim_label="$(fmt_cpu $cpu_lim_used_m) / $(fmt_cpu $cpu_lim_hard_m)"
mem_req_label="$(fmt_mem $mem_req_used_mib) / $(fmt_mem $mem_req_hard_mib)"
mem_lim_label="$(fmt_mem $mem_lim_used_mib) / $(fmt_mem $mem_lim_hard_mib)"

# ── Output ───────────────────────────────────────────────────────────────────

jq -nc \
    --argjson podMetrics "$pod_metrics" \
    --argjson cpuActualM  "$cpu_actual" \
    --argjson memActualMi "$mem_actual" \
    --argjson cpuReqPct   "$cpu_req_pct" \
    --argjson cpuLimPct   "$cpu_lim_pct" \
    --argjson memReqPct   "$mem_req_pct" \
    --argjson memLimPct   "$mem_lim_pct" \
    --arg     cpuReqLabel "$cpu_req_label" \
    --arg     cpuLimLabel "$cpu_lim_label" \
    --arg     memReqLabel "$mem_req_label" \
    --arg     memLimLabel "$mem_lim_label" \
    '{
        podMetrics: $podMetrics,
        quota: {
            cpuActualM:  $cpuActualM,
            memActualMi: $memActualMi,
            cpuReqPct:   $cpuReqPct,
            cpuLimPct:   $cpuLimPct,
            memReqPct:   $memReqPct,
            memLimPct:   $memLimPct,
            cpuReqLabel: $cpuReqLabel,
            cpuLimLabel: $cpuLimLabel,
            memReqLabel: $memReqLabel,
            memLimLabel: $memLimLabel
        }
    }'
```

- [ ] **Step 2: Test standalone**

```bash
chezmoi apply ~/.config/quickshell/scripts/k8s-metrics.sh
~/.config/quickshell/scripts/k8s-metrics.sh tenant-5 | jq .
```

Expected: JSON with `podMetrics` array (joplin/n8n/shared-postgres have real cpuM/memMi; dnddrugs has -1/-1) and `quota` object with percentages matching `kubectl get resourcequota -n tenant-5 -o json`.

Spot-check manually:
```bash
# cpuLimPct should be ~0.73 (1450m used / 2000m hard)
kubectl get resourcequota -n tenant-5 -o jsonpath='{.items[0].status}'
```

- [ ] **Step 3: Commit**

```bash
git add scripts/executable_k8s-metrics.sh
git commit -m "k8s-widget: add k8s-metrics.sh"
```

---

## Task 4: Kubernetes.qml Singleton

**Files:**
- Create: `shared/Kubernetes.qml`

- [ ] **Step 1: Write the Singleton**

Create `shared/Kubernetes.qml`:

```qml
pragma Singleton

import Quickshell
import Quickshell.Io
import QtQuick

Singleton {
    id: root

    property string status: "loading"
    property var pods: []
    property int readyCount: 0
    property int totalCount: 0
    property var quota: null
    property int lastUpdatedSecs: 0

    // Increment seconds since last successful status fetch
    Timer {
        interval: 1000
        running: root.status !== "loading"
        repeat: true
        onTriggered: root.lastUpdatedSecs++
    }

    // Staleness check — flip to "stale" if no update for >60s
    Timer {
        interval: 10000
        running: true
        repeat: true
        onTriggered: {
            if (root.lastUpdatedSecs > 60
                && (root.status === "ok" || root.status === "degraded")) {
                root.status = "stale";
            }
        }
    }

    // ─── Status script ────────────────────────────────────────────────────────
    Process {
        id: statusProc
        command: ["bash", Config.scriptsDir + "/k8s-status.sh", Config.kubeNamespace]
        stdout: StdioCollector {
            onStreamFinished: {
                if (this.text.trim() === "") {
                    root.status = (root.status === "ok" || root.status === "degraded")
                        ? "stale" : "error";
                    return;
                }
                try {
                    let data = JSON.parse(this.text);
                    // Preserve existing cpuM/memMi from last metrics fetch
                    let byApp = {};
                    for (let p of root.pods) byApp[p.app] = p;
                    root.pods = data.pods.map(p => ({
                        app:   p.app,
                        ready: p.ready,
                        cpuM:  byApp[p.app]?.cpuM  ?? -1,
                        memMi: byApp[p.app]?.memMi ?? -1
                    }));
                    root.readyCount = data.readyCount;
                    root.totalCount = data.totalCount;
                    root.status = data.pods.every(p => p.ready) ? "ok" : "degraded";
                    root.lastUpdatedSecs = 0;
                } catch(e) {
                    console.warn("Kubernetes: status parse error:", e);
                    root.status = (root.status === "ok" || root.status === "degraded")
                        ? "stale" : "error";
                }
            }
        }
    }

    Timer {
        interval: Config.kubeStatusRefreshMs
        running: true
        repeat: true
        onTriggered: statusProc.running = true
    }

    // ─── Metrics script ───────────────────────────────────────────────────────
    Process {
        id: metricsProc
        command: ["bash", Config.scriptsDir + "/k8s-metrics.sh", Config.kubeNamespace]
        stdout: StdioCollector {
            onStreamFinished: {
                if (this.text.trim() === "") return;
                try {
                    let data = JSON.parse(this.text);
                    let byApp = {};
                    for (let m of data.podMetrics) byApp[m.app] = m;
                    root.pods = root.pods.map(p => ({
                        app:   p.app,
                        ready: p.ready,
                        cpuM:  byApp[p.app]?.cpuM  ?? -1,
                        memMi: byApp[p.app]?.memMi ?? -1
                    }));
                    root.quota = data.quota;
                } catch(e) {
                    console.warn("Kubernetes: metrics parse error:", e);
                }
            }
        }
    }

    Timer {
        interval: Config.kubeMetricsRefreshMs
        running: true
        repeat: true
        onTriggered: metricsProc.running = true
    }

    // Stagger metrics 500ms after status to avoid a CPU spike at startup
    Timer {
        interval: 500
        repeat: false
        onTriggered: metricsProc.running = true
    }

    Component.onCompleted: statusProc.running = true
}
```

- [ ] **Step 2: Apply and verify quickshell loads without errors**

```bash
chezmoi apply ~/.config/quickshell/shared/Kubernetes.qml
# Restart quickshell. Check for QML errors:
# (kill and relaunch, or use your normal quickshell reload mechanism)
# The bar should appear as before — Kubernetes.qml is loaded but nothing displays yet.
```

- [ ] **Step 3: Commit**

```bash
git add shared/Kubernetes.qml
git commit -m "k8s-widget: add Kubernetes.qml singleton"
```

---

## Task 5: KubernetesPill + Bar wiring (pill only)

**Files:**
- Create: `bar/KubernetesPill.qml`
- Modify: `Bar.qml` (pill only — popout comes in Task 6)

- [ ] **Step 1: Write KubernetesPill.qml**

Create `bar/KubernetesPill.qml`:

```qml
import Quickshell
import QtQuick
import QtQuick.Layouts
import "../shared" as Shared

BarPill {
    groupName: "kubernetes"
    accentColor: {
        let s = Shared.Kubernetes.status;
        if (s === "ok")       return Shared.Theme.green;
        if (s === "degraded") return Shared.Theme.yellow;
        if (s === "error")    return Shared.Theme.red;
        if (s === "stale")    return Shared.Theme.overlay0;
        return Shared.Theme.blue;  // "loading"
    }

    content: [
        Text {
            Layout.alignment: Qt.AlignHCenter
            text: "\u{f03be}"  // 󱃾 Nerd Font kubernetes wheel
            color: {
                let s = Shared.Kubernetes.status;
                if (s === "ok")       return Shared.Theme.green;
                if (s === "degraded") return Shared.Theme.yellow;
                if (s === "error")    return Shared.Theme.red;
                if (s === "stale")    return Shared.Theme.overlay0;
                return Shared.Theme.blue;
            }
            font.pixelSize: Shared.Theme.fontLarge
            font.family: Shared.Theme.iconFont
        },
        Text {
            Layout.alignment: Qt.AlignHCenter
            text: Shared.Kubernetes.status === "loading"
                ? "…"
                : Shared.Kubernetes.readyCount + "/" + Shared.Kubernetes.totalCount
            color: {
                let s = Shared.Kubernetes.status;
                if (s === "ok")       return Shared.Theme.green;
                if (s === "degraded") return Shared.Theme.yellow;
                if (s === "error")    return Shared.Theme.red;
                if (s === "stale")    return Shared.Theme.overlay0;
                return Shared.Theme.blue;
            }
            font.pixelSize: Shared.Theme.fontSmall
            font.family: Shared.Theme.fontFamily
            font.bold: true
        }
    ]
}
```

- [ ] **Step 2: Add pill to Bar.qml**

In `Bar.qml`, find the line `BarComponents.SystemPill { id: systemBtn }` (currently the last item in the bottom section, line ~127). Add `KubernetesPill` **directly above it**:

```qml
                    BarComponents.KubernetesPill { id: kubernetesBtn }

                    BarComponents.SystemPill { id: systemBtn }
```

The surrounding context for orientation:
```qml
                    BarComponents.BarPill {
                        id: notifBtn
                        // ... (notification pill, unchanged)
                    }

                    BarComponents.KubernetesPill { id: kubernetesBtn }  // ← insert here

                    BarComponents.SystemPill { id: systemBtn }
```

- [ ] **Step 3: Apply and verify pill appears**

```bash
chezmoi apply ~/.config/quickshell/bar/KubernetesPill.qml \
              ~/.config/quickshell/Bar.qml
# Restart quickshell
# Expected: k8s wheel icon + "4/4" appears in the bar in green
# Clicking it does nothing yet (no popout wired)
```

- [ ] **Step 4: Commit**

```bash
git add bar/KubernetesPill.qml Bar.qml
git commit -m "k8s-widget: add KubernetesPill to bar"
```

---

## Task 6: KubernetesPopout + full Bar.qml wiring

**Files:**
- Create: `bar/popouts/KubernetesPopout.qml`
- Modify: `Bar.qml` (add PopoutSlot + update visible guard)

- [ ] **Step 1: Write KubernetesPopout.qml**

Create `bar/popouts/KubernetesPopout.qml`:

```qml
import Quickshell
import QtQuick
import QtQuick.Layouts
import "../../shared" as Shared

Item {
    id: root

    implicitWidth: Shared.Theme.popoutWidth
    implicitHeight: col.implicitHeight + Shared.Theme.popoutPadding * 2

    PopoutBackground { anchors.fill: parent }
    MouseArea { anchors.fill: parent }

    // Reusable quota bar component
    component QuotaBar: Column {
        property string rowLabel: ""
        property string valueLabel: ""
        property real fill: 0
        property color barColor: Shared.Theme.green
        property real barOpacity: 1.0

        Layout.fillWidth: true
        width: parent.width
        spacing: 3

        RowLayout {
            width: parent.width
            Text {
                text: rowLabel
                color: Shared.Theme.overlay0
                font.pixelSize: 9
                font.family: Shared.Theme.fontFamily
            }
            Item { Layout.fillWidth: true }
            Text {
                text: valueLabel
                color: Shared.Theme.subtext0
                font.pixelSize: 9
                font.family: Shared.Theme.fontFamily
            }
            Text {
                text: Math.round(fill * 100) + "%"
                color: barColor
                font.pixelSize: 9
                font.family: Shared.Theme.fontFamily
            }
        }
        Rectangle {
            width: parent.width
            height: 4
            color: Shared.Theme.surface1
            radius: 2
            Rectangle {
                width: Math.max(0, Math.min(1, parent.parent.fill)) * parent.width
                height: parent.height
                color: parent.parent.barColor
                opacity: parent.parent.barOpacity
                radius: 2
            }
        }
    }

    ColumnLayout {
        id: col
        anchors.left: parent.left
        anchors.right: parent.right
        anchors.verticalCenter: parent.verticalCenter
        anchors.margins: Shared.Theme.popoutPadding
        spacing: 8

        // ── Header ────────────────────────────────────────────
        RowLayout {
            Layout.fillWidth: true
            Text {
                text: "K8s — " + Shared.Config.kubeNamespace
                color: Shared.Theme.text
                font.pixelSize: 16
                font.family: Shared.Theme.fontFamily
                font.bold: true
                Layout.fillWidth: true
            }
            Text {
                text: "\u{f03be}"
                color: {
                    let s = Shared.Kubernetes.status;
                    if (s === "ok")       return Shared.Theme.green;
                    if (s === "degraded") return Shared.Theme.yellow;
                    if (s === "error")    return Shared.Theme.red;
                    if (s === "stale")    return Shared.Theme.overlay0;
                    return Shared.Theme.blue;
                }
                font.pixelSize: 20
                font.family: Shared.Theme.iconFont
            }
        }

        // ── Pods section ──────────────────────────────────────
        Text {
            text: "PODS"
            color: Shared.Theme.overlay0
            font.pixelSize: 9
            font.family: Shared.Theme.fontFamily
            font.letterSpacing: 1
        }

        // Column header row
        RowLayout {
            Layout.fillWidth: true
            spacing: 4
            Item { Layout.fillWidth: true }
            Text { text: "CPU";    color: Shared.Theme.overlay0; font.pixelSize: 8; font.family: Shared.Theme.fontFamily; Layout.preferredWidth: 44; horizontalAlignment: Text.AlignRight }
            Text { text: "MEM";    color: Shared.Theme.overlay0; font.pixelSize: 8; font.family: Shared.Theme.fontFamily; Layout.preferredWidth: 52; horizontalAlignment: Text.AlignRight }
            Text { text: "STATUS"; color: Shared.Theme.overlay0; font.pixelSize: 8; font.family: Shared.Theme.fontFamily; Layout.preferredWidth: 62; horizontalAlignment: Text.AlignRight }
        }

        // Pod rows
        Repeater {
            model: Shared.Kubernetes.pods
            delegate: Rectangle {
                required property var modelData
                Layout.fillWidth: true
                implicitHeight: 28
                color: Shared.Theme.surface0
                radius: Shared.Theme.radiusSmall

                RowLayout {
                    anchors.fill: parent
                    anchors.leftMargin: 10
                    anchors.rightMargin: 10
                    spacing: 4

                    Text {
                        text: modelData.app
                        color: Shared.Theme.text
                        font.pixelSize: 11
                        font.family: Shared.Theme.fontFamily
                        Layout.fillWidth: true
                        elide: Text.ElideRight
                    }
                    Text {
                        text: modelData.cpuM === -1 ? "—" : modelData.cpuM + "m"
                        color: Shared.Theme.sky
                        font.pixelSize: 11
                        font.family: Shared.Theme.fontFamily
                        Layout.preferredWidth: 44
                        horizontalAlignment: Text.AlignRight
                    }
                    Text {
                        text: modelData.memMi === -1 ? "—" : modelData.memMi + "Mi"
                        color: Shared.Theme.mauve
                        font.pixelSize: 11
                        font.family: Shared.Theme.fontFamily
                        Layout.preferredWidth: 52
                        horizontalAlignment: Text.AlignRight
                    }
                    RowLayout {
                        spacing: 3
                        Layout.preferredWidth: 62
                        layoutDirection: Qt.RightToLeft
                        Text {
                            text: modelData.ready ? "Ready" : "NotReady"
                            color: modelData.ready ? Shared.Theme.green : Shared.Theme.red
                            font.pixelSize: 10
                            font.family: Shared.Theme.fontFamily
                        }
                        Text {
                            text: "●"
                            color: modelData.ready ? Shared.Theme.green : Shared.Theme.red
                            font.pixelSize: 9
                        }
                    }
                }
            }
        }

        // ── Quota section ─────────────────────────────────────
        Text {
            text: "QUOTA"
            color: Shared.Theme.overlay0
            font.pixelSize: 9
            font.family: Shared.Theme.fontFamily
            font.letterSpacing: 1
            topPadding: 4
        }

        // CPU card
        Rectangle {
            Layout.fillWidth: true
            color: Shared.Theme.surface0
            radius: Shared.Theme.radiusSmall
            implicitHeight: cpuCardCol.implicitHeight + 20

            Column {
                id: cpuCardCol
                anchors.left: parent.left
                anchors.right: parent.right
                anchors.margins: 12
                anchors.verticalCenter: parent.verticalCenter
                spacing: 6

                RowLayout {
                    width: parent.width
                    Text { text: "CPU"; color: Shared.Theme.sky; font.pixelSize: 10; font.family: Shared.Theme.fontFamily; font.bold: true }
                    Item { Layout.fillWidth: true }
                    Text {
                        text: Shared.Kubernetes.quota ? Shared.Kubernetes.quota.cpuActualM + "m actual" : "—"
                        color: Shared.Theme.text
                        font.pixelSize: 10
                        font.family: Shared.Theme.fontFamily
                    }
                }
                QuotaBar {
                    rowLabel: "%REQ"
                    valueLabel: Shared.Kubernetes.quota?.cpuReqLabel ?? ""
                    fill: Shared.Kubernetes.quota?.cpuReqPct ?? 0
                    barColor: Shared.Theme.green
                }
                QuotaBar {
                    rowLabel: "%LIM"
                    valueLabel: Shared.Kubernetes.quota?.cpuLimLabel ?? ""
                    fill: Shared.Kubernetes.quota?.cpuLimPct ?? 0
                    barColor: Shared.Theme.yellow
                    barOpacity: 0.6
                }
            }
        }

        // MEM card
        Rectangle {
            Layout.fillWidth: true
            color: Shared.Theme.surface0
            radius: Shared.Theme.radiusSmall
            implicitHeight: memCardCol.implicitHeight + 20

            Column {
                id: memCardCol
                anchors.left: parent.left
                anchors.right: parent.right
                anchors.margins: 12
                anchors.verticalCenter: parent.verticalCenter
                spacing: 6

                RowLayout {
                    width: parent.width
                    Text { text: "MEM"; color: Shared.Theme.mauve; font.pixelSize: 10; font.family: Shared.Theme.fontFamily; font.bold: true }
                    Item { Layout.fillWidth: true }
                    Text {
                        text: Shared.Kubernetes.quota ? Shared.Kubernetes.quota.memActualMi + "Mi actual" : "—"
                        color: Shared.Theme.text
                        font.pixelSize: 10
                        font.family: Shared.Theme.fontFamily
                    }
                }
                QuotaBar {
                    rowLabel: "%REQ"
                    valueLabel: Shared.Kubernetes.quota?.memReqLabel ?? ""
                    fill: Shared.Kubernetes.quota?.memReqPct ?? 0
                    barColor: Shared.Theme.mauve
                }
                QuotaBar {
                    rowLabel: "%LIM"
                    valueLabel: Shared.Kubernetes.quota?.memLimLabel ?? ""
                    fill: Shared.Kubernetes.quota?.memLimPct ?? 0
                    barColor: Shared.Theme.mauve
                    barOpacity: 0.6
                }
            }
        }

        // ── Footer ────────────────────────────────────────────
        Text {
            text: "Updated " + Shared.Kubernetes.lastUpdatedSecs + "s ago"
            color: Shared.Theme.overlay0
            font.pixelSize: 9
            font.family: Shared.Theme.fontFamily
            Layout.alignment: Qt.AlignRight
        }
    }
}
```

- [ ] **Step 2: Add PopoutSlot to Bar.qml and update visible guard**

In `Bar.qml`, inside the `Item { id: popoutArea }` block (around line 175), add after the `systemSlot` PopoutSlot:

```qml
                    PopoutSlot {
                        id: kubernetesSlot
                        name: "kubernetes"
                        verticalAnchor: "bottom"
                        sourceComponent: Popouts.KubernetesPopout {}
                    }
```

Then update the `visible:` line on `popoutWindow` (line 145) to include `kubernetesSlot.animating`:

```qml
                visible: modelData.name === Shared.Config.monitor && (popoutOpen || notifSlot.animating || mediaSlot.animating || weatherSlot.animating || datetimeSlot.animating || systemSlot.animating || kubernetesSlot.animating)
```

- [ ] **Step 3: Apply and verify full widget**

```bash
chezmoi apply ~/.config/quickshell/bar/popouts/KubernetesPopout.qml \
              ~/.config/quickshell/Bar.qml
# Restart quickshell
```

Verify:
1. Pill shows `4/4` in green
2. Click pill → popout opens (smooth animation matching other popouts)
3. Pod table shows all 4 pods; joplin/n8n/shared-postgres have CPU/MEM values; dnddrugs shows `—`
4. Quota bars for CPU and MEM match values from:
   ```bash
   kubectl top pods -n tenant-5
   kubectl get resourcequota -n tenant-5 -o json
   ```
5. Footer shows "Updated Xs ago" incrementing each second
6. Click outside popout → closes cleanly

- [ ] **Step 4: Test degraded state**

```bash
kubectl rollout restart deployment/joplin -n tenant-5
# Watch pill: within ~30s it should turn yellow and show 3/4
# Once joplin pod is Ready again: pill returns to green 4/4
```

- [ ] **Step 5: Commit**

```bash
git add bar/popouts/KubernetesPopout.qml Bar.qml
git commit -m "k8s-widget: add KubernetesPopout and complete bar wiring"
```

---

## Verification Checklist

```bash
# Scripts work standalone
~/.config/quickshell/scripts/k8s-status.sh tenant-5 | jq .
~/.config/quickshell/scripts/k8s-metrics.sh tenant-5 | jq .

# Full widget smoke test (after chezmoi apply + quickshell restart)
# ✓ Pill visible with correct count + green color
# ✓ Popout opens on click, closes on outside click or Escape
# ✓ Pod table accurate (compare to: kubectl get pods -n tenant-5)
# ✓ Quota bars accurate (compare to: kubectl get resourcequota -n tenant-5 -o json)
# ✓ dnddrugs row shows — for CPU/MEM
# ✓ No animation glitches (popout fades/scales like other popouts)
# ✓ Degraded state: kubectl rollout restart deployment/joplin → yellow pill, NotReady row
```
