diff --git a/.gemini/settings.json b/.gemini/settings.json
new file mode 100644
index 0000000..7ca6cb9
--- /dev/null
+++ b/.gemini/settings.json
@@ -0,0 +1,10 @@
+{
+ "hooksConfig": {
+ "disabled": [
+ "security-gate",
+ "slm-memory-sync",
+ "dynamic-context",
+ "slm-observe"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/dot_config/hypr/hyprland.d.lua/input.lua.tmpl b/dot_config/hypr/hyprland.d.lua/input.lua.tmpl
index 9195c65..6489f5b 100644
--- a/dot_config/hypr/hyprland.d.lua/input.lua.tmpl
+++ b/dot_config/hypr/hyprland.d.lua/input.lua.tmpl
@@ -7,7 +7,7 @@ hl.config({
numlock_by_default = true,
repeat_rate = 25,
repeat_delay = 600,
- follow_mouse = 1,
+ follow_mouse = 0,
focus_on_close = 2,
mouse_refocus = true,
diff --git a/dot_config/hypr/hyprland.lua.refactor/hyprland.lua b/dot_config/hypr/hyprland.lua.refactor/hyprland.lua
new file mode 100644
index 0000000..985267a
--- /dev/null
+++ b/dot_config/hypr/hyprland.lua.refactor/hyprland.lua
@@ -0,0 +1,366 @@
+-- This is an example Hyprland Lua config file.
+-- Refer to the wiki for more information.
+-- https://wiki.hypr.land/Configuring/Start/
+
+-- Please note not all available settings / options are set here.
+-- For a full list, see the wiki
+
+-- You can (and should!!) split this configuration into multiple files
+-- Create your files separately and then require them like this:
+-- require("myColors")
+
+------------------
+---- MONITORS ----
+------------------
+
+-- See https://wiki.hypr.land/Configuring/Basics/Monitors/
+hl.monitor({
+ output = "",
+ mode = "preferred",
+ position = "auto",
+ scale = "auto",
+})
+
+---------------------
+---- MY PROGRAMS ----
+---------------------
+
+-- Set programs that you use
+local terminal = "kitty"
+local fileManager = "dolphin"
+local menu = "hyprlauncher"
+
+-------------------
+---- AUTOSTART ----
+-------------------
+
+-- See https://wiki.hypr.land/Configuring/Basics/Autostart/
+
+-- Autostart necessary processes (like notifications daemons, status bars, etc.)
+-- Or execute your favorite apps at launch like this:
+--
+-- hl.on("hyprland.start", function ()
+-- hl.exec_cmd(terminal)
+-- hl.exec_cmd("nm-applet")
+-- hl.exec_cmd("waybar & hyprpaper & firefox")
+-- end)
+
+-------------------------------
+---- ENVIRONMENT VARIABLES ----
+-------------------------------
+
+-- See https://wiki.hypr.land/Configuring/Advanced-and-Cool/Environment-variables/
+
+hl.env("XCURSOR_SIZE", "24")
+hl.env("HYPRCURSOR_SIZE", "24")
+
+-----------------------
+----- PERMISSIONS -----
+-----------------------
+
+-- See https://wiki.hypr.land/Configuring/Advanced-and-Cool/Permissions/
+-- Please note permission changes here require a Hyprland restart and are not applied on-the-fly
+-- for security reasons
+
+-- hl.config({
+-- ecosystem = {
+-- enforce_permissions = true,
+-- },
+-- })
+
+-- hl.permission("/usr/(bin|local/bin)/grim", "screencopy", "allow")
+-- hl.permission("/usr/(lib|libexec|lib64)/xdg-desktop-portal-hyprland", "screencopy", "allow")
+-- hl.permission("/usr/(bin|local/bin)/hyprpm", "plugin", "allow")
+
+-----------------------
+---- LOOK AND FEEL ----
+-----------------------
+
+-- Refer to https://wiki.hypr.land/Configuring/Basics/Variables/
+hl.config({
+ general = {
+ gaps_in = 5,
+ gaps_out = 20,
+
+ border_size = 2,
+
+ col = {
+ active_border = { colors = { "rgba(33ccffee)", "rgba(00ff99ee)" }, angle = 45 },
+ inactive_border = "rgba(595959aa)",
+ },
+
+ -- Set to true to enable resizing windows by clicking and dragging on borders and gaps
+ resize_on_border = false,
+
+ -- Please see https://wiki.hypr.land/Configuring/Advanced-and-Cool/Tearing/ before you turn this on
+ allow_tearing = false,
+
+ layout = "dwindle",
+ },
+
+ decoration = {
+ rounding = 10,
+ rounding_power = 2,
+
+ -- Change transparency of focused and unfocused windows
+ active_opacity = 1.0,
+ inactive_opacity = 1.0,
+
+ shadow = {
+ enabled = true,
+ range = 4,
+ render_power = 3,
+ color = 0xee1a1a1a,
+ },
+
+ blur = {
+ enabled = true,
+ size = 3,
+ passes = 1,
+ vibrancy = 0.1696,
+ },
+ },
+
+ animations = {
+ enabled = true,
+ },
+})
+
+-- Default curves and animations, see https://wiki.hypr.land/Configuring/Advanced-and-Cool/Animations/
+hl.curve("easeOutQuint", { type = "bezier", points = { { 0.23, 1 }, { 0.32, 1 } } })
+hl.curve("easeInOutCubic", { type = "bezier", points = { { 0.65, 0.05 }, { 0.36, 1 } } })
+hl.curve("linear", { type = "bezier", points = { { 0, 0 }, { 1, 1 } } })
+hl.curve("almostLinear", { type = "bezier", points = { { 0.5, 0.5 }, { 0.75, 1 } } })
+hl.curve("quick", { type = "bezier", points = { { 0.15, 0 }, { 0.1, 1 } } })
+
+-- Default springs
+hl.curve("easy", { type = "spring", mass = 1, stiffness = 71.2633, dampening = 15.8273644 })
+
+hl.animation({ leaf = "global", enabled = true, speed = 10, bezier = "default" })
+hl.animation({ leaf = "border", enabled = true, speed = 5.39, bezier = "easeOutQuint" })
+hl.animation({ leaf = "windows", enabled = true, speed = 4.79, spring = "easy" })
+hl.animation({ leaf = "windowsIn", enabled = true, speed = 4.1, spring = "easy", style = "popin 87%" })
+hl.animation({ leaf = "windowsOut", enabled = true, speed = 1.49, bezier = "linear", style = "popin 87%" })
+hl.animation({ leaf = "fadeIn", enabled = true, speed = 1.73, bezier = "almostLinear" })
+hl.animation({ leaf = "fadeOut", enabled = true, speed = 1.46, bezier = "almostLinear" })
+hl.animation({ leaf = "fade", enabled = true, speed = 3.03, bezier = "quick" })
+hl.animation({ leaf = "layers", enabled = true, speed = 3.81, bezier = "easeOutQuint" })
+hl.animation({ leaf = "layersIn", enabled = true, speed = 4, bezier = "easeOutQuint", style = "fade" })
+hl.animation({ leaf = "layersOut", enabled = true, speed = 1.5, bezier = "linear", style = "fade" })
+hl.animation({ leaf = "fadeLayersIn", enabled = true, speed = 1.79, bezier = "almostLinear" })
+hl.animation({ leaf = "fadeLayersOut", enabled = true, speed = 1.39, bezier = "almostLinear" })
+hl.animation({ leaf = "workspaces", enabled = true, speed = 1.94, bezier = "almostLinear", style = "fade" })
+hl.animation({ leaf = "workspacesIn", enabled = true, speed = 1.21, bezier = "almostLinear", style = "fade" })
+hl.animation({ leaf = "workspacesOut", enabled = true, speed = 1.94, bezier = "almostLinear", style = "fade" })
+hl.animation({ leaf = "zoomFactor", enabled = true, speed = 7, bezier = "quick" })
+
+-- Ref https://wiki.hypr.land/Configuring/Basics/Workspace-Rules/
+-- "Smart gaps" / "No gaps when only"
+-- uncomment all if you wish to use that.
+-- hl.workspace_rule({ workspace = "w[tv1]", gaps_out = 0, gaps_in = 0 })
+-- hl.workspace_rule({ workspace = "f[1]", gaps_out = 0, gaps_in = 0 })
+-- hl.window_rule({
+-- name = "no-gaps-wtv1",
+-- match = { float = false, workspace = "w[tv1]" },
+-- border_size = 0,
+-- rounding = 0,
+-- })
+-- hl.window_rule({
+-- name = "no-gaps-f1",
+-- match = { float = false, workspace = "f[1]" },
+-- border_size = 0,
+-- rounding = 0,
+-- })
+
+-- See https://wiki.hypr.land/Configuring/Layouts/Dwindle-Layout/ for more
+hl.config({
+ dwindle = {
+ preserve_split = true, -- You probably want this
+ },
+})
+
+-- See https://wiki.hypr.land/Configuring/Layouts/Master-Layout/ for more
+hl.config({
+ master = {
+ new_status = "master",
+ },
+})
+
+-- See https://wiki.hypr.land/Configuring/Layouts/Scrolling-Layout/ for more
+hl.config({
+ scrolling = {
+ fullscreen_on_one_column = true,
+ },
+})
+
+----------------
+---- MISC ----
+----------------
+
+hl.config({
+ misc = {
+ force_default_wallpaper = -1, -- Set to 0 or 1 to disable the anime mascot wallpapers
+ disable_hyprland_logo = false, -- If true disables the random hyprland logo / anime girl background. :(
+ },
+})
+
+---------------
+---- INPUT ----
+---------------
+
+hl.config({
+ input = {
+ kb_layout = "us",
+ kb_variant = "",
+ kb_model = "",
+ kb_options = "",
+ kb_rules = "",
+
+ follow_mouse = 1,
+
+ sensitivity = 0, -- -1.0 - 1.0, 0 means no modification.
+
+ touchpad = {
+ natural_scroll = false,
+ },
+ },
+})
+
+hl.gesture({
+ fingers = 3,
+ direction = "horizontal",
+ action = "workspace",
+})
+
+-- Example per-device config
+-- See https://wiki.hypr.land/Configuring/Advanced-and-Cool/Devices/ for more
+hl.device({
+ name = "epic-mouse-v1",
+ sensitivity = -0.5,
+})
+
+---------------------
+---- KEYBINDINGS ----
+---------------------
+
+local mainMod = "SUPER" -- Sets "Windows" key as main modifier
+
+-- Example binds, see https://wiki.hypr.land/Configuring/Basics/Binds/ for more
+hl.bind(mainMod .. " + Q", hl.dsp.exec_cmd(terminal))
+local closeWindowBind = hl.bind(mainMod .. " + C", hl.dsp.window.close())
+-- closeWindowBind:set_enabled(false)
+hl.bind(
+ mainMod .. " + M",
+ hl.dsp.exec_cmd("command -v hyprshutdown >/dev/null 2>&1 && hyprshutdown || hyprctl dispatch 'hl.dsp.exit()'")
+)
+hl.bind(mainMod .. " + E", hl.dsp.exec_cmd(fileManager))
+hl.bind(mainMod .. " + V", hl.dsp.window.float({ action = "toggle" }))
+hl.bind(mainMod .. " + R", hl.dsp.exec_cmd(menu))
+hl.bind(mainMod .. " + P", hl.dsp.window.pseudo())
+hl.bind(mainMod .. " + J", hl.dsp.layout("togglesplit")) -- dwindle only
+
+-- Move focus with mainMod + arrow keys
+hl.bind(mainMod .. " + left", hl.dsp.focus({ direction = "left" }))
+hl.bind(mainMod .. " + right", hl.dsp.focus({ direction = "right" }))
+hl.bind(mainMod .. " + up", hl.dsp.focus({ direction = "up" }))
+hl.bind(mainMod .. " + down", hl.dsp.focus({ direction = "down" }))
+
+-- Switch workspaces with mainMod + [0-9]
+-- Move active window to a workspace with mainMod + SHIFT + [0-9]
+for i = 1, 10 do
+ local key = i % 10 -- 10 maps to key 0
+ hl.bind(mainMod .. " + " .. key, hl.dsp.focus({ workspace = i }))
+ hl.bind(mainMod .. " + SHIFT + " .. key, hl.dsp.window.move({ workspace = i }))
+end
+
+-- Example special workspace (scratchpad)
+hl.bind(mainMod .. " + S", hl.dsp.workspace.toggle_special("magic"))
+hl.bind(mainMod .. " + SHIFT + S", hl.dsp.window.move({ workspace = "special:magic" }))
+
+-- Scroll through existing workspaces with mainMod + scroll
+hl.bind(mainMod .. " + mouse_down", hl.dsp.focus({ workspace = "e+1" }))
+hl.bind(mainMod .. " + mouse_up", hl.dsp.focus({ workspace = "e-1" }))
+
+-- Move/resize windows with mainMod + LMB/RMB and dragging
+hl.bind(mainMod .. " + mouse:272", hl.dsp.window.drag(), { mouse = true })
+hl.bind(mainMod .. " + mouse:273", hl.dsp.window.resize(), { mouse = true })
+
+-- Laptop multimedia keys for volume and LCD brightness
+hl.bind(
+ "XF86AudioRaiseVolume",
+ hl.dsp.exec_cmd("wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%+"),
+ { locked = true, repeating = true }
+)
+hl.bind(
+ "XF86AudioLowerVolume",
+ hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"),
+ { locked = true, repeating = true }
+)
+hl.bind(
+ "XF86AudioMute",
+ hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"),
+ { locked = true, repeating = true }
+)
+hl.bind(
+ "XF86AudioMicMute",
+ hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle"),
+ { locked = true, repeating = true }
+)
+hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd("brightnessctl -e4 -n2 set 5%+"), { locked = true, repeating = true })
+hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd("brightnessctl -e4 -n2 set 5%-"), { locked = true, repeating = true })
+
+-- Requires playerctl
+hl.bind("XF86AudioNext", hl.dsp.exec_cmd("playerctl next"), { locked = true })
+hl.bind("XF86AudioPause", hl.dsp.exec_cmd("playerctl play-pause"), { locked = true })
+hl.bind("XF86AudioPlay", hl.dsp.exec_cmd("playerctl play-pause"), { locked = true })
+hl.bind("XF86AudioPrev", hl.dsp.exec_cmd("playerctl previous"), { locked = true })
+
+--------------------------------
+---- WINDOWS AND WORKSPACES ----
+--------------------------------
+
+-- See https://wiki.hypr.land/Configuring/Basics/Window-Rules/
+-- and https://wiki.hypr.land/Configuring/Basics/Workspace-Rules/
+
+-- Example window rules that are useful
+
+local suppressMaximizeRule = hl.window_rule({
+ -- Ignore maximize requests from all apps. You'll probably like this.
+ name = "suppress-maximize-events",
+ match = { class = ".*" },
+
+ suppress_event = "maximize",
+})
+-- suppressMaximizeRule:set_enabled(false)
+
+hl.window_rule({
+ -- Fix some dragging issues with XWayland
+ name = "fix-xwayland-drags",
+ match = {
+ class = "^$",
+ title = "^$",
+ xwayland = true,
+ float = true,
+ fullscreen = false,
+ pin = false,
+ },
+
+ no_focus = true,
+})
+
+-- Layer rules also return a handle.
+-- local overlayLayerRule = hl.layer_rule({
+-- name = "no-anim-overlay",
+-- match = { namespace = "^my-overlay$" },
+-- no_anim = true,
+-- })
+-- overlayLayerRule:set_enabled(false)
+
+-- Hyprland-run windowrule
+hl.window_rule({
+ name = "move-hyprland-run",
+ match = { class = "hyprland-run" },
+
+ move = "20 monitor_h-120",
+ float = true,
+})
diff --git a/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/.server-stopped b/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/.server-stopped
new file mode 100644
index 0000000..c678256
--- /dev/null
+++ b/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/.server-stopped
@@ -0,0 +1 @@
+{"reason":"idle timeout","timestamp":1776812893183}
diff --git a/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/.server.log b/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/.server.log
new file mode 100644
index 0000000..4f5de78
--- /dev/null
+++ b/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/.server.log
@@ -0,0 +1,16 @@
+{"type":"server-started","port":57943,"host":"127.0.0.1","url_host":"localhost","url":"http://localhost:57943","screen_dir":"/home/mpuchstein/.local/share/chezmoi/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192"}
+{"type":"screen-added","file":"/home/mpuchstein/.local/share/chezmoi/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/pill-options.html"}
+{"source":"user-event","type":"click","text":"\n 3/3\n \n \n \n B — Icon + node ratio\n Shows ready nodes vs total (e.g. 3/3). Immediately tells you if a node is down.","choice":"b","id":null,"timestamp":1776810288228}
+{"source":"user-event","type":"click","text":"\n 3/3\n \n \n \n B — Icon + node ratio\n Shows ready nodes vs total (e.g. 3/3). Immediately tells you if a node is down.","choice":"b","id":null,"timestamp":1776810289433}
+{"source":"user-event","type":"click","text":"\n 3/3\n \n \n \n B — Icon + node ratio\n Shows ready nodes vs total (e.g. 3/3). Immediately tells you if a node is down.","choice":"b","id":null,"timestamp":1776810289627}
+{"type":"screen-added","file":"/home/mpuchstein/.local/share/chezmoi/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/pill-combined.html"}
+{"source":"user-event","type":"click","text":"\n 3/3\n \n \n \n B — Icon + ratio (ratio IS the status)\n Two rows. Ratio color encodes health: green = all ready, yellow = degraded, red = critical. No redundant \"OK\" label.","choice":"b","id":null,"timestamp":1776810328915}
+{"type":"screen-added","file":"/home/mpuchstein/.local/share/chezmoi/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/popout-layout.html"}
+{"type":"screen-added","file":"/home/mpuchstein/.local/share/chezmoi/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/popout-layout-v2.html"}
+{"type":"screen-added","file":"/home/mpuchstein/.local/share/chezmoi/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/popout-layout-v3.html"}
+{"source":"user-event","type":"click","text":"A\n Bars with actual + % labelsVisual bars, more scannable","choice":"a","id":null,"timestamp":1776810810800}
+{"type":"screen-added","file":"/home/mpuchstein/.local/share/chezmoi/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/waiting.html"}
+{"type":"screen-added","file":"/home/mpuchstein/.local/share/chezmoi/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/popout-layout-v4.html"}
+{"source":"user-event","type":"click","text":"✓\n \n This works\n Per-pod table + quota bars with real numbers","choice":"looks-good","id":null,"timestamp":1776811068409}
+{"type":"screen-added","file":"/home/mpuchstein/.local/share/chezmoi/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/waiting-2.html"}
+{"type":"server-stopped","reason":"idle timeout"}
diff --git a/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/.server.pid b/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/.server.pid
new file mode 100644
index 0000000..a016f52
--- /dev/null
+++ b/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/.server.pid
@@ -0,0 +1 @@
+128167
diff --git a/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/pill-combined.html b/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/pill-combined.html
new file mode 100644
index 0000000..38234ed
--- /dev/null
+++ b/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/pill-combined.html
@@ -0,0 +1,33 @@
+
Combined: icon + node ratio + status color
+Two variations on how to stack all three elements in the 52px pill.
+
+
+
+
+
+
+
A — Icon / ratio / status stacked
+
Three rows. Icon accent color matches status. Ratio stays neutral text color.
+
+
+
+
+
+
+
B — Icon + ratio (ratio IS the status)
+
Two rows. Ratio color encodes health: green = all ready, yellow = degraded, red = critical. No redundant "OK" label.
+
+
+
+
diff --git a/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/pill-options.html b/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/pill-options.html
new file mode 100644
index 0000000..b4cd008
--- /dev/null
+++ b/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/pill-options.html
@@ -0,0 +1,44 @@
+What should the bar pill show?
+The pill sits in the left sidebar (52px wide). Pick the compact display style you prefer.
+
+
+
+
+
+
+
A — Icon only
+
Pill color shifts: green = healthy, yellow = degraded, red = critical. Minimal, like the system pill.
+
+
+
+
+
+
+
B — Icon + node ratio
+
Shows ready nodes vs total (e.g. 3/3). Immediately tells you if a node is down.
+
+
+
+
+
+
+
C — Icon + status text
+
Shows OK / WARN / ERR in the pill accent color. Clear at a glance, no counting required.
+
+
+
+
diff --git a/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/popout-layout-v2.html b/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/popout-layout-v2.html
new file mode 100644
index 0000000..6dbbc79
--- /dev/null
+++ b/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/popout-layout-v2.html
@@ -0,0 +1,94 @@
+Updated layout — namespace quota instead of cluster metrics
+Nodes show status only (no metrics-server needed). Resources show quota consumption from kubectl.
+
+
+
+
+
+
+
+
Nodes
+
+
+ node-1
+ ● Ready
+
+
+ node-2
+ ● Ready
+
+
+ node-3
+ ● NotReady
+
+
+
+
+
+
+
+
+
+
Namespace Quota — tenant-5
+
+
+
+ CPU requests
+ 1.2 / 4 cores
+
+
+
+
+
+ Memory requests
+ 3.4 / 8 GiB
+
+
+
+
+
+
+
+
+
Updated 8s ago
+
+
+
+
+
+
+
✓
+
+
This works
+
Node status, pod counts, namespace quota bars
+
+
+
diff --git a/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/popout-layout-v3.html b/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/popout-layout-v3.html
new file mode 100644
index 0000000..649d826
--- /dev/null
+++ b/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/popout-layout-v3.html
@@ -0,0 +1,113 @@
+Namespace resources — k9s-style columns
+Aggregated across all pods in tenant-5. Two options for how to display the data.
+
+
+
+
+
+
+
+
Namespace Resources
+
+
+
+
+
+
+
+ MEM
+ 2.8 GiB
+
+
+
+
+
+
+
+
+
+
+
+
+
Namespace Resources
+
+
+
+
+ ACTUAL
+ %R
+ %L
+
+
+
+
+ CPU
+ 420m
+ 35%
+ 21%
+
+
+
+
+ MEM
+ 2.8G
+ 58%
+ 34%
+
+
+
%R = % of requests quota · %L = % of limits quota
+
+
+
+
+
+
+
+
+
A
+
Bars with actual + % labels
Visual bars, more scannable
+
+
+
B
+
Compact table row
Denser, mirrors k9s columns exactly
+
+
diff --git a/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/popout-layout-v4.html b/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/popout-layout-v4.html
new file mode 100644
index 0000000..89c618f
--- /dev/null
+++ b/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/popout-layout-v4.html
@@ -0,0 +1,117 @@
+Updated layout — pods replace nodes, per-pod metrics
+Based on your real kubectl data. dnddrugs absent from kubectl top → shown as —
+
+
+
+
+
+
+
+
Pods
+
+
+ CPUMEMSTATUS
+
+
+
+ joplin
+ 13m
+ 343Mi
+ ● Ready
+
+
+ n8n
+ 6m
+ 427Mi
+ ● Ready
+
+
+ shared-pg
+ 10m
+ 151Mi
+ ● Ready
+
+
+ dnddrugs
+ —
+ —
+ ● Ready
+
+
+
+
+
+
+
Quota — tenant-5
+
+
+
+
+
+ CPU
+ 29m actual
+
+
+
+ %REQ 78m / 2000m
+ 4%
+
+
+
+
+
+ %LIM 1450m / 2000m
+ 73%
+
+
+
+
+
+
+
+
+ MEM
+ 921Mi actual
+
+
+
+ %REQ 1.01Gi / 4Gi
+ 25%
+
+
+
+
+
+ %LIM 2.14Gi / 4Gi
+ 54%
+
+
+
+
+
+
+
+
+
Updated 8s ago
+
+
+
+Pill now shows ready/total pods, e.g. 4/4 green → 3/4 yellow if one goes unready.
+
+
+
+
✓
+
+
This works
+
Per-pod table + quota bars with real numbers
+
+
+
diff --git a/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/popout-layout.html b/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/popout-layout.html
new file mode 100644
index 0000000..5bcc1d1
--- /dev/null
+++ b/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/popout-layout.html
@@ -0,0 +1,97 @@
+Popout layout — does this cover what you need?
+320px wide panel. Click to select if this looks right, or describe what's missing below.
+
+
+
+
+
+
+
+
Nodes
+
+
+
node-1
+
+ CPU 38%
+ MEM 61%
+ ● Ready
+
+
+
+
node-2
+
+ CPU 12%
+ MEM 44%
+ ● Ready
+
+
+
+
node-3
+
+ CPU —
+ MEM —
+ ● NotReady
+
+
+
+
+
+
+
+
+
+
+
Cluster Resources
+
+
+
+ CPU
+ 1.8 / 8 cores
+
+
+
+
+
+ Memory
+ 5.2 / 16 GiB
+
+
+
+
+
+
+
+
Updated 14s ago
+
+
+
+
+
+
+
✓
+
+
This covers it
+
Nodes with per-node CPU/mem/status, pod counts, cluster resource bars
+
+
+
diff --git a/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/waiting-2.html b/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/waiting-2.html
new file mode 100644
index 0000000..ef07652
--- /dev/null
+++ b/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/waiting-2.html
@@ -0,0 +1,3 @@
+
+
Continuing in terminal...
+
diff --git a/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/waiting.html b/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/waiting.html
new file mode 100644
index 0000000..ef07652
--- /dev/null
+++ b/dot_config/quickshell/.superpowers/brainstorm/128159-1776810192/waiting.html
@@ -0,0 +1,3 @@
+
+
Continuing in terminal...
+
diff --git a/dot_config/quickshell/bar/Workspaces.qml b/dot_config/quickshell/bar/Workspaces.qml
index 44d4487..ad880a3 100644
--- a/dot_config/quickshell/bar/Workspaces.qml
+++ b/dot_config/quickshell/bar/Workspaces.qml
@@ -66,9 +66,9 @@ Item {
acceptedButtons: Qt.NoButton
onWheel: event => {
if (event.angleDelta.y > 0)
- Hyprland.dispatch("workspace m-1");
+ Hyprland.dispatch("hl.dsp.focus({ workspace = 'm-1' })");
else
- Hyprland.dispatch("workspace m+1");
+ Hyprland.dispatch("hl.dsp.focus({ workspace = 'm+1' })");
}
}
@@ -155,7 +155,7 @@ Item {
MouseArea {
anchors.fill: parent
- onClicked: Hyprland.dispatch("workspace name:" + wsItem.wsName)
+ onClicked: Hyprland.dispatch("hl.dsp.focus({ workspace = 'name:" + wsItem.wsName + "' })")
}
}
}
diff --git a/dot_config/quickshell/docs/superpowers/plans/2026-04-22-k8s-monitoring-widget.md b/dot_config/quickshell/docs/superpowers/plans/2026-04-22-k8s-monitoring-widget.md
new file mode 100644
index 0000000..2770ddd
--- /dev/null
+++ b/dot_config/quickshell/docs/superpowers/plans/2026-04-22-k8s-monitoring-widget.md
@@ -0,0 +1,947 @@
+# 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
+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
+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
+```
diff --git a/dot_config/quickshell/docs/superpowers/specs/2026-04-22-k8s-monitoring-widget-design.md b/dot_config/quickshell/docs/superpowers/specs/2026-04-22-k8s-monitoring-widget-design.md
new file mode 100644
index 0000000..f9ff8bd
--- /dev/null
+++ b/dot_config/quickshell/docs/superpowers/specs/2026-04-22-k8s-monitoring-widget-design.md
@@ -0,0 +1,279 @@
+# K8s Monitoring Widget — Design Spec
+
+**Date:** 2026-04-22
+**Status:** Approved
+
+---
+
+## Context
+
+The user hosts several applications (joplin, n8n, grocy, dnddrugs, shared-postgres) on a Kubernetes cluster (ITSH Cloud, namespace `tenant-5`). There is no always-open terminal or dashboard for at-a-glance cluster health. The goal is a Quickshell bar widget that surfaces pod health and resource pressure without leaving the desktop.
+
+**Constraints discovered during design:**
+- `kubectl get nodes` is forbidden — the service account only has namespace scope
+- `kubectl top pods` works — metrics-server is available
+- `kubectl get resourcequota` works — quota has both requests and limits tracked
+- Pods absent from `kubectl top` (e.g. dnddrugs) should show `—`, not an error
+
+**Dependencies:** `kubectl` and `jq` must be in `$PATH` on the host.
+
+---
+
+## Files
+
+### Create
+
+| File | Purpose |
+|---|---|
+| `scripts/executable_k8s-status.sh` | Fetches pod list + ready status → JSON (30s poll) |
+| `scripts/executable_k8s-metrics.sh` | Fetches kubectl top + resourcequota → JSON (15s poll) |
+| `shared/Kubernetes.qml` | Singleton: owns all k8s state, runs both scripts, exposes properties |
+| `bar/KubernetesPill.qml` | Bar pill: k8s icon + ready/total pod ratio, color-coded by health |
+| `bar/popouts/KubernetesPopout.qml` | Popout: per-pod table + quota bars |
+
+> The `executable_` prefix is the chezmoi convention (see `scripts/executable_gpu.sh`). Chezmoi maps these to plain filenames at apply time with the executable bit set.
+
+### Modify
+
+| File | Change |
+|---|---|
+| `Bar.qml` | Import `KubernetesPill`, add `PopoutSlot`, add `kubernetesSlot.animating` to popout `visible` guard |
+| `shared/Config.qml.tmpl` | Add `kubeNamespace`, `kubeStatusRefreshMs`, `kubeMetricsRefreshMs` |
+
+---
+
+## Scripts
+
+Both scripts accept the namespace as `$1` and are invoked from QML as:
+```qml
+command: ["bash", Config.scriptsDir + "/k8s-status.sh", Config.kubeNamespace]
+```
+
+### `scripts/executable_k8s-status.sh`
+
+Runs: `kubectl get pods -n "$1" -o json`
+
+Extracts per pod:
+- `app`: from `metadata.labels["app.kubernetes.io/instance"]`, fallback to `metadata.name`
+- `ready`: `true` if `status.conditions` contains `{type: "Ready", status: "True"}`
+
+Outputs JSON:
+```json
+{
+ "pods": [{"app": "joplin", "ready": true}, ...],
+ "readyCount": 4,
+ "totalCount": 4
+}
+```
+
+On any error: exits non-zero, outputs nothing. The QML handler checks `this.text.trim() === ""` to detect this.
+
+### `scripts/executable_k8s-metrics.sh`
+
+Runs two commands sequentially:
+1. `kubectl get pods -n "$1" -o json` — builds a `podName → app` label map
+2. `kubectl top pods -n "$1" --no-headers` — parses into `{podName → {cpuM, memMi}}`
+3. `kubectl get resourcequota -n "$1" -o json`
+
+**Pod name → app mapping:** `kubectl top` returns raw pod names (e.g. `joplin-7bfd5bc954-jq47w`). The metrics script resolves each top pod name to its `app.kubernetes.io/instance` label using the pod list from step 1, producing `podMetrics` keyed by `app` label. Pods in the top output that have no matching pod object are skipped.
+
+**Unit normalization for quota values:** All CPU values are normalized to millicores (e.g. `"1"` → `1000`, `"500m"` → `500`). All memory values are normalized to MiB (e.g. `"4Gi"` → `4096`, `"512Mi"` → `512`, raw bytes → `/ 1048576`).
+
+From resourcequota `status.used` and `status.hard` (after normalization), computes:
+- `cpuActualM`: sum of CPU millicores from `kubectl top` output (actual live usage, not from resourcequota)
+- `memActualMi`: sum of MEM MiB from `kubectl top` output (same — actual, not requests)
+- `cpuReqPct`, `cpuLimPct`: `used / hard` as 0.0–1.0 fractions
+- `memReqPct`, `memLimPct`: same for memory
+- Human-readable labels (e.g. `"78m / 2000m"`, `"1.01Gi / 4Gi"`) for display
+
+Outputs JSON:
+```json
+{
+ "podMetrics": [{"app": "joplin", "cpuM": 13, "memMi": 343}, ...],
+ "quota": {
+ "cpuActualM": 29,
+ "cpuReqPct": 0.04, "cpuLimPct": 0.73,
+ "memActualMi": 921,
+ "memReqPct": 0.25, "memLimPct": 0.54,
+ "cpuReqLabel": "78m / 2000m",
+ "cpuLimLabel": "1450m / 2000m",
+ "memReqLabel": "1.01Gi / 4Gi",
+ "memLimLabel": "2.14Gi / 4Gi"
+ }
+}
+```
+
+Pods not present in `kubectl top` output get `cpuM: -1, memMi: -1` (displayed as `—`).
+
+On any error: exits non-zero, outputs nothing.
+
+---
+
+## Singleton: `shared/Kubernetes.qml`
+
+Root element must be `Singleton` (the Quickshell type), matching every other file in `shared/`:
+
+```qml
+pragma Singleton
+import Quickshell
+import Quickshell.Io
+import QtQuick
+
+Singleton {
+ id: root
+ // properties, Processes, Timers here
+}
+```
+
+### Properties
+
+| Property | Type | Description |
+|---|---|---|
+| `status` | string | `"loading"` \| `"ok"` \| `"degraded"` \| `"error"` \| `"stale"` |
+| `pods` | var | Array of `{app, ready, cpuM, memMi}` — merged from both scripts |
+| `readyCount` | int | Number of ready pods |
+| `totalCount` | int | Total pod count |
+| `quota` | var | Quota object from metrics script |
+| `lastUpdatedSecs` | int | Seconds since last successful **status** fetch |
+
+### Health logic
+
+- `"loading"` (blue/accent): no data yet — initial state before first fetch completes
+- `"ok"` (green): all pods ready, last status fetch < 60s ago
+- `"degraded"` (yellow): any pod not ready
+- `"error"` (red): status script failed and no prior data
+- `"stale"` (overlay0): last successful status fetch > 60s ago (connectivity lost). Transitions back to `"ok"` or `"degraded"` on the next successful parse.
+
+Metrics staleness (15s cadence) is not tracked separately — if metrics are stale, pods show `cpuM: -1` (displayed as `—`), which is sufficient signal.
+
+### Polling
+
+Two independent `Process` + `StdioCollector` pairs:
+
+| Process | Script | Interval | Timer start |
+|---|---|---|---|
+| `statusProc` | `k8s-status.sh` | 30 000ms | `Component.onCompleted` |
+| `metricsProc` | `k8s-metrics.sh` | 15 000ms | Staggered 500ms after status (via single-shot Timer) |
+
+**Merge:** After each metrics fetch, the Singleton iterates `podMetrics` and patches `cpuM`/`memMi` into the existing `pods` array by matching on `app`. Pods in `pods` with no match in `podMetrics` retain `cpuM: -1, memMi: -1`.
+
+**Error handling in `onStreamFinished`:**
+```js
+if (this.text.trim() === "") {
+ // script failed
+ root.status = (root.status === "ok" || root.status === "degraded") ? "stale" : "error";
+ return;
+}
+try {
+ let data = JSON.parse(this.text);
+ // update properties
+ root.status = "ok"; // or "degraded" if any pod not ready
+} catch(e) {
+ root.status = (root.status === "ok" || root.status === "degraded") ? "stale" : "error";
+}
+```
+
+---
+
+## UI Components
+
+### `bar/KubernetesPill.qml`
+
+Follows the `BarPill` pattern exactly (see `WeatherPill.qml`):
+
+```
+groupName: "kubernetes"
+accentColor: status → green(ok) / yellow(degraded) / red(error) / overlay0(stale) / blue(loading)
+content: [
+ Text { icon glyph (U+F03BE, Nerd Font k8s wheel), font.family: iconFont },
+ Text {
+ text: status === "loading" ? "…" : readyCount + "/" + totalCount
+ color: accentColor
+ }
+]
+```
+
+### `bar/popouts/KubernetesPopout.qml`
+
+Two sections:
+
+**Pods table** — one row per pod:
+- Columns: `app name | CPU | MEM | ● Ready/NotReady`
+- CPU/MEM shown as `13m` / `343Mi`; shown as `—` if `cpuM === -1`
+- Ready: green dot + "Ready"; red dot + "NotReady"
+
+**Quota section** — two cards (CPU, MEM), each card contains:
+- Header: label + actual value (`29m actual` / `921Mi actual`)
+- `%REQ` bar: `cpuReqLabel` + percentage + colored bar (green / `Theme.green`)
+- `%LIM` bar: `cpuLimLabel` + percentage + colored bar (yellow dimmed / `Theme.yellow` at 60% opacity)
+
+Footer: `Updated Xs ago` from `lastUpdatedSecs`.
+
+### `Bar.qml` wiring
+
+```qml
+// In pill column:
+BarComponents.KubernetesPill {}
+
+// PopoutSlot (add alongside existing slots):
+PopoutSlot {
+ id: kubernetesSlot
+ name: "kubernetes"
+ verticalAnchor: "bottom"
+ sourceComponent: Popouts.KubernetesPopout {}
+}
+
+// Update the popout PanelWindow visible guard (existing line, add the new condition):
+visible: notifSlot.animating || mediaSlot.animating || weatherSlot.animating
+ || datetimeSlot.animating || systemSlot.animating || kubernetesSlot.animating
+```
+
+---
+
+## Config additions (`shared/Config.qml.tmpl`)
+
+```qml
+readonly property string kubeNamespace: "tenant-5"
+readonly property int kubeStatusRefreshMs: 30000
+readonly property int kubeMetricsRefreshMs: 15000
+```
+
+---
+
+## Error Handling Summary
+
+| Scenario | Pill | Popout |
+|---|---|---|
+| First load | blue `…` | empty / loading indicator |
+| All pods ready | green `4/4` | normal |
+| Any pod NotReady | yellow `3/4` | affected row shows red dot |
+| Status script fails (first time) | red `0/0` | error message |
+| Status script fails (had data) | overlay `4/4` | stale indicator + last known data |
+| Metrics script fails | unchanged | CPU/MEM show `—` |
+
+---
+
+## Verification
+
+```bash
+# 1. Test scripts standalone (after chezmoi apply)
+~/.config/quickshell/scripts/k8s-status.sh tenant-5 | jq .
+~/.config/quickshell/scripts/k8s-metrics.sh tenant-5 | jq .
+
+# 2. Apply and restart quickshell
+chezmoi apply ~/.config/quickshell
+# kill and relaunch quickshell
+
+# 3. Visual checks:
+# - Pill appears with correct ready/total count and green color
+# - Click pill → popout opens with pod table (joplin/n8n/shared-postgres show CPU/MEM, dnddrugs shows —)
+# - Quota bars match: kubectl top pods -n tenant-5 and kubectl get resourcequota -n tenant-5 -o json
+
+# 4. Degraded state (transient, brief outage):
+kubectl rollout restart deployment/joplin -n tenant-5
+# → pill turns yellow, joplin row shows NotReady during rollout
+# → returns green once pod is Ready again
+
+# 5. Stale state (simulate connectivity loss):
+# Temporarily rename kubeconfig or revoke access, wait >60s → pill turns overlay color
+```