---
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/<name>/` 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
<!-- WRONG — fails in Cockpit 360+ -->
<script src="../base1/cockpit.js"></script>

<!-- RIGHT — always works -->
<script src="/cockpit/base1/cockpit.js"></script>
```

**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 `<head>`: `<meta http-equiv="cache-control" content="no-cache, no-store, must-revalidate">` — 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/<name>/index.html

# For a full rewrite, pipe through sudo tee
sudo tee /usr/share/cockpit/<name>/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 `</div>`** closing the grid container early.

Example of the bug:
```html
<div class="grid">
  <div class="card">...</div>
  <div class="card">...</div>
</div>   <!-- ⚠ grid closes HERE — WRONG! -->
  <div class="card">...</div>  <!-- outside grid — renders full-width stacked -->
```

**To diagnose**: Run `grep -c '<div class="card"'` inside the grid section and compare to the expected count. Every card must be a direct child of `<div class="grid">`. An extra `</div>` between cards closes the grid and dumps subsequent cards outside it.

**When removing elements like ring gauges**, remove the entire container `<div>` — 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
<div class="grid">
  <div class="card accent-blue"><h2>🌡 Thermal</h2>...</div>
  <div class="card accent-cyan"><h2>🔥 CPU</h2>...</div>
  <div class="card accent-orange"><h2>💾 Memory</h2>...</div>
  <div class="card accent-purple"><h2>🔧 Details</h2>...</div>
  <div class="card accent-red"><h2>⚡ Power</h2>...</div>
  <div class="card accent-green"><h2>📊 GPU Load</h2>...</div>
  <div class="card model-card">...</div>
  <div class="card full"><h2>🔄 Processes</h2>...</div>
  <div class="full footer-row">...</div>
</div>
```

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
<div class="ring-container">
  <svg class="ring-svg" viewBox="0 0 36 36">
    <circle class="ring-bg" cx="18" cy="18" r="15.5"/>
    <circle class="ring-fg" id="uring" cx="18" cy="18" r="15.5"
      stroke-dasharray="97.4" stroke-dashoffset="97.4"/>
  </svg>
  <div class="ring-text" id="uptxt">0%</div>
</div>
```

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 `<script src>` path is wrong (Cockpit 360 needs absolute path `/cockpit/base1/cockpit.js`) |
| 2️⃣ Check package registration | `cockpit-bridge --packages \| grep nvidia` — should show the package path |
| 3️⃣ Verify monitor service | `systemctl status nvidia-gpu-monitor` — check it's running and files have recent timestamps |
| 4️⃣ **Browser cache** (most likely) | Cockpit aggressively caches HTML/JS. Try: DevTools → right-click refresh → "Empty Cache and Hard Reload" |
| 5️⃣ Private window test | Open an Incognito/Private window, login to Cockpit, check the GPU page. If it works there, the old tab had stale cache |
| 6️⃣ Restart Cockpit | `sudo systemctl restart cockpit` then verify in Private window (step 5) |

**Root cause when data is fine but page says "NO GPU":** Usually a stale browser cache serving an older version of the HTML that had a broken JS path, or the page loaded before the data files existed (race condition on first boot). The `.fail()` handler in `cockpit.spawn(["cat", ...])` shows the alert when the `cat` fails or returns empty data — Cockpit's cached HTML may reference a different file path than what's currently on disk.

**Prevention:** Add `stream=True` to the `cockpit.spawn()` call or add a 1-second retry in the update function. But the most reliable fix is the Private Window test to distinguish cache from actual malfunction.

## Reference Files

- `references/nvidia-gpu-dashboard.html` — deployable Cockpit dashboard page (stat-based, ring-gauge-free, cache-busting metas, CPU+GPU). Copy this to `/usr/share/cockpit/nvidia-gpu/index.html` as a starting point.
- `references/cpu-temp-data-format.md` — CPU temperature data format, sysfs sensor zones, adding CPU card to GPU dashboard (accent-cyan, temp-group layout, JS parsing).
- `references/nvidia-fan-curves.md` — GPU fan curve and power-draw reporting reference (GTX 1050 Ti).
- `references/cockpit-360-quirks.md` — Cockpit 360 JS API sandboxing notes: what works (spawn with cat), what fails (script, file.read for absolute paths), and diagnostic steps. (Absorbed from `cockpit-plugin-dev`.)
- `references/cockpit-connectivity.md` — Cockpit access and connectivity: socket activation (why service shows inactive), HTTPS requirement, self-signed cert, and Tailscale access.
- `templates/nvidia-gpu-monitor.service` — systemd unit template for the monitor script.
