--- name: cockpit-custom-packages description: "Build and debug custom Cockpit web console packages (dashboards, monitoring pages). Covers manifest CSP, JavaScript API quirks in Cockpit 360, and the data-flow pattern for live dashboards." version: 1.1.0 category: devops tags: [cockpit, monitoring, dashboard, gpu, nvidia, web-ui] platforms: [linux] --- # Cockpit Custom Packages Build and debug custom package pages for Cockpit (the Linux web console). Covers manifest setup, JavaScript API quirks in Cockpit 360, and the data-flow pattern for live dashboards. ## Architecture Pattern Cockpit packages live under `/usr/share/cockpit//` with: - `manifest.json` — registers the package in Cockpit's sidebar - `index.html` — the dashboard page **Data flow for live monitoring**: a systemd service writes data to files every N seconds; the page reads those files to display. The page cannot run `nvidia-smi` or other commands directly — Cockpit 360's bridge sandboxes execution. ## Manifest (manifest.json) ```json { "version": 1, "requires": { "cockpit": "260" }, "menu": { "index": { "label": "My Dashboard", "order": 90 } }, "content-security-policy": "default-src 'self'; script-src 'self' 'unsafe-inline'; connect-src 'self' https://localhost:9090 wss://localhost:9090" } ``` ### Critical CSP Rule The CSP MUST include `'self'` in `script-src`. If you write only `script-src 'unsafe-inline'` (a common mistake), it OVERRIDES the `'self'` inherited from `default-src`, and Cockpit blocks ALL external script files including `cockpit.js`. Symptom: page renders HTML but NO JavaScript executes — `cockpit` is undefined, no API calls work, page appears frozen. ## CRITICAL: cockpit.js Path in Cockpit 360+ **Cockpit ≥360 stores `cockpit.js` as `cockpit.js.gz`.** The relative path `../base1/cockpit.js` does NOT resolve through Cockpit's transparent gzip decompression from inside a package directory. The script silently fails to load, and NONE of the Cockpit JavaScript API works. **Always use the absolute path:** ```html ``` **Symptom when broken:** Page title/heading renders but no JavaScript executes. No console errors may be visible. All API calls silently do nothing. > **Note:** The umbrella `cockpit-custom-packages` absorbed `cockpit-monitoring` and `cockpit-plugin-dev`. The cockpit.js.gz insight came from `cockpit-monitoring`; detailed API sandboxing notes are in `references/cockpit-360-quirks.md` (from `cockpit-plugin-dev`). ## Cockpit 360 JavaScript API ### What works | API | Works? | Notes | |-----|--------|-------| | `cockpit.spawn(["cat", path], {err:"message"})` | **YES** | Use to read pre-written data files | | `cockpit.script("cmd")` | **NO** — hangs silently | Command never resolves or rejects | | `cockpit.file(path).read()` | **NO** — hangs silently | Both relative and absolute paths fail | | `cockpit.info` | **YES** | Version, user info, etc. | ### Reading data files ```javascript var PKG = "/usr/share/cockpit/my-package"; cockpit.spawn(["cat", PKG + "/data.txt"], {err: "message"}) .done(function(data) { // data contains file contents var fields = data.trim().split(', '); }) .fail(function(exitCode, data) { // exitCode is the problem description string when err:"message" }); ``` ## Deployment - **Always restart cockpit after deploy**: `sudo systemctl restart cockpit` then verify in an **Incognito/Private window**. Cockpit aggressively caches HTML — hard refresh (Ctrl+Shift+R) often isn't enough. The correct verification sequence: 1. `sudo systemctl restart cockpit` 2. Open a **private/incognito browser window** → login to Cockpit → check the page 3. If it works there, the old tab had stale cache. Force-reload the old tab via DevTools → right-click refresh → "Empty Cache and Hard Reload" 4. For stubborn cases, add cache-busting meta tags to ``: `` — these help but Cockpit's internal caching may still serve stale content, so the private window test is the gold standard. - **Manifest changes**: `sudo cp` then `sudo systemctl restart cockpit` — same caching rules apply. **Do NOT assume a hard refresh (Ctrl+Shift+R) is enough** — it often isn't for Cockpit packages. Always restart cockpit AND verify in a private window if the user reports no visual change after a deploy. ### Editing files in /usr/share/cockpit/ The `patch` and `write_file` tools cannot write to `/usr/share/cockpit/` because they lack sudo. Use one of these patterns instead: ```bash # Write to a temp file first, then sudo cp cat > /tmp/new-page.html << 'EOF' ...content... EOF sudo cp /tmp/new-page.html /usr/share/cockpit//index.html # For a full rewrite, pipe through sudo tee sudo tee /usr/share/cockpit//index.html > /dev/null << 'EOF' ...content... EOF ``` For complex multi-card HTML, prefer rebuilding with a Python `execute_code` script (safer than multi-line `sed` which can miss tag boundaries and break nesting). ## Dashboard Layout ### Card grid layout pitfall (most common bug) When cards appear outside their intended grid column (stacked full-width down the page), the cause is almost always a **misplaced ``** closing the grid container early. Example of the bug: ```html
...
...
...
``` **To diagnose**: Run `grep -c '
`. An extra `
` between cards closes the grid and dumps subsequent cards outside it. **When removing elements like ring gauges**, remove the entire container `
` — don't just delete inner content, as leftover closing tags break the nest hierarchy. Use Python string replacement on the full HTML rather than multi-line `sed`. ### Clean multi-card grid template ```html

🌡 Thermal

...

🔥 CPU

...

💾 Memory

...

🔧 Details

...

⚡ Power

...

📊 GPU Load

...
...

🔄 Processes

...
``` Each card is one self-contained `div` with no nested block elements crossing card boundaries. No text nodes or wrapping divs between cards inside the grid. ## Dashboard Design Preferences For GPU/system monitoring dashboards in Cockpit, prefer: - ✅ **Stat rows**: left-label/right-value flex rows with `justify-content: space-between`. Compact, clean, updates live. - ✅ **Temperature cards**: big number (2.5rem) with mini side-stats (fan, util, PCH, ACPI) in a `temp-group` flex layout. - ❌ **Ring gauges / SVG donuts**: avoid unless the user explicitly requests one. They take up card space, look busy, and the user will ask you to shrink or remove them. - Each card gets a colored `3px solid` accent top border to distinguish categories (blue=thermal, cyan=CPU, orange=memory, purple=details, red=power, green=GPU load). - All numeric values use `font-family: "SF Mono", "JetBrains Mono", "Fira Code", monospace` with `font-weight: 600`. - If a ring gauge is requested per user request: default to 16-30px SVG with thin 4px stroke, and be ready to remove it on feedback. ## CPU Temperature Monitoring Add CPU temp alongside GPU stats by extending the same monitor script. Full details in `references/cpu-temp-data-format.md`. **Monitor script addition** — insert before `sleep 5`: ```bash pkg=$(cat /sys/class/thermal/thermal_zone3/temp 2>/dev/null || echo 0) pch=$(cat /sys/class/thermal/thermal_zone2/temp 2>/dev/null || echo 0) acpi=$(cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null || echo 0) echo "$((pkg/1000)),$((pch/1000)),$((acpi/1000))" > /usr/share/cockpit/nvidia-gpu/cpu.txt ``` **Dashboard card** — use `accent-cyan` for CPU: ```css .card.accent-cyan{border-top:3px solid #06b6d4} ``` ## NVIDIA GPU Data Format The `nvidia-gpu-monitor` systemd service writes three text files every 5 seconds. ### gpu.txt (main stats) ``` NVIDIA GeForce GTX 1050 Ti, 580.159.03, 44, 0, 445, 4096, [N/A], P0, 31 ``` Fields (comma+space separated): | Index | Field | Example | Notes | |-------|-------|---------|-------| | 0 | Model | `NVIDIA GeForce GTX 1050 Ti` | Full GPU name | | 1 | Driver | `580.159.03` | Driver version string | | 2 | Temp | `44` | Integer °C | | 3 | Util % | `0` | Integer percentage | | 4 | Mem Used | `445` | MiB (not GiB) | | 5 | Mem Total | `4096` | MiB | | 6 | Power | `[N/A]` | Watts or `[N/A]` if no sensor | | 7 | Perf State | `P0` | P0=active, P8/P12=idle | | 8 | Fan % | `31` | Integer or `[N/A]` | **Parsing**: `data.trim().split(', ')` → 9-element array. Always check `p.length >= 9`. **Memory display helper**: ```javascript function fmt(m){ var v=parseInt(m); if(isNaN(v)) return m+' MiB'; return v>=1024 ? (v/1024).toFixed(1)+' GiB' : v+' MiB'; } ``` **Fan display**: if value is `[N/A]` or `"0"`, show `—` (em dash) instead of `0%` — many Pascal cards have zero-RPM mode below 50°C. ### cuda.txt / processes.txt ``` cuda.txt: 13.0 processes.txt: 1234, python3, 256 \n 5678, ffmpeg, 1024 ``` ### cpu.txt ``` 48,43,46 ``` Comma-separated: CPU package, PCH, ACPI temperatures in °C. See `references/cpu-temp-data-format.md` for full details. ## SVG Ring Gauge Technique Use only when the user requests it. Default to small (30px) with thin stroke (4): ```html
0%
``` Circumference `C = 2*pi*15.5 ≈ 97.4`. Set `stroke-dashoffset = C - (pct/100 * C)` in JS. SVG container sized via `.ring-svg{width:Npx;height:Npx}`. **Dark mode gotcha**: The ring background uses `stroke: var(--pf-t--global--background--color--secondary--default,#eee)` — on Cockpit's dark theme this resolves to dark gray/black, making the ring look like a "black circle." This is the background track, not the usage arc. ## Troubleshooting: "NO GPU Detected" When Data Files Are Valid If the Cockpit GPU page shows "⚠ No NVIDIA GPU detected" but `cat /usr/share/cockpit/nvidia-gpu/gpu.txt` has real data: | Check | What to do | |-------|-----------| | 1️⃣ Verify JS loaded | Open browser DevTools → Console. If `cockpit` is undefined, the `