chore updating hyprland to lua
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"hooksConfig": {
|
||||
"disabled": [
|
||||
"security-gate",
|
||||
"slm-memory-sync",
|
||||
"dynamic-context",
|
||||
"slm-observe"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
{"reason":"idle timeout","timestamp":1776812893183}
|
||||
@@ -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"}
|
||||
@@ -0,0 +1 @@
|
||||
128167
|
||||
@@ -0,0 +1,33 @@
|
||||
<h2>Combined: icon + node ratio + status color</h2>
|
||||
<p class="subtitle">Two variations on how to stack all three elements in the 52px pill.</p>
|
||||
|
||||
<div class="cards">
|
||||
|
||||
<div class="card" data-choice="a" onclick="toggleSelect(this)">
|
||||
<div class="card-image" style="display:flex;align-items:center;justify-content:center;padding:28px 0;background:#1e1e2e;border-radius:8px">
|
||||
<div style="display:flex;flex-direction:column;align-items:center;gap:5px;background:#313244;border-radius:14px;padding:10px 14px;width:42px">
|
||||
<span style="font-size:18px;color:#a6e3a1"></span>
|
||||
<span style="font-size:10px;color:#cdd6f4;font-family:monospace;font-weight:bold">3/3</span>
|
||||
<span style="font-size:8px;color:#a6e3a1;font-family:monospace;letter-spacing:0.5px">OK</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3>A — Icon / ratio / status stacked</h3>
|
||||
<p>Three rows. Icon accent color matches status. Ratio stays neutral text color.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" data-choice="b" onclick="toggleSelect(this)">
|
||||
<div class="card-image" style="display:flex;align-items:center;justify-content:center;padding:28px 0;background:#1e1e2e;border-radius:8px">
|
||||
<div style="display:flex;flex-direction:column;align-items:center;gap:5px;background:#313244;border-radius:14px;padding:10px 14px;width:42px">
|
||||
<span style="font-size:18px;color:#a6e3a1"></span>
|
||||
<span style="font-size:9px;color:#a6e3a1;font-family:monospace;font-weight:bold">3/3</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3>B — Icon + ratio (ratio IS the status)</h3>
|
||||
<p>Two rows. Ratio color encodes health: green = all ready, yellow = degraded, red = critical. No redundant "OK" label.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,44 @@
|
||||
<h2>What should the bar pill show?</h2>
|
||||
<p class="subtitle">The pill sits in the left sidebar (52px wide). Pick the compact display style you prefer.</p>
|
||||
|
||||
<div class="cards">
|
||||
|
||||
<div class="card" data-choice="a" onclick="toggleSelect(this)">
|
||||
<div class="card-image" style="display:flex;align-items:center;justify-content:center;padding:24px 0;background:#1e1e2e;border-radius:8px">
|
||||
<div style="display:flex;flex-direction:column;align-items:center;gap:6px;background:#313244;border-radius:14px;padding:10px 14px;width:42px">
|
||||
<span style="font-size:18px;color:#89b4fa"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3>A — Icon only</h3>
|
||||
<p>Pill color shifts: green = healthy, yellow = degraded, red = critical. Minimal, like the system pill.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" data-choice="b" onclick="toggleSelect(this)">
|
||||
<div class="card-image" style="display:flex;align-items:center;justify-content:center;padding:24px 0;background:#1e1e2e;border-radius:8px">
|
||||
<div style="display:flex;flex-direction:column;align-items:center;gap:6px;background:#313244;border-radius:14px;padding:10px 14px;width:42px">
|
||||
<span style="font-size:18px;color:#89b4fa"></span>
|
||||
<span style="font-size:10px;color:#cdd6f4;font-family:monospace;font-weight:bold">3/3</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3>B — Icon + node ratio</h3>
|
||||
<p>Shows ready nodes vs total (e.g. 3/3). Immediately tells you if a node is down.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" data-choice="c" onclick="toggleSelect(this)">
|
||||
<div class="card-image" style="display:flex;align-items:center;justify-content:center;padding:24px 0;background:#1e1e2e;border-radius:8px">
|
||||
<div style="display:flex;flex-direction:column;align-items:center;gap:6px;background:#313244;border-radius:14px;padding:10px 14px;width:42px">
|
||||
<span style="font-size:18px;color:#a6e3a1"></span>
|
||||
<span style="font-size:9px;color:#a6e3a1;font-family:monospace;font-weight:bold">OK</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3>C — Icon + status text</h3>
|
||||
<p>Shows OK / WARN / ERR in the pill accent color. Clear at a glance, no counting required.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,94 @@
|
||||
<h2>Updated layout — namespace quota instead of cluster metrics</h2>
|
||||
<p class="subtitle">Nodes show status only (no metrics-server needed). Resources show quota consumption from kubectl.</p>
|
||||
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">K8s — tenant-5</div>
|
||||
<div class="mockup-body" style="background:#1e1e2e;padding:16px;display:flex;flex-direction:column;gap:14px;font-family:monospace">
|
||||
|
||||
<!-- Nodes section -->
|
||||
<div>
|
||||
<div style="font-size:10px;color:#6c7086;letter-spacing:1px;text-transform:uppercase;margin-bottom:8px">Nodes</div>
|
||||
<div style="display:flex;flex-direction:column;gap:6px">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;background:#313244;border-radius:8px;padding:7px 10px">
|
||||
<span style="color:#cdd6f4;font-size:12px">node-1</span>
|
||||
<span style="font-size:11px;color:#a6e3a1">● Ready</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;background:#313244;border-radius:8px;padding:7px 10px">
|
||||
<span style="color:#cdd6f4;font-size:12px">node-2</span>
|
||||
<span style="font-size:11px;color:#a6e3a1">● Ready</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;background:#313244;border-radius:8px;padding:7px 10px">
|
||||
<span style="color:#cdd6f4;font-size:12px">node-3</span>
|
||||
<span style="font-size:11px;color:#f38ba8">● NotReady</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pods summary -->
|
||||
<div>
|
||||
<div style="font-size:10px;color:#6c7086;letter-spacing:1px;text-transform:uppercase;margin-bottom:8px">Pods — tenant-5</div>
|
||||
<div style="display:flex;gap:8px">
|
||||
<div style="flex:1;background:#313244;border-radius:8px;padding:10px;text-align:center">
|
||||
<div style="font-size:22px;font-weight:bold;color:#a6e3a1">12</div>
|
||||
<div style="font-size:9px;color:#6c7086;margin-top:2px">Running</div>
|
||||
</div>
|
||||
<div style="flex:1;background:#313244;border-radius:8px;padding:10px;text-align:center">
|
||||
<div style="font-size:22px;font-weight:bold;color:#f9e2af">1</div>
|
||||
<div style="font-size:9px;color:#6c7086;margin-top:2px">Pending</div>
|
||||
</div>
|
||||
<div style="flex:1;background:#313244;border-radius:8px;padding:10px;text-align:center">
|
||||
<div style="font-size:22px;font-weight:bold;color:#f38ba8">0</div>
|
||||
<div style="font-size:9px;color:#6c7086;margin-top:2px">Failed</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Namespace quota -->
|
||||
<div>
|
||||
<div style="font-size:10px;color:#6c7086;letter-spacing:1px;text-transform:uppercase;margin-bottom:8px">Namespace Quota — tenant-5</div>
|
||||
<div style="display:flex;flex-direction:column;gap:8px">
|
||||
<div>
|
||||
<div style="display:flex;justify-content:space-between;margin-bottom:4px">
|
||||
<span style="font-size:10px;color:#89dceb">CPU requests</span>
|
||||
<span style="font-size:10px;color:#cdd6f4">1.2 / 4 cores</span>
|
||||
</div>
|
||||
<div style="height:6px;background:#313244;border-radius:3px">
|
||||
<div style="height:100%;width:30%;background:#89dceb;border-radius:3px"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="display:flex;justify-content:space-between;margin-bottom:4px">
|
||||
<span style="font-size:10px;color:#cba6f7">Memory requests</span>
|
||||
<span style="font-size:10px;color:#cdd6f4">3.4 / 8 GiB</span>
|
||||
</div>
|
||||
<div style="height:6px;background:#313244;border-radius:3px">
|
||||
<div style="height:100%;width:42%;background:#cba6f7;border-radius:3px"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="display:flex;justify-content:space-between;margin-bottom:4px">
|
||||
<span style="font-size:10px;color:#f9e2af">Pods</span>
|
||||
<span style="font-size:10px;color:#cdd6f4">13 / 20</span>
|
||||
</div>
|
||||
<div style="height:6px;background:#313244;border-radius:3px">
|
||||
<div style="height:100%;width:65%;background:#f9e2af;border-radius:3px"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Last updated -->
|
||||
<div style="text-align:right;font-size:9px;color:#45475a">Updated 8s ago</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="options" style="margin-top:16px">
|
||||
<div class="option" data-choice="looks-good" onclick="toggleSelect(this)">
|
||||
<div class="letter">✓</div>
|
||||
<div class="content">
|
||||
<h3>This works</h3>
|
||||
<p>Node status, pod counts, namespace quota bars</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
<h2>Namespace resources — k9s-style columns</h2>
|
||||
<p class="subtitle">Aggregated across all pods in tenant-5. Two options for how to display the data.</p>
|
||||
|
||||
<div class="split">
|
||||
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">A — Bar with actual + % labels</div>
|
||||
<div class="mockup-body" style="background:#1e1e2e;padding:14px;display:flex;flex-direction:column;gap:10px;font-family:monospace">
|
||||
|
||||
<div style="font-size:10px;color:#6c7086;letter-spacing:1px;text-transform:uppercase;margin-bottom:2px">Namespace Resources</div>
|
||||
|
||||
<!-- CPU -->
|
||||
<div style="background:#313244;border-radius:8px;padding:10px 12px;display:flex;flex-direction:column;gap:6px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:baseline">
|
||||
<span style="font-size:10px;color:#89dceb;font-weight:bold">CPU</span>
|
||||
<span style="font-size:11px;color:#cdd6f4">420m</span>
|
||||
</div>
|
||||
<div>
|
||||
<div style="display:flex;justify-content:space-between;margin-bottom:3px">
|
||||
<span style="font-size:9px;color:#6c7086">%REQ</span>
|
||||
<span style="font-size:9px;color:#a6e3a1">35%</span>
|
||||
</div>
|
||||
<div style="height:4px;background:#1e1e2e;border-radius:2px">
|
||||
<div style="height:100%;width:35%;background:#a6e3a1;border-radius:2px"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="display:flex;justify-content:space-between;margin-bottom:3px">
|
||||
<span style="font-size:9px;color:#6c7086">%LIM</span>
|
||||
<span style="font-size:9px;color:#f9e2af">21%</span>
|
||||
</div>
|
||||
<div style="height:4px;background:#1e1e2e;border-radius:2px">
|
||||
<div style="height:100%;width:21%;background:#f9e2af;border-radius:2px"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MEM -->
|
||||
<div style="background:#313244;border-radius:8px;padding:10px 12px;display:flex;flex-direction:column;gap:6px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:baseline">
|
||||
<span style="font-size:10px;color:#cba6f7;font-weight:bold">MEM</span>
|
||||
<span style="font-size:11px;color:#cdd6f4">2.8 GiB</span>
|
||||
</div>
|
||||
<div>
|
||||
<div style="display:flex;justify-content:space-between;margin-bottom:3px">
|
||||
<span style="font-size:9px;color:#6c7086">%REQ</span>
|
||||
<span style="font-size:9px;color:#a6e3a1">58%</span>
|
||||
</div>
|
||||
<div style="height:4px;background:#1e1e2e;border-radius:2px">
|
||||
<div style="height:100%;width:58%;background:#cba6f7;border-radius:2px"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="display:flex;justify-content:space-between;margin-bottom:3px">
|
||||
<span style="font-size:9px;color:#6c7086">%LIM</span>
|
||||
<span style="font-size:9px;color:#f9e2af">34%</span>
|
||||
</div>
|
||||
<div style="height:4px;background:#1e1e2e;border-radius:2px">
|
||||
<div style="height:100%;width:34%;background:#cba6f7;border-radius:2px;opacity:0.6"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">B — Compact table row</div>
|
||||
<div class="mockup-body" style="background:#1e1e2e;padding:14px;display:flex;flex-direction:column;gap:10px;font-family:monospace">
|
||||
|
||||
<div style="font-size:10px;color:#6c7086;letter-spacing:1px;text-transform:uppercase;margin-bottom:2px">Namespace Resources</div>
|
||||
|
||||
<!-- header row -->
|
||||
<div style="display:grid;grid-template-columns:2fr 1.5fr 1fr 1fr;gap:4px;padding:0 4px">
|
||||
<span style="font-size:8px;color:#45475a"></span>
|
||||
<span style="font-size:8px;color:#45475a;text-align:right">ACTUAL</span>
|
||||
<span style="font-size:8px;color:#45475a;text-align:right">%R</span>
|
||||
<span style="font-size:8px;color:#45475a;text-align:right">%L</span>
|
||||
</div>
|
||||
|
||||
<!-- CPU row -->
|
||||
<div style="display:grid;grid-template-columns:2fr 1.5fr 1fr 1fr;gap:4px;background:#313244;border-radius:8px;padding:8px 10px;align-items:center">
|
||||
<span style="font-size:11px;color:#89dceb;font-weight:bold">CPU</span>
|
||||
<span style="font-size:11px;color:#cdd6f4;text-align:right">420m</span>
|
||||
<span style="font-size:11px;color:#a6e3a1;text-align:right">35%</span>
|
||||
<span style="font-size:11px;color:#f9e2af;text-align:right">21%</span>
|
||||
</div>
|
||||
|
||||
<!-- MEM row -->
|
||||
<div style="display:grid;grid-template-columns:2fr 1.5fr 1fr 1fr;gap:4px;background:#313244;border-radius:8px;padding:8px 10px;align-items:center">
|
||||
<span style="font-size:11px;color:#cba6f7;font-weight:bold">MEM</span>
|
||||
<span style="font-size:11px;color:#cdd6f4;text-align:right">2.8G</span>
|
||||
<span style="font-size:11px;color:#a6e3a1;text-align:right">58%</span>
|
||||
<span style="font-size:11px;color:#f9e2af;text-align:right">34%</span>
|
||||
</div>
|
||||
|
||||
<div style="font-size:9px;color:#45475a;margin-top:2px">%R = % of requests quota · %L = % of limits quota</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="options" style="margin-top:16px">
|
||||
<div class="option" data-choice="a" onclick="toggleSelect(this)">
|
||||
<div class="letter">A</div>
|
||||
<div class="content"><h3>Bars with actual + % labels</h3><p>Visual bars, more scannable</p></div>
|
||||
</div>
|
||||
<div class="option" data-choice="b" onclick="toggleSelect(this)">
|
||||
<div class="letter">B</div>
|
||||
<div class="content"><h3>Compact table row</h3><p>Denser, mirrors k9s columns exactly</p></div>
|
||||
</div>
|
||||
</div>
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
<h2>Updated layout — pods replace nodes, per-pod metrics</h2>
|
||||
<p class="subtitle">Based on your real kubectl data. dnddrugs absent from kubectl top → shown as —</p>
|
||||
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">K8s — tenant-5</div>
|
||||
<div class="mockup-body" style="background:#1e1e2e;padding:16px;display:flex;flex-direction:column;gap:14px;font-family:monospace">
|
||||
|
||||
<!-- Pods section -->
|
||||
<div>
|
||||
<div style="font-size:10px;color:#6c7086;letter-spacing:1px;text-transform:uppercase;margin-bottom:8px">Pods</div>
|
||||
<!-- header -->
|
||||
<div style="display:grid;grid-template-columns:1fr 1.2fr 1.2fr 1fr;gap:4px;padding:0 10px 4px;font-size:8px;color:#45475a">
|
||||
<span></span><span style="text-align:right">CPU</span><span style="text-align:right">MEM</span><span style="text-align:right">STATUS</span>
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;gap:5px">
|
||||
<div style="display:grid;grid-template-columns:1fr 1.2fr 1.2fr 1fr;gap:4px;background:#313244;border-radius:8px;padding:7px 10px;align-items:center">
|
||||
<span style="font-size:11px;color:#cdd6f4">joplin</span>
|
||||
<span style="font-size:11px;color:#89dceb;text-align:right">13m</span>
|
||||
<span style="font-size:11px;color:#cba6f7;text-align:right">343Mi</span>
|
||||
<span style="font-size:10px;color:#a6e3a1;text-align:right">● Ready</span>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1.2fr 1.2fr 1fr;gap:4px;background:#313244;border-radius:8px;padding:7px 10px;align-items:center">
|
||||
<span style="font-size:11px;color:#cdd6f4">n8n</span>
|
||||
<span style="font-size:11px;color:#89dceb;text-align:right">6m</span>
|
||||
<span style="font-size:11px;color:#cba6f7;text-align:right">427Mi</span>
|
||||
<span style="font-size:10px;color:#a6e3a1;text-align:right">● Ready</span>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1.2fr 1.2fr 1fr;gap:4px;background:#313244;border-radius:8px;padding:7px 10px;align-items:center">
|
||||
<span style="font-size:11px;color:#cdd6f4">shared-pg</span>
|
||||
<span style="font-size:11px;color:#89dceb;text-align:right">10m</span>
|
||||
<span style="font-size:11px;color:#cba6f7;text-align:right">151Mi</span>
|
||||
<span style="font-size:10px;color:#a6e3a1;text-align:right">● Ready</span>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1.2fr 1.2fr 1fr;gap:4px;background:#313244;border-radius:8px;padding:7px 10px;align-items:center">
|
||||
<span style="font-size:11px;color:#cdd6f4">dnddrugs</span>
|
||||
<span style="font-size:11px;color:#45475a;text-align:right">—</span>
|
||||
<span style="font-size:11px;color:#45475a;text-align:right">—</span>
|
||||
<span style="font-size:10px;color:#a6e3a1;text-align:right">● Ready</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quota section -->
|
||||
<div>
|
||||
<div style="font-size:10px;color:#6c7086;letter-spacing:1px;text-transform:uppercase;margin-bottom:8px">Quota — tenant-5</div>
|
||||
<div style="display:flex;flex-direction:column;gap:10px">
|
||||
|
||||
<!-- CPU -->
|
||||
<div style="background:#313244;border-radius:8px;padding:10px 12px;display:flex;flex-direction:column;gap:6px">
|
||||
<div style="display:flex;justify-content:space-between">
|
||||
<span style="font-size:10px;color:#89dceb;font-weight:bold">CPU</span>
|
||||
<span style="font-size:10px;color:#cdd6f4">29m actual</span>
|
||||
</div>
|
||||
<div>
|
||||
<div style="display:flex;justify-content:space-between;margin-bottom:3px">
|
||||
<span style="font-size:9px;color:#6c7086">%REQ 78m / 2000m</span>
|
||||
<span style="font-size:9px;color:#a6e3a1">4%</span>
|
||||
</div>
|
||||
<div style="height:4px;background:#1e1e2e;border-radius:2px">
|
||||
<div style="height:100%;width:4%;background:#a6e3a1;border-radius:2px"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="display:flex;justify-content:space-between;margin-bottom:3px">
|
||||
<span style="font-size:9px;color:#6c7086">%LIM 1450m / 2000m</span>
|
||||
<span style="font-size:9px;color:#f9e2af">73%</span>
|
||||
</div>
|
||||
<div style="height:4px;background:#1e1e2e;border-radius:2px">
|
||||
<div style="height:100%;width:73%;background:#f9e2af;border-radius:2px"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MEM -->
|
||||
<div style="background:#313244;border-radius:8px;padding:10px 12px;display:flex;flex-direction:column;gap:6px">
|
||||
<div style="display:flex;justify-content:space-between">
|
||||
<span style="font-size:10px;color:#cba6f7;font-weight:bold">MEM</span>
|
||||
<span style="font-size:10px;color:#cdd6f4">921Mi actual</span>
|
||||
</div>
|
||||
<div>
|
||||
<div style="display:flex;justify-content:space-between;margin-bottom:3px">
|
||||
<span style="font-size:9px;color:#6c7086">%REQ 1.01Gi / 4Gi</span>
|
||||
<span style="font-size:9px;color:#a6e3a1">25%</span>
|
||||
</div>
|
||||
<div style="height:4px;background:#1e1e2e;border-radius:2px">
|
||||
<div style="height:100%;width:25%;background:#cba6f7;border-radius:2px"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="display:flex;justify-content:space-between;margin-bottom:3px">
|
||||
<span style="font-size:9px;color:#6c7086">%LIM 2.14Gi / 4Gi</span>
|
||||
<span style="font-size:9px;color:#f9e2af">54%</span>
|
||||
</div>
|
||||
<div style="height:4px;background:#1e1e2e;border-radius:2px">
|
||||
<div style="height:100%;width:54%;background:#cba6f7;border-radius:2px;opacity:0.6"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align:right;font-size:9px;color:#45475a">Updated 8s ago</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="subtitle" style="margin-top:16px">Pill now shows ready/total pods, e.g. <strong style="color:#a6e3a1">4/4</strong> green → <strong style="color:#f9e2af">3/4</strong> yellow if one goes unready.</p>
|
||||
|
||||
<div class="options" style="margin-top:8px">
|
||||
<div class="option" data-choice="looks-good" onclick="toggleSelect(this)">
|
||||
<div class="letter">✓</div>
|
||||
<div class="content">
|
||||
<h3>This works</h3>
|
||||
<p>Per-pod table + quota bars with real numbers</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,97 @@
|
||||
<h2>Popout layout — does this cover what you need?</h2>
|
||||
<p class="subtitle">320px wide panel. Click to select if this looks right, or describe what's missing below.</p>
|
||||
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">K8s — tenant-5</div>
|
||||
<div class="mockup-body" style="background:#1e1e2e;padding:16px;display:flex;flex-direction:column;gap:14px;font-family:monospace">
|
||||
|
||||
<!-- Nodes section -->
|
||||
<div>
|
||||
<div style="font-size:10px;color:#6c7086;letter-spacing:1px;text-transform:uppercase;margin-bottom:8px">Nodes</div>
|
||||
<div style="display:flex;flex-direction:column;gap:6px">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;background:#313244;border-radius:8px;padding:7px 10px">
|
||||
<span style="color:#cdd6f4;font-size:12px">node-1</span>
|
||||
<div style="display:flex;gap:10px;align-items:center">
|
||||
<span style="font-size:10px;color:#89dceb">CPU <b style="color:#cdd6f4">38%</b></span>
|
||||
<span style="font-size:10px;color:#cba6f7">MEM <b style="color:#cdd6f4">61%</b></span>
|
||||
<span style="font-size:11px;color:#a6e3a1">● Ready</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;background:#313244;border-radius:8px;padding:7px 10px">
|
||||
<span style="color:#cdd6f4;font-size:12px">node-2</span>
|
||||
<div style="display:flex;gap:10px;align-items:center">
|
||||
<span style="font-size:10px;color:#89dceb">CPU <b style="color:#cdd6f4">12%</b></span>
|
||||
<span style="font-size:10px;color:#cba6f7">MEM <b style="color:#cdd6f4">44%</b></span>
|
||||
<span style="font-size:11px;color:#a6e3a1">● Ready</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;background:#313244;border-radius:8px;padding:7px 10px">
|
||||
<span style="color:#cdd6f4;font-size:12px">node-3</span>
|
||||
<div style="display:flex;gap:10px;align-items:center">
|
||||
<span style="font-size:10px;color:#89dceb">CPU <b style="color:#cdd6f4">—</b></span>
|
||||
<span style="font-size:10px;color:#cba6f7">MEM <b style="color:#cdd6f4">—</b></span>
|
||||
<span style="font-size:11px;color:#f38ba8">● NotReady</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pods summary -->
|
||||
<div>
|
||||
<div style="font-size:10px;color:#6c7086;letter-spacing:1px;text-transform:uppercase;margin-bottom:8px">Pods — tenant-5</div>
|
||||
<div style="display:flex;gap:8px">
|
||||
<div style="flex:1;background:#313244;border-radius:8px;padding:10px;text-align:center">
|
||||
<div style="font-size:22px;font-weight:bold;color:#a6e3a1">12</div>
|
||||
<div style="font-size:9px;color:#6c7086;margin-top:2px">Running</div>
|
||||
</div>
|
||||
<div style="flex:1;background:#313244;border-radius:8px;padding:10px;text-align:center">
|
||||
<div style="font-size:22px;font-weight:bold;color:#f9e2af">1</div>
|
||||
<div style="font-size:9px;color:#6c7086;margin-top:2px">Pending</div>
|
||||
</div>
|
||||
<div style="flex:1;background:#313244;border-radius:8px;padding:10px;text-align:center">
|
||||
<div style="font-size:22px;font-weight:bold;color:#f38ba8">0</div>
|
||||
<div style="font-size:9px;color:#6c7086;margin-top:2px">Failed</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cluster resources -->
|
||||
<div>
|
||||
<div style="font-size:10px;color:#6c7086;letter-spacing:1px;text-transform:uppercase;margin-bottom:8px">Cluster Resources</div>
|
||||
<div style="display:flex;flex-direction:column;gap:8px">
|
||||
<div>
|
||||
<div style="display:flex;justify-content:space-between;margin-bottom:4px">
|
||||
<span style="font-size:10px;color:#89dceb">CPU</span>
|
||||
<span style="font-size:10px;color:#cdd6f4">1.8 / 8 cores</span>
|
||||
</div>
|
||||
<div style="height:6px;background:#313244;border-radius:3px">
|
||||
<div style="height:100%;width:22%;background:#89dceb;border-radius:3px"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="display:flex;justify-content:space-between;margin-bottom:4px">
|
||||
<span style="font-size:10px;color:#cba6f7">Memory</span>
|
||||
<span style="font-size:10px;color:#cdd6f4">5.2 / 16 GiB</span>
|
||||
</div>
|
||||
<div style="height:6px;background:#313244;border-radius:3px">
|
||||
<div style="height:100%;width:52%;background:#cba6f7;border-radius:3px"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Last updated -->
|
||||
<div style="text-align:right;font-size:9px;color:#45475a">Updated 14s ago</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="options" style="margin-top:16px">
|
||||
<div class="option" data-choice="looks-good" onclick="toggleSelect(this)">
|
||||
<div class="letter">✓</div>
|
||||
<div class="content">
|
||||
<h3>This covers it</h3>
|
||||
<p>Nodes with per-node CPU/mem/status, pod counts, cluster resource bars</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh">
|
||||
<p class="subtitle">Continuing in terminal...</p>
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh">
|
||||
<p class="subtitle">Continuing in terminal...</p>
|
||||
</div>
|
||||
@@ -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 + "' })")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <namespace>
|
||||
set -euo pipefail
|
||||
|
||||
NS="${1:-tenant-5}"
|
||||
|
||||
pods_json=$(kubectl get pods -n "$NS" -o json 2>/dev/null) || exit 0
|
||||
|
||||
echo "$pods_json" | jq -c '
|
||||
[.items[] | {
|
||||
app: (.metadata.labels["app.kubernetes.io/instance"] // .metadata.name),
|
||||
ready: ((.status.conditions // [])
|
||||
| map(select(.type == "Ready"))
|
||||
| if length > 0 then .[0].status == "True" else false end)
|
||||
}] as $pods |
|
||||
{
|
||||
pods: $pods,
|
||||
readyCount: ($pods | map(select(.ready)) | length),
|
||||
totalCount: ($pods | length)
|
||||
}
|
||||
'
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Test standalone**
|
||||
|
||||
```bash
|
||||
chezmoi apply ~/.config/quickshell/scripts/k8s-status.sh
|
||||
~/.config/quickshell/scripts/k8s-status.sh tenant-5 | jq .
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```json
|
||||
{
|
||||
"pods": [
|
||||
{"app": "dnddrugs", "ready": true},
|
||||
{"app": "joplin", "ready": true},
|
||||
{"app": "n8n", "ready": true},
|
||||
{"app": "shared-postgres", "ready": true}
|
||||
],
|
||||
"readyCount": 4,
|
||||
"totalCount": 4
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add scripts/executable_k8s-status.sh
|
||||
git commit -m "k8s-widget: add k8s-status.sh"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: k8s-metrics.sh
|
||||
|
||||
**Files:**
|
||||
- Create: `scripts/executable_k8s-metrics.sh`
|
||||
|
||||
- [ ] **Step 1: Write the script**
|
||||
|
||||
Create `scripts/executable_k8s-metrics.sh`:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# Outputs per-pod CPU/MEM metrics and namespace quota as JSON.
|
||||
# Usage: k8s-metrics.sh <namespace>
|
||||
set -euo pipefail
|
||||
|
||||
NS="${1:-tenant-5}"
|
||||
|
||||
# ── Unit normalization ───────────────────────────────────────────────────────
|
||||
|
||||
# Normalize CPU string to millicores integer
|
||||
normalize_cpu() {
|
||||
local val="$1"
|
||||
if [[ "$val" =~ ^([0-9]+)m$ ]]; then
|
||||
echo "${BASH_REMATCH[1]}"
|
||||
elif [[ "$val" =~ ^([0-9]+(\.[0-9]+)?)$ ]]; then
|
||||
awk "BEGIN { printf \"%d\", ${BASH_REMATCH[1]} * 1000 }"
|
||||
else
|
||||
echo 0
|
||||
fi
|
||||
}
|
||||
|
||||
# Normalize memory string to MiB integer
|
||||
normalize_mem() {
|
||||
local val="$1"
|
||||
if [[ "$val" =~ ^([0-9]+)Gi$ ]]; then
|
||||
echo $(( ${BASH_REMATCH[1]} * 1024 ))
|
||||
elif [[ "$val" =~ ^([0-9]+)Mi$ ]]; then
|
||||
echo "${BASH_REMATCH[1]}"
|
||||
elif [[ "$val" =~ ^([0-9]+)Ki$ ]]; then
|
||||
echo $(( ${BASH_REMATCH[1]} / 1024 ))
|
||||
elif [[ "$val" =~ ^([0-9]+)$ ]]; then
|
||||
echo $(( ${BASH_REMATCH[1]} / 1048576 ))
|
||||
else
|
||||
echo 0
|
||||
fi
|
||||
}
|
||||
|
||||
# Format millicores for display ("420m" or "1.5c")
|
||||
fmt_cpu() {
|
||||
local m="$1"
|
||||
if [[ $m -lt 1000 ]]; then
|
||||
echo "${m}m"
|
||||
else
|
||||
awk "BEGIN { printf \"%.1fc\", $m / 1000 }"
|
||||
fi
|
||||
}
|
||||
|
||||
# Format MiB for display ("343Mi" or "1.50Gi")
|
||||
fmt_mem() {
|
||||
local mib="$1"
|
||||
if [[ $mib -lt 1024 ]]; then
|
||||
echo "${mib}Mi"
|
||||
else
|
||||
awk "BEGIN { printf \"%.2fGi\", $mib / 1024 }"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Fetch data ───────────────────────────────────────────────────────────────
|
||||
|
||||
pods_json=$(kubectl get pods -n "$NS" -o json 2>/dev/null) || exit 0
|
||||
top_output=$(kubectl top pods -n "$NS" --no-headers 2>/dev/null) || top_output=""
|
||||
quota_json=$(kubectl get resourcequota -n "$NS" -o json 2>/dev/null) || exit 0
|
||||
|
||||
# ── Build podName → app map ──────────────────────────────────────────────────
|
||||
|
||||
declare -A name_to_app
|
||||
while IFS= read -r line; do
|
||||
pod_name=$(echo "$line" | jq -r '.name')
|
||||
app=$(echo "$line" | jq -r '.app')
|
||||
name_to_app["$pod_name"]="$app"
|
||||
done < <(echo "$pods_json" | jq -c '.items[] | {name: .metadata.name, app: (.metadata.labels["app.kubernetes.io/instance"] // .metadata.name)}')
|
||||
|
||||
# ── Parse kubectl top ────────────────────────────────────────────────────────
|
||||
|
||||
declare -A top_cpu # app → cpuM
|
||||
declare -A top_mem # app → memMi
|
||||
cpu_actual=0
|
||||
mem_actual=0
|
||||
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" ]] && continue
|
||||
read -r pod_name cpu_str mem_str <<< "$line"
|
||||
app="${name_to_app[$pod_name]:-}"
|
||||
[[ -z "$app" ]] && continue
|
||||
cpu_m=$(normalize_cpu "$cpu_str")
|
||||
mem_mib=$(normalize_mem "$mem_str")
|
||||
top_cpu["$app"]=$cpu_m
|
||||
top_mem["$app"]=$mem_mib
|
||||
cpu_actual=$(( cpu_actual + cpu_m ))
|
||||
mem_actual=$(( mem_actual + mem_mib ))
|
||||
done <<< "$top_output"
|
||||
|
||||
# ── Build podMetrics JSON array ──────────────────────────────────────────────
|
||||
|
||||
pod_metrics="["
|
||||
first=1
|
||||
while IFS= read -r line; do
|
||||
app=$(echo "$line" | jq -r '.app')
|
||||
cpu_m="${top_cpu[$app]:-"-1"}"
|
||||
mem_mib="${top_mem[$app]:-"-1"}"
|
||||
[[ $first -eq 0 ]] && pod_metrics+=","
|
||||
pod_metrics+=$(jq -nc --arg app "$app" --argjson cpu "$cpu_m" --argjson mem "$mem_mib" \
|
||||
'{app: $app, cpuM: $cpu, memMi: $mem}')
|
||||
first=0
|
||||
done < <(echo "$pods_json" | jq -c '.items[] | {app: (.metadata.labels["app.kubernetes.io/instance"] // .metadata.name)}')
|
||||
pod_metrics+="]"
|
||||
|
||||
# ── Parse resourcequota ──────────────────────────────────────────────────────
|
||||
|
||||
q_cpu_req_used=$(echo "$quota_json" | jq -r '.items[0].status.used["requests.cpu"] // "0"')
|
||||
q_cpu_req_hard=$(echo "$quota_json" | jq -r '.items[0].status.hard["requests.cpu"] // "0"')
|
||||
q_cpu_lim_used=$(echo "$quota_json" | jq -r '.items[0].status.used["limits.cpu"] // "0"')
|
||||
q_cpu_lim_hard=$(echo "$quota_json" | jq -r '.items[0].status.hard["limits.cpu"] // "0"')
|
||||
q_mem_req_used=$(echo "$quota_json" | jq -r '.items[0].status.used["requests.memory"] // "0"')
|
||||
q_mem_req_hard=$(echo "$quota_json" | jq -r '.items[0].status.hard["requests.memory"] // "0"')
|
||||
q_mem_lim_used=$(echo "$quota_json" | jq -r '.items[0].status.used["limits.memory"] // "0"')
|
||||
q_mem_lim_hard=$(echo "$quota_json" | jq -r '.items[0].status.hard["limits.memory"] // "0"')
|
||||
|
||||
cpu_req_used_m=$(normalize_cpu "$q_cpu_req_used")
|
||||
cpu_req_hard_m=$(normalize_cpu "$q_cpu_req_hard")
|
||||
cpu_lim_used_m=$(normalize_cpu "$q_cpu_lim_used")
|
||||
cpu_lim_hard_m=$(normalize_cpu "$q_cpu_lim_hard")
|
||||
mem_req_used_mib=$(normalize_mem "$q_mem_req_used")
|
||||
mem_req_hard_mib=$(normalize_mem "$q_mem_req_hard")
|
||||
mem_lim_used_mib=$(normalize_mem "$q_mem_lim_used")
|
||||
mem_lim_hard_mib=$(normalize_mem "$q_mem_lim_hard")
|
||||
|
||||
cpu_req_pct=$(awk "BEGIN { printf \"%.4f\", $cpu_req_used_m / ($cpu_req_hard_m > 0 ? $cpu_req_hard_m : 1) }")
|
||||
cpu_lim_pct=$(awk "BEGIN { printf \"%.4f\", $cpu_lim_used_m / ($cpu_lim_hard_m > 0 ? $cpu_lim_hard_m : 1) }")
|
||||
mem_req_pct=$(awk "BEGIN { printf \"%.4f\", $mem_req_used_mib / ($mem_req_hard_mib > 0 ? $mem_req_hard_mib : 1) }")
|
||||
mem_lim_pct=$(awk "BEGIN { printf \"%.4f\", $mem_lim_used_mib / ($mem_lim_hard_mib > 0 ? $mem_lim_hard_mib : 1) }")
|
||||
|
||||
cpu_req_label="$(fmt_cpu $cpu_req_used_m) / $(fmt_cpu $cpu_req_hard_m)"
|
||||
cpu_lim_label="$(fmt_cpu $cpu_lim_used_m) / $(fmt_cpu $cpu_lim_hard_m)"
|
||||
mem_req_label="$(fmt_mem $mem_req_used_mib) / $(fmt_mem $mem_req_hard_mib)"
|
||||
mem_lim_label="$(fmt_mem $mem_lim_used_mib) / $(fmt_mem $mem_lim_hard_mib)"
|
||||
|
||||
# ── Output ───────────────────────────────────────────────────────────────────
|
||||
|
||||
jq -nc \
|
||||
--argjson podMetrics "$pod_metrics" \
|
||||
--argjson cpuActualM "$cpu_actual" \
|
||||
--argjson memActualMi "$mem_actual" \
|
||||
--argjson cpuReqPct "$cpu_req_pct" \
|
||||
--argjson cpuLimPct "$cpu_lim_pct" \
|
||||
--argjson memReqPct "$mem_req_pct" \
|
||||
--argjson memLimPct "$mem_lim_pct" \
|
||||
--arg cpuReqLabel "$cpu_req_label" \
|
||||
--arg cpuLimLabel "$cpu_lim_label" \
|
||||
--arg memReqLabel "$mem_req_label" \
|
||||
--arg memLimLabel "$mem_lim_label" \
|
||||
'{
|
||||
podMetrics: $podMetrics,
|
||||
quota: {
|
||||
cpuActualM: $cpuActualM,
|
||||
memActualMi: $memActualMi,
|
||||
cpuReqPct: $cpuReqPct,
|
||||
cpuLimPct: $cpuLimPct,
|
||||
memReqPct: $memReqPct,
|
||||
memLimPct: $memLimPct,
|
||||
cpuReqLabel: $cpuReqLabel,
|
||||
cpuLimLabel: $cpuLimLabel,
|
||||
memReqLabel: $memReqLabel,
|
||||
memLimLabel: $memLimLabel
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Test standalone**
|
||||
|
||||
```bash
|
||||
chezmoi apply ~/.config/quickshell/scripts/k8s-metrics.sh
|
||||
~/.config/quickshell/scripts/k8s-metrics.sh tenant-5 | jq .
|
||||
```
|
||||
|
||||
Expected: JSON with `podMetrics` array (joplin/n8n/shared-postgres have real cpuM/memMi; dnddrugs has -1/-1) and `quota` object with percentages matching `kubectl get resourcequota -n tenant-5 -o json`.
|
||||
|
||||
Spot-check manually:
|
||||
```bash
|
||||
# cpuLimPct should be ~0.73 (1450m used / 2000m hard)
|
||||
kubectl get resourcequota -n tenant-5 -o jsonpath='{.items[0].status}'
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add scripts/executable_k8s-metrics.sh
|
||||
git commit -m "k8s-widget: add k8s-metrics.sh"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Kubernetes.qml Singleton
|
||||
|
||||
**Files:**
|
||||
- Create: `shared/Kubernetes.qml`
|
||||
|
||||
- [ ] **Step 1: Write the Singleton**
|
||||
|
||||
Create `shared/Kubernetes.qml`:
|
||||
|
||||
```qml
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property string status: "loading"
|
||||
property var pods: []
|
||||
property int readyCount: 0
|
||||
property int totalCount: 0
|
||||
property var quota: null
|
||||
property int lastUpdatedSecs: 0
|
||||
|
||||
// Increment seconds since last successful status fetch
|
||||
Timer {
|
||||
interval: 1000
|
||||
running: root.status !== "loading"
|
||||
repeat: true
|
||||
onTriggered: root.lastUpdatedSecs++
|
||||
}
|
||||
|
||||
// Staleness check — flip to "stale" if no update for >60s
|
||||
Timer {
|
||||
interval: 10000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
if (root.lastUpdatedSecs > 60
|
||||
&& (root.status === "ok" || root.status === "degraded")) {
|
||||
root.status = "stale";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Status script ────────────────────────────────────────────────────────
|
||||
Process {
|
||||
id: statusProc
|
||||
command: ["bash", Config.scriptsDir + "/k8s-status.sh", Config.kubeNamespace]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (this.text.trim() === "") {
|
||||
root.status = (root.status === "ok" || root.status === "degraded")
|
||||
? "stale" : "error";
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let data = JSON.parse(this.text);
|
||||
// Preserve existing cpuM/memMi from last metrics fetch
|
||||
let byApp = {};
|
||||
for (let p of root.pods) byApp[p.app] = p;
|
||||
root.pods = data.pods.map(p => ({
|
||||
app: p.app,
|
||||
ready: p.ready,
|
||||
cpuM: byApp[p.app]?.cpuM ?? -1,
|
||||
memMi: byApp[p.app]?.memMi ?? -1
|
||||
}));
|
||||
root.readyCount = data.readyCount;
|
||||
root.totalCount = data.totalCount;
|
||||
root.status = data.pods.every(p => p.ready) ? "ok" : "degraded";
|
||||
root.lastUpdatedSecs = 0;
|
||||
} catch(e) {
|
||||
console.warn("Kubernetes: status parse error:", e);
|
||||
root.status = (root.status === "ok" || root.status === "degraded")
|
||||
? "stale" : "error";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: Config.kubeStatusRefreshMs
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: statusProc.running = true
|
||||
}
|
||||
|
||||
// ─── Metrics script ───────────────────────────────────────────────────────
|
||||
Process {
|
||||
id: metricsProc
|
||||
command: ["bash", Config.scriptsDir + "/k8s-metrics.sh", Config.kubeNamespace]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (this.text.trim() === "") return;
|
||||
try {
|
||||
let data = JSON.parse(this.text);
|
||||
let byApp = {};
|
||||
for (let m of data.podMetrics) byApp[m.app] = m;
|
||||
root.pods = root.pods.map(p => ({
|
||||
app: p.app,
|
||||
ready: p.ready,
|
||||
cpuM: byApp[p.app]?.cpuM ?? -1,
|
||||
memMi: byApp[p.app]?.memMi ?? -1
|
||||
}));
|
||||
root.quota = data.quota;
|
||||
} catch(e) {
|
||||
console.warn("Kubernetes: metrics parse error:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: Config.kubeMetricsRefreshMs
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: metricsProc.running = true
|
||||
}
|
||||
|
||||
// Stagger metrics 500ms after status to avoid a CPU spike at startup
|
||||
Timer {
|
||||
interval: 500
|
||||
repeat: false
|
||||
onTriggered: metricsProc.running = true
|
||||
}
|
||||
|
||||
Component.onCompleted: statusProc.running = true
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Apply and verify quickshell loads without errors**
|
||||
|
||||
```bash
|
||||
chezmoi apply ~/.config/quickshell/shared/Kubernetes.qml
|
||||
# Restart quickshell. Check for QML errors:
|
||||
# (kill and relaunch, or use your normal quickshell reload mechanism)
|
||||
# The bar should appear as before — Kubernetes.qml is loaded but nothing displays yet.
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add shared/Kubernetes.qml
|
||||
git commit -m "k8s-widget: add Kubernetes.qml singleton"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: KubernetesPill + Bar wiring (pill only)
|
||||
|
||||
**Files:**
|
||||
- Create: `bar/KubernetesPill.qml`
|
||||
- Modify: `Bar.qml` (pill only — popout comes in Task 6)
|
||||
|
||||
- [ ] **Step 1: Write KubernetesPill.qml**
|
||||
|
||||
Create `bar/KubernetesPill.qml`:
|
||||
|
||||
```qml
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import "../shared" as Shared
|
||||
|
||||
BarPill {
|
||||
groupName: "kubernetes"
|
||||
accentColor: {
|
||||
let s = Shared.Kubernetes.status;
|
||||
if (s === "ok") return Shared.Theme.green;
|
||||
if (s === "degraded") return Shared.Theme.yellow;
|
||||
if (s === "error") return Shared.Theme.red;
|
||||
if (s === "stale") return Shared.Theme.overlay0;
|
||||
return Shared.Theme.blue; // "loading"
|
||||
}
|
||||
|
||||
content: [
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: "\u{f03be}" // Nerd Font kubernetes wheel
|
||||
color: {
|
||||
let s = Shared.Kubernetes.status;
|
||||
if (s === "ok") return Shared.Theme.green;
|
||||
if (s === "degraded") return Shared.Theme.yellow;
|
||||
if (s === "error") return Shared.Theme.red;
|
||||
if (s === "stale") return Shared.Theme.overlay0;
|
||||
return Shared.Theme.blue;
|
||||
}
|
||||
font.pixelSize: Shared.Theme.fontLarge
|
||||
font.family: Shared.Theme.iconFont
|
||||
},
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: Shared.Kubernetes.status === "loading"
|
||||
? "…"
|
||||
: Shared.Kubernetes.readyCount + "/" + Shared.Kubernetes.totalCount
|
||||
color: {
|
||||
let s = Shared.Kubernetes.status;
|
||||
if (s === "ok") return Shared.Theme.green;
|
||||
if (s === "degraded") return Shared.Theme.yellow;
|
||||
if (s === "error") return Shared.Theme.red;
|
||||
if (s === "stale") return Shared.Theme.overlay0;
|
||||
return Shared.Theme.blue;
|
||||
}
|
||||
font.pixelSize: Shared.Theme.fontSmall
|
||||
font.family: Shared.Theme.fontFamily
|
||||
font.bold: true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add pill to Bar.qml**
|
||||
|
||||
In `Bar.qml`, find the line `BarComponents.SystemPill { id: systemBtn }` (currently the last item in the bottom section, line ~127). Add `KubernetesPill` **directly above it**:
|
||||
|
||||
```qml
|
||||
BarComponents.KubernetesPill { id: kubernetesBtn }
|
||||
|
||||
BarComponents.SystemPill { id: systemBtn }
|
||||
```
|
||||
|
||||
The surrounding context for orientation:
|
||||
```qml
|
||||
BarComponents.BarPill {
|
||||
id: notifBtn
|
||||
// ... (notification pill, unchanged)
|
||||
}
|
||||
|
||||
BarComponents.KubernetesPill { id: kubernetesBtn } // ← insert here
|
||||
|
||||
BarComponents.SystemPill { id: systemBtn }
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Apply and verify pill appears**
|
||||
|
||||
```bash
|
||||
chezmoi apply ~/.config/quickshell/bar/KubernetesPill.qml \
|
||||
~/.config/quickshell/Bar.qml
|
||||
# Restart quickshell
|
||||
# Expected: k8s wheel icon + "4/4" appears in the bar in green
|
||||
# Clicking it does nothing yet (no popout wired)
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add bar/KubernetesPill.qml Bar.qml
|
||||
git commit -m "k8s-widget: add KubernetesPill to bar"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: KubernetesPopout + full Bar.qml wiring
|
||||
|
||||
**Files:**
|
||||
- Create: `bar/popouts/KubernetesPopout.qml`
|
||||
- Modify: `Bar.qml` (add PopoutSlot + update visible guard)
|
||||
|
||||
- [ ] **Step 1: Write KubernetesPopout.qml**
|
||||
|
||||
Create `bar/popouts/KubernetesPopout.qml`:
|
||||
|
||||
```qml
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import "../../shared" as Shared
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
implicitWidth: Shared.Theme.popoutWidth
|
||||
implicitHeight: col.implicitHeight + Shared.Theme.popoutPadding * 2
|
||||
|
||||
PopoutBackground { anchors.fill: parent }
|
||||
MouseArea { anchors.fill: parent }
|
||||
|
||||
// Reusable quota bar component
|
||||
component QuotaBar: Column {
|
||||
property string rowLabel: ""
|
||||
property string valueLabel: ""
|
||||
property real fill: 0
|
||||
property color barColor: Shared.Theme.green
|
||||
property real barOpacity: 1.0
|
||||
|
||||
Layout.fillWidth: true
|
||||
width: parent.width
|
||||
spacing: 3
|
||||
|
||||
RowLayout {
|
||||
width: parent.width
|
||||
Text {
|
||||
text: rowLabel
|
||||
color: Shared.Theme.overlay0
|
||||
font.pixelSize: 9
|
||||
font.family: Shared.Theme.fontFamily
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
Text {
|
||||
text: valueLabel
|
||||
color: Shared.Theme.subtext0
|
||||
font.pixelSize: 9
|
||||
font.family: Shared.Theme.fontFamily
|
||||
}
|
||||
Text {
|
||||
text: Math.round(fill * 100) + "%"
|
||||
color: barColor
|
||||
font.pixelSize: 9
|
||||
font.family: Shared.Theme.fontFamily
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 4
|
||||
color: Shared.Theme.surface1
|
||||
radius: 2
|
||||
Rectangle {
|
||||
width: Math.max(0, Math.min(1, parent.parent.fill)) * parent.width
|
||||
height: parent.height
|
||||
color: parent.parent.barColor
|
||||
opacity: parent.parent.barOpacity
|
||||
radius: 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: col
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Shared.Theme.popoutPadding
|
||||
spacing: 8
|
||||
|
||||
// ── Header ────────────────────────────────────────────
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Text {
|
||||
text: "K8s — " + Shared.Config.kubeNamespace
|
||||
color: Shared.Theme.text
|
||||
font.pixelSize: 16
|
||||
font.family: Shared.Theme.fontFamily
|
||||
font.bold: true
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Text {
|
||||
text: "\u{f03be}"
|
||||
color: {
|
||||
let s = Shared.Kubernetes.status;
|
||||
if (s === "ok") return Shared.Theme.green;
|
||||
if (s === "degraded") return Shared.Theme.yellow;
|
||||
if (s === "error") return Shared.Theme.red;
|
||||
if (s === "stale") return Shared.Theme.overlay0;
|
||||
return Shared.Theme.blue;
|
||||
}
|
||||
font.pixelSize: 20
|
||||
font.family: Shared.Theme.iconFont
|
||||
}
|
||||
}
|
||||
|
||||
// ── Pods section ──────────────────────────────────────
|
||||
Text {
|
||||
text: "PODS"
|
||||
color: Shared.Theme.overlay0
|
||||
font.pixelSize: 9
|
||||
font.family: Shared.Theme.fontFamily
|
||||
font.letterSpacing: 1
|
||||
}
|
||||
|
||||
// Column header row
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
Item { Layout.fillWidth: true }
|
||||
Text { text: "CPU"; color: Shared.Theme.overlay0; font.pixelSize: 8; font.family: Shared.Theme.fontFamily; Layout.preferredWidth: 44; horizontalAlignment: Text.AlignRight }
|
||||
Text { text: "MEM"; color: Shared.Theme.overlay0; font.pixelSize: 8; font.family: Shared.Theme.fontFamily; Layout.preferredWidth: 52; horizontalAlignment: Text.AlignRight }
|
||||
Text { text: "STATUS"; color: Shared.Theme.overlay0; font.pixelSize: 8; font.family: Shared.Theme.fontFamily; Layout.preferredWidth: 62; horizontalAlignment: Text.AlignRight }
|
||||
}
|
||||
|
||||
// Pod rows
|
||||
Repeater {
|
||||
model: Shared.Kubernetes.pods
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 28
|
||||
color: Shared.Theme.surface0
|
||||
radius: Shared.Theme.radiusSmall
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 10
|
||||
anchors.rightMargin: 10
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
text: modelData.app
|
||||
color: Shared.Theme.text
|
||||
font.pixelSize: 11
|
||||
font.family: Shared.Theme.fontFamily
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
Text {
|
||||
text: modelData.cpuM === -1 ? "—" : modelData.cpuM + "m"
|
||||
color: Shared.Theme.sky
|
||||
font.pixelSize: 11
|
||||
font.family: Shared.Theme.fontFamily
|
||||
Layout.preferredWidth: 44
|
||||
horizontalAlignment: Text.AlignRight
|
||||
}
|
||||
Text {
|
||||
text: modelData.memMi === -1 ? "—" : modelData.memMi + "Mi"
|
||||
color: Shared.Theme.mauve
|
||||
font.pixelSize: 11
|
||||
font.family: Shared.Theme.fontFamily
|
||||
Layout.preferredWidth: 52
|
||||
horizontalAlignment: Text.AlignRight
|
||||
}
|
||||
RowLayout {
|
||||
spacing: 3
|
||||
Layout.preferredWidth: 62
|
||||
layoutDirection: Qt.RightToLeft
|
||||
Text {
|
||||
text: modelData.ready ? "Ready" : "NotReady"
|
||||
color: modelData.ready ? Shared.Theme.green : Shared.Theme.red
|
||||
font.pixelSize: 10
|
||||
font.family: Shared.Theme.fontFamily
|
||||
}
|
||||
Text {
|
||||
text: "●"
|
||||
color: modelData.ready ? Shared.Theme.green : Shared.Theme.red
|
||||
font.pixelSize: 9
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Quota section ─────────────────────────────────────
|
||||
Text {
|
||||
text: "QUOTA"
|
||||
color: Shared.Theme.overlay0
|
||||
font.pixelSize: 9
|
||||
font.family: Shared.Theme.fontFamily
|
||||
font.letterSpacing: 1
|
||||
topPadding: 4
|
||||
}
|
||||
|
||||
// CPU card
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
color: Shared.Theme.surface0
|
||||
radius: Shared.Theme.radiusSmall
|
||||
implicitHeight: cpuCardCol.implicitHeight + 20
|
||||
|
||||
Column {
|
||||
id: cpuCardCol
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: 12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 6
|
||||
|
||||
RowLayout {
|
||||
width: parent.width
|
||||
Text { text: "CPU"; color: Shared.Theme.sky; font.pixelSize: 10; font.family: Shared.Theme.fontFamily; font.bold: true }
|
||||
Item { Layout.fillWidth: true }
|
||||
Text {
|
||||
text: Shared.Kubernetes.quota ? Shared.Kubernetes.quota.cpuActualM + "m actual" : "—"
|
||||
color: Shared.Theme.text
|
||||
font.pixelSize: 10
|
||||
font.family: Shared.Theme.fontFamily
|
||||
}
|
||||
}
|
||||
QuotaBar {
|
||||
rowLabel: "%REQ"
|
||||
valueLabel: Shared.Kubernetes.quota?.cpuReqLabel ?? ""
|
||||
fill: Shared.Kubernetes.quota?.cpuReqPct ?? 0
|
||||
barColor: Shared.Theme.green
|
||||
}
|
||||
QuotaBar {
|
||||
rowLabel: "%LIM"
|
||||
valueLabel: Shared.Kubernetes.quota?.cpuLimLabel ?? ""
|
||||
fill: Shared.Kubernetes.quota?.cpuLimPct ?? 0
|
||||
barColor: Shared.Theme.yellow
|
||||
barOpacity: 0.6
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MEM card
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
color: Shared.Theme.surface0
|
||||
radius: Shared.Theme.radiusSmall
|
||||
implicitHeight: memCardCol.implicitHeight + 20
|
||||
|
||||
Column {
|
||||
id: memCardCol
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: 12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 6
|
||||
|
||||
RowLayout {
|
||||
width: parent.width
|
||||
Text { text: "MEM"; color: Shared.Theme.mauve; font.pixelSize: 10; font.family: Shared.Theme.fontFamily; font.bold: true }
|
||||
Item { Layout.fillWidth: true }
|
||||
Text {
|
||||
text: Shared.Kubernetes.quota ? Shared.Kubernetes.quota.memActualMi + "Mi actual" : "—"
|
||||
color: Shared.Theme.text
|
||||
font.pixelSize: 10
|
||||
font.family: Shared.Theme.fontFamily
|
||||
}
|
||||
}
|
||||
QuotaBar {
|
||||
rowLabel: "%REQ"
|
||||
valueLabel: Shared.Kubernetes.quota?.memReqLabel ?? ""
|
||||
fill: Shared.Kubernetes.quota?.memReqPct ?? 0
|
||||
barColor: Shared.Theme.mauve
|
||||
}
|
||||
QuotaBar {
|
||||
rowLabel: "%LIM"
|
||||
valueLabel: Shared.Kubernetes.quota?.memLimLabel ?? ""
|
||||
fill: Shared.Kubernetes.quota?.memLimPct ?? 0
|
||||
barColor: Shared.Theme.mauve
|
||||
barOpacity: 0.6
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Footer ────────────────────────────────────────────
|
||||
Text {
|
||||
text: "Updated " + Shared.Kubernetes.lastUpdatedSecs + "s ago"
|
||||
color: Shared.Theme.overlay0
|
||||
font.pixelSize: 9
|
||||
font.family: Shared.Theme.fontFamily
|
||||
Layout.alignment: Qt.AlignRight
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add PopoutSlot to Bar.qml and update visible guard**
|
||||
|
||||
In `Bar.qml`, inside the `Item { id: popoutArea }` block (around line 175), add after the `systemSlot` PopoutSlot:
|
||||
|
||||
```qml
|
||||
PopoutSlot {
|
||||
id: kubernetesSlot
|
||||
name: "kubernetes"
|
||||
verticalAnchor: "bottom"
|
||||
sourceComponent: Popouts.KubernetesPopout {}
|
||||
}
|
||||
```
|
||||
|
||||
Then update the `visible:` line on `popoutWindow` (line 145) to include `kubernetesSlot.animating`:
|
||||
|
||||
```qml
|
||||
visible: modelData.name === Shared.Config.monitor && (popoutOpen || notifSlot.animating || mediaSlot.animating || weatherSlot.animating || datetimeSlot.animating || systemSlot.animating || kubernetesSlot.animating)
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Apply and verify full widget**
|
||||
|
||||
```bash
|
||||
chezmoi apply ~/.config/quickshell/bar/popouts/KubernetesPopout.qml \
|
||||
~/.config/quickshell/Bar.qml
|
||||
# Restart quickshell
|
||||
```
|
||||
|
||||
Verify:
|
||||
1. Pill shows `4/4` in green
|
||||
2. Click pill → popout opens (smooth animation matching other popouts)
|
||||
3. Pod table shows all 4 pods; joplin/n8n/shared-postgres have CPU/MEM values; dnddrugs shows `—`
|
||||
4. Quota bars for CPU and MEM match values from:
|
||||
```bash
|
||||
kubectl top pods -n tenant-5
|
||||
kubectl get resourcequota -n tenant-5 -o json
|
||||
```
|
||||
5. Footer shows "Updated Xs ago" incrementing each second
|
||||
6. Click outside popout → closes cleanly
|
||||
|
||||
- [ ] **Step 4: Test degraded state**
|
||||
|
||||
```bash
|
||||
kubectl rollout restart deployment/joplin -n tenant-5
|
||||
# Watch pill: within ~30s it should turn yellow and show 3/4
|
||||
# Once joplin pod is Ready again: pill returns to green 4/4
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add bar/popouts/KubernetesPopout.qml Bar.qml
|
||||
git commit -m "k8s-widget: add KubernetesPopout and complete bar wiring"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
```bash
|
||||
# Scripts work standalone
|
||||
~/.config/quickshell/scripts/k8s-status.sh tenant-5 | jq .
|
||||
~/.config/quickshell/scripts/k8s-metrics.sh tenant-5 | jq .
|
||||
|
||||
# Full widget smoke test (after chezmoi apply + quickshell restart)
|
||||
# ✓ Pill visible with correct count + green color
|
||||
# ✓ Popout opens on click, closes on outside click or Escape
|
||||
# ✓ Pod table accurate (compare to: kubectl get pods -n tenant-5)
|
||||
# ✓ Quota bars accurate (compare to: kubectl get resourcequota -n tenant-5 -o json)
|
||||
# ✓ dnddrugs row shows — for CPU/MEM
|
||||
# ✓ No animation glitches (popout fades/scales like other popouts)
|
||||
# ✓ Degraded state: kubectl rollout restart deployment/joplin → yellow pill, NotReady row
|
||||
```
|
||||
+279
@@ -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
|
||||
```
|
||||
Reference in New Issue
Block a user