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.

+ +
+ +
+
+
+ 󱃾 + 3/3 + OK +
+
+
+

A — Icon / ratio / status stacked

+

Three rows. Icon accent color matches status. Ratio stays neutral text color.

+
+
+ +
+
+
+ 󱃾 + 3/3 +
+
+
+

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.

+
+
+ +
+
+
+ 󱃾 + 3/3 +
+
+
+

B — Icon + node ratio

+

Shows ready nodes vs total (e.g. 3/3). Immediately tells you if a node is down.

+
+
+ +
+
+
+ 󱃾 + OK +
+
+
+

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.

+ +
+
K8s — tenant-5
+
+ + +
+
Nodes
+
+
+ node-1 + ● Ready +
+
+ node-2 + ● Ready +
+
+ node-3 + ● NotReady +
+
+
+ + +
+
Pods — tenant-5
+
+
+
12
+
Running
+
+
+
1
+
Pending
+
+
+
0
+
Failed
+
+
+
+ + +
+
Namespace Quota — tenant-5
+
+
+
+ CPU requests + 1.2 / 4 cores +
+
+
+
+
+
+
+ Memory requests + 3.4 / 8 GiB +
+
+
+
+
+
+
+ Pods + 13 / 20 +
+
+
+
+
+
+
+ + +
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.

+ +
+ +
+
A — Bar with actual + % labels
+
+ +
Namespace Resources
+ + +
+
+ CPU + 420m +
+
+
+ %REQ + 35% +
+
+
+
+
+
+
+ %LIM + 21% +
+
+
+
+
+
+ + +
+
+ MEM + 2.8 GiB +
+
+
+ %REQ + 58% +
+
+
+
+
+
+
+ %LIM + 34% +
+
+
+
+
+
+ +
+
+ +
+
B — Compact table row
+
+ +
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 —

+ +
+
K8s — tenant-5
+
+ + +
+
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.

+ +
+
K8s — tenant-5
+
+ + +
+
Nodes
+
+
+ node-1 +
+ CPU 38% + MEM 61% + ● Ready +
+
+
+ node-2 +
+ CPU 12% + MEM 44% + ● Ready +
+
+
+ node-3 +
+ CPU + MEM + ● NotReady +
+
+
+
+ + +
+
Pods — tenant-5
+
+
+
12
+
Running
+
+
+
1
+
Pending
+
+
+
0
+
Failed
+
+
+
+ + +
+
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 +```