initial commit

This commit is contained in:
ray
2026-07-12 10:17:17 -04:00
commit dab5a4ebc6
1424 changed files with 330463 additions and 0 deletions
@@ -0,0 +1,277 @@
---
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.
@@ -0,0 +1,40 @@
# Cockpit 360 JS API Sandboxing Notes
Server: rayserver, Cockpit version 360 (dpkg).
## What Works
- Basic page rendering (HTML, CSS, JS)
- Cockpit CSS theme (../base1/cockpit.css)
- Cockpit JS library (../base1/cockpit.js)
## What Fails
### cockpit.script() — ALWAYS FAILS for system commands
Even with full paths like `/usr/bin/nvidia-smi`, the call never resolves.
The `.done()` callback never fires, `.fail()` may not fire either — it just hangs.
Does NOT produce an error in browser console.
Tested: `cockpit.script("/usr/bin/nvidia-smi --query-gpu=name ...")` — hangs indefinitely.
### cockpit.file().read() — FAILS for absolute paths
Trying to read `/run/nvidia-gpu.txt`, `/tmp/anything``.fail()` fires.
The error object is empty or useless.
Likely Cockpit 360 restricts file reads to package-relative paths or whitelisted dirs.
### cockpit.spawn() — UNCONFIRMED
Currently testing: `cockpit.spawn(["cat", "/run/nvidia-gpu.txt"])`.
Theory: simpler commands (cat vs nvidia-smi) may work since they don't need GPU driver access.
## What's Working (infrastructure)
- `nvidia-gpu-monitor.service` — writes every 5s to:
- `/run/nvidia-gpu.txt` — GPU name, driver, temp, util, memory, power, pstate, fan
- `/run/nvidia-processes.txt` — PID, process_name, used_memory
- `/run/nvidia-cuda.txt` — CUDA version string
- Files are world-readable (644), owned by root
- `nvidia-smi` works perfectly from terminal and from the service
## Current Page Location
`/usr/share/cockpit/nvidia-gpu/index.html`
Manifest: `/usr/share/cockpit/nvidia-gpu/manifest.json` (CSP allows unsafe-inline, menu section)
@@ -0,0 +1,28 @@
# Cockpit Connectivity Notes
## Socket Activation (normal behavior)
Cockpit is socket-activated — `cockpit.socket` listens on `*:9090` and spawns `cockpit.service` on demand. Seeing `cockpit.service` as `inactive (dead)` is **normal** when no one is connected.
```
cockpit.socket → active (listening) on *:9090
cockpit.service → inactive (dead) — wakes on connection
```
## HTTPS required
Cockpit **refuses plain HTTP**. Always use `https://`:
- `https://<hostname>:9090`
- `http://<hostname>:9090` ✗ (connection refused or hangs)
## Self-signed certificate
Cockpit uses a self-signed cert by default. Browsers show a warning that must be bypassed (click "Advanced" → "Proceed").
## Tailscale access
With Tailscale installed on the server, access Cockpit via the Tailscale IP:
```
https://<tailscale-ip>:9090
```
No firewall changes needed — Cockpit listens on `*:9090` which includes the `tailscale0` interface. Same HTTPS + self-signed cert caveats apply.
@@ -0,0 +1,89 @@
# CPU Temperature Data Format (from nvidia-gpu-monitor)
The monitor script has been extended to write CPU temperatures alongside GPU data.
## cpu.txt (added by cockpit-custom-packages SKILL.md user session)
Written by the same systemd service (`nvidia-gpu-monitor`) that writes GPU data, every 5 seconds.
```
48,43,46
```
Comma-separated integers (no spaces):
| Index | Sensor | Source | Notes |
|-------|--------|--------|-------|
| 0 | CPU Package | `/sys/class/thermal/thermal_zone3/temp` | Main CPU temp, integer °C |
| 1 | PCH | `/sys/class/thermal/thermal_zone2/temp` | Platform Controller Hub temp |
| 2 | ACPI | `/sys/class/thermal/thermal_zone0/temp` | ACPI zone temp |
**Parsing**: `data.trim().split(',')` gives a 3-element array. Check `s.length >= 3` before accessing.
**Reading from shell**: `cat /sys/class/thermal/thermal_zone3/temp` returns millidegrees (e.g. `48000` = 48°C). Divide by 1000 for °C.
## Adding to the monitor script
Insert this block before the `sleep 5` in the monitor loop:
```bash
# CPU temps
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
```
## Adding a CPU Card to the Dashboard
Use the `accent-cyan` color class to distinguish from GPU cards:
```css
.card.accent-cyan{border-top:3px solid #06b6d4}
```
HTML pattern (compact, big-temp + mini stats):
```html
<div class="card accent-cyan"><h2>🔥 CPU</h2>
<div class="temp-group">
<div class="temp-big"><span class="temp-cold" id="cpu-temp">--</span><span class="unit">°C</span></div>
<div class="temp-mini">
<div class="temp-mini-item"><span>PCH</span><span class="val" id="pch-temp">-</span></div>
<div class="temp-mini-item"><span>ACPI</span><span class="val" id="acpi-temp">-</span></div>
</div>
</div>
<div class="bar-track"><div class="bar-fill temp" id="cpu-bar" style="width:0%"></div></div>
<div class="bar-labels"><span></span><span>50°</span><span>75°</span><span>100°</span></div>
</div>
```
JS to populate (inserted before the CUDA read):
```javascript
cockpit.spawn(["cat",PKG+"/cpu.txt"],{err:"message"}).done(function(d){
if(d&&d.trim()){var s=d.trim().split(',');if(s.length>=3){
var cp=parseInt(s[0]);$('cpu-temp').textContent=isNaN(cp)?'--':cp;
$('pch-temp').textContent=s[1]+'°';$('acpi-temp').textContent=s[2]+'°';
var tp=Math.min(100,Math.max(0,cp));$('cpu-bar').style.width=tp+'%';
var tc='temp-cold';if(cp>=50)tc='temp-mild';if(cp>=65)tc='temp-warm';if(cp>=80)tc='temp-hot';
$('cpu-temp').className=tc;
}}
});
```
Also add the 7th animation delay entry:
```css
.grid>*:nth-child(7){animation-delay:0.24s}
```
## Sensor Availability
Not all systems have all three thermal zones. The script uses `|| echo 0` fallback. Common zone mappings:
| Zone index | Typical sensor | Common on |
|------------|---------------|-----------|
| 0 | ACPI | All x86_64 |
| 1 | ACPI (secondary) | Laptops, multi-zone |
| 2 | PCH | Intel Comet Lake+ |
| 3 | x86_pkg_temp | All modern CPUs |
@@ -0,0 +1,39 @@
# NVIDIA Fan Curves (GTX 1050 Ti Reference)
The user's GTX 1050 Ti runs a passive/quiet fan curve baked into the GPU BIOS.
## Typical Stock Fan Curve
| Temp Range | Fan Speed | Behavior |
|-------------|------------|----------------------------------------------|
| < 50°C | 20-31% | Idle floor — stays at minimum for silence |
| 50-55°C | 30-40% | Start of ramp |
| 55-65°C | 40-60% | Moderate load |
| 65-75°C | 60-80% | Sustained load (gaming, rendering) |
| 75-80°C | 80-90% | Heavy load |
| 80°C+ | 90-100% | Thermal protection |
## Key Observations
- **31% at 44°C is normal**: The card was in P0 (active perf state) with 7% util, but 44°C is well below the 50°C ramp threshold. The fan stays at its idle floor.
- **Perf state matters**: P8 (idle) → P0 (active) transition happens when the card has work. Temp can climb to ~50°C before the fan responds meaningfully.
- **Fan at 0% or `[N/A]`**: Means the card relies on passive cooling or the fan hasn't spun up yet. Some Pascal cards have a zero-RPM mode below 50°C.
- **No user-side fan curve tool**: Custom fan curves require `nvidia-settings` (X11) or third-party tools — not available in headless/server setups.
## Power Draw Reporting
The GTX 1050 Ti (and other budget Pascal GPUs) **cannot report power draw** via `nvidia-smi`:
- `nvidia-smi --query-gpu=power.draw` returns `[N/A]`
- The card has a 75W TDP, runs off PCIe slot power only (no 6/8-pin connector), and lacks the power monitoring hardware/BIOS support
- This is not a driver issue or misconfiguration — the sensor simply doesn't exist on this hardware
- Cards that do report power: GTX 1060+, RTX series, and higher-end models with dedicated power sensors
- **What to tell the user**: "The GTX 1050 Ti doesn't have a power sensor. It's normal — the card literally can't tell us. Nothing to fix."
## What to Tell the User
If asked "why is the fan at X% when temp is Y°C":
1. Check perf state (P0 = active, P8/P12 = idle)
2. Compare temp against the curve above
3. Under 50°C at 20-31% = perfectly normal, BIOS prioritizes quiet
4. If temp exceeds 55°C and fan stays below 40%, something may be wrong (check for dust, thermal paste, or fan failure via `nvidia-smi -q -d TEMPERATURE`)
@@ -0,0 +1,153 @@
<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8"><title>NVIDIA GPU</title>
<meta http-equiv="cache-control" content="no-cache, no-store, must-revalidate">
<link rel="stylesheet" href="../base1/cockpit.css">
<script src="../base1/cockpit.js"></script>
<style>
body{padding:0;font-family:var(--pf-t--global--font--family--body,Inter,RedHatText,Overpass,sans-serif);background:var(--pf-t--global--background--color--primary--default,#f0f2f5)}
.ct-page-fill{padding:1.5rem;max-width:1100px;margin:0 auto}
.page-header{display:flex;align-items:center;gap:1rem;margin-bottom:1.5rem;padding:1rem 1.25rem;background:var(--pf-t--global--background--color--secondary--default,linear-gradient(135deg,#0f172a,#1e293b));border-radius:14px;border:1px solid var(--pf-t--global--border--color--default,rgba(255,255,255,0.08));box-shadow:0 2px 12px rgba(0,0,0,0.06)}
.page-header h1{font-size:1.15rem;font-weight:700;margin:0;background:linear-gradient(135deg,#38bdf8,#a78bfa);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
.page-header .subtitle{font-size:0.7rem;color:var(--pf-t--global--text--color--subtle,#94a3b8);margin-left:auto;font-family:"SF Mono","JetBrains Mono","Fira Code",monospace;background:var(--pf-t--global--background--color--secondary--default,rgba(255,255,255,0.06));padding:0.2rem 0.6rem;border-radius:6px}
.status-dot{width:10px;height:10px;border-radius:50%;display:inline-block;position:relative}
.status-dot::after{content:'';position:absolute;inset:-4px;border-radius:50%;opacity:0.25}
.status-dot.ok{background:#22c55e;box-shadow:0 0 10px #22c55e;animation:pulse-dot 2s ease-in-out infinite}
.status-dot.warn{background:#f59e0b} .status-dot.err{background:#ef4444}
@keyframes pulse-dot{0%,100%{opacity:0.25}50%{opacity:0.6}}
.grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:1rem}
@media(max-width:900px){.grid{grid-template-columns:1fr 1fr}}
@media(max-width:600px){.grid{grid-template-columns:1fr}}
.card{background:var(--pf-t--global--background--color--primary--default,#fff);border:1px solid var(--pf-t--global--border--color--default,#e2e8f0);border-radius:12px;padding:1.25rem;transition:box-shadow 0.2s}
.card:hover{box-shadow:0 4px 20px rgba(0,0,0,0.06)}
.card.full{grid-column:1/-1}
.card.accent-blue{border-top:3px solid #3b82f6}.card.accent-green{border-top:3px solid #22c55e}
.card.accent-orange{border-top:3px solid #f59e0b}.card.accent-purple{border-top:3px solid #a855f7}
.card.accent-red{border-top:3px solid #ef4444}.card.accent-cyan{border-top:3px solid #06b6d4}
.card h2{font-size:0.65rem;text-transform:uppercase;letter-spacing:0.06em;color:var(--pf-t--global--text--color--subtle,#94a3b8);margin:0 0 0.85rem 0;font-weight:700}
.stat{display:flex;justify-content:space-between;align-items:center;padding:0.45rem 0}
.stat+.stat{border-top:1px solid var(--pf-t--global--border--color--default,rgba(0,0,0,0.04))}
.stat-label{color:var(--pf-t--global--text--color--subtle,#64748b);font-size:0.82rem;font-weight:500}
.stat-value{font-weight:600;font-family:"SF Mono","JetBrains Mono","Fira Code",monospace;font-size:0.85rem;color:var(--pf-t--global--text--color--default,#1e293b);white-space:nowrap}
.temp-group{display:flex;align-items:center;gap:1.5rem;justify-content:center;flex-wrap:wrap;margin:0.3rem 0 0.5rem}
.temp-big{font-size:2.5rem;font-weight:300;font-family:"SF Mono";line-height:1;display:flex;align-items:baseline;gap:2px}
.temp-big .unit{font-size:1rem;color:#94a3b8}
.temp-cold{color:#3b82f6}.temp-mild{color:#22c55e}.temp-warm{color:#f59e0b}.temp-hot{color:#ef4444}
.temp-mini{display:flex;flex-direction:column;gap:0.2rem;min-width:70px}
.temp-mini-item{display:flex;justify-content:space-between;font-size:0.78rem;color:#64748b}
.temp-mini-item .val{font-family:"SF Mono";font-weight:600}
.bar-track{background:#e2e8f0;border-radius:6px;height:6px;overflow:hidden;margin:0.4rem 0}
.bar-fill{height:100%;border-radius:6px;transition:width 0.6s}
.bar-fill.green{background:linear-gradient(90deg,#22c55e,#16a34a)}
.bar-fill.amber{background:linear-gradient(90deg,#f59e0b,#d97706)}
.bar-fill.red{background:linear-gradient(90deg,#ef4444,#dc2626)}
.bar-fill.temp{background:linear-gradient(90deg,#3b82f6,#22c55e,#f59e0b,#ef4444)}
.bar-labels{display:flex;justify-content:space-between;font-size:0.6rem;color:#94a3b8;margin-top:2px}
.mem-row{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:0.3rem}
.mem-used{font-size:1.3rem;font-weight:600;font-family:"SF Mono";color:#1e293b}
.mem-of{font-size:0.78rem;color:#94a3b8}
.mem-pct{text-align:right;font-size:0.78rem;color:#94a3b8;font-family:"SF Mono";margin-top:2px}
.power-block{text-align:center;padding:0.4rem 0}
.power-num{font-size:1.6rem;font-weight:300;font-family:"SF Mono";color:#1e293b}
.power-label{font-size:0.68rem;color:#94a3b8;margin-top:0.1rem}
.model-card{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:0.4rem;text-align:center}
.model-card .name{font-weight:700;font-size:0.9rem;color:#1e293b}
.model-card .sub{font-size:0.72rem;color:#94a3b8}
table{width:100%;border-collapse:collapse;font-size:0.82rem}
th{text-align:left;padding:0.45rem 0.5rem;color:#94a3b8;font-weight:600;font-size:0.65rem;text-transform:uppercase;border-bottom:2px solid #e2e8f0}
td{padding:0.45rem 0.5rem;border-bottom:1px solid rgba(0,0,0,0.03)}
td:last-child{text-align:right;font-family:"SF Mono";font-weight:600}
tr:hover td{background:rgba(0,0,0,0.015)}
.footer-row{display:flex;gap:1rem;flex-wrap:wrap;margin-top:1rem;padding:0.5rem 1rem;background:rgba(0,0,0,0.015);border:1px solid rgba(0,0,0,0.04);border-radius:8px;font-size:0.72rem;color:#94a3b8}
.alert-box{display:none;background:linear-gradient(135deg,#fef2f2,#fff5f5);border:1px solid #fecaca;color:#991b1b;padding:0.9rem 1.25rem;border-radius:10px;margin-bottom:1rem;font-size:0.85rem;align-items:center;gap:0.6rem}
@keyframes fadeUp{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
.grid>*{animation:fadeUp 0.3s ease-out both}
.grid>*:nth-child(1){animation-delay:0s}
.grid>*:nth-child(2){animation-delay:0.04s}
.grid>*:nth-child(3){animation-delay:0.08s}
.grid>*:nth-child(4){animation-delay:0.12s}
.grid>*:nth-child(5){animation-delay:0.16s}
.grid>*:nth-child(6){animation-delay:0.20s}
.grid>*:nth-child(7){animation-delay:0.24s}
</style></head>
<body>
<div class="ct-page-fill">
<div id="nogpu" class="alert-box"><span class="icon"></span> No NVIDIA GPU detected</div>
<div class="page-header"><span class="status-dot" id="sdot"></span><h1>🎮 <span id="gpumodel">NVIDIA GPU</span></h1><span class="subtitle" id="pstate">P?</span></div>
<div id="content"><div class="grid">
<!-- Thermal -->
<div class="card accent-blue"><h2>🌡 Thermal</h2>
<div class="temp-group"><div class="temp-big"><span class="temp-cold" id="gt">--</span><span class="unit">°C</span></div>
<div class="temp-mini"><div class="temp-mini-item"><span>Fan</span><span class="val" id="gf"></span></div><div class="temp-mini-item"><span>Util</span><span class="val" id="gu">-</span></div></div></div>
<div class="bar-track"><div class="bar-fill temp" id="tbar" style="width:0%"></div></div>
<div class="bar-labels"><span></span><span>50°</span><span>75°</span><span>100°</span></div></div>
<!-- CPU -->
<div class="card accent-cyan"><h2>🔥 CPU</h2>
<div class="temp-group"><div class="temp-big"><span class="temp-cold" id="cpu-temp">--</span><span class="unit">°C</span></div>
<div class="temp-mini"><div class="temp-mini-item"><span>PCH</span><span class="val" id="pch-temp">-</span></div><div class="temp-mini-item"><span>ACPI</span><span class="val" id="acpi-temp">-</span></div></div></div>
<div class="bar-track"><div class="bar-fill temp" id="cpu-bar" style="width:0%"></div></div>
<div class="bar-labels"><span></span><span>50°</span><span>75°</span><span>100°</span></div></div>
<!-- Memory -->
<div class="card accent-orange"><h2>💾 Memory</h2>
<div class="mem-row"><span class="mem-used" id="mu">-</span><span class="mem-of">of <span id="mt">-</span></span></div>
<div class="bar-track" style="height:8px"><div class="bar-fill green" id="mbar" style="width:0%"></div></div>
<div class="mem-pct" id="mpct">-</div></div>
<!-- Details -->
<div class="card accent-purple"><h2>🔧 Details</h2>
<div class="stat"><span class="stat-label">Driver</span><span class="stat-value" id="dv">-</span></div>
<div class="stat"><span class="stat-label">CUDA Version</span><span class="stat-value" id="cv">-</span></div>
<div class="stat"><span class="stat-label">Perf State</span><span class="stat-value" id="gp">-</span></div>
<div class="stat"><span class="stat-label">Fan Speed</span><span class="stat-value" id="gf2">-</span></div></div>
<!-- Power -->
<div class="card accent-red"><h2>⚡ Power</h2>
<div class="power-block"><div class="power-num" id="pw">N/A</div><div class="power-label">Power Draw</div></div></div>
<!-- GPU Load -->
<div class="card accent-green"><h2>📊 GPU Load</h2>
<div class="stat"><span class="stat-label">Utilization</span><span class="stat-value" id="gu2">0%</span></div></div>
<!-- Model -->
<div class="card model-card"><div class="name" id="gpmodel2">-</div><div class="sub" id="driverver">-</div></div>
<!-- Processes -->
<div class="card full"><h2>🔄 Active Processes</h2>
<table><thead><tr><th>PID</th><th>Process</th><th>Memory</th></tr></thead>
<tbody id="pl"><tr><td colspan="3" style="text-align:center;color:#94a3b8;padding:1rem 0">No GPU processes running</td></tr></tbody></table></div>
<!-- Footer -->
<div class="full footer-row"><span>🕒 <span id="lu">-</span></span><span id="modelright" style="opacity:0.55">-</span></div>
</div></div></div>
<script>
(function(){
function $(i){return document.getElementById(i);}
function fmt(m){var v=parseInt(m);if(isNaN(v))return m+' MiB';return v>=1024?(v/1024).toFixed(1)+' GiB':v+' MiB';}
var PKG="/usr/share/cockpit/nvidia-gpu";
function update(){
var ts=new Date().toLocaleTimeString();
cockpit.spawn(["cat",PKG+"/gpu.txt"],{err:"message"}).done(function(data){
if(!data||!data.trim()){$('nogpu').style.display='flex';$('content').style.display='none';$('sdot').className='status-dot err';return;}
$('nogpu').style.display='none';$('content').style.display='block';var p=data.trim().split(', ');if(p.length<9)return;
var model=p[0];$('gpumodel').textContent=model;$('gpmodel2').textContent=model;$('sdot').className='status-dot ok';
$('dv').textContent=p[1];$('driverver').textContent='Driver '+p[1];$('modelright').textContent=model+' — Driver '+p[1];
var t=parseInt(p[2]);var tdisp=$('gt');
if(!isNaN(t)){tdisp.textContent=t;var tcls='temp-cold';if(t>=50)tcls='temp-mild';if(t>=65)tcls='temp-warm';if(t>=80)tcls='temp-hot';
tdisp.className=tcls;var tp=Math.min(100,Math.max(0,t));$('tbar').style.width=tp+'%';
$('sdot').className='status-dot '+(t<50?'ok':(t<75?'warn':'err'));}
var uPct=parseInt(p[3])||0;$('gu').textContent=uPct+'%';$('gu2').textContent=uPct+'%';
var uMem=parseInt(p[4])||0;var tMem=parseInt(p[5])||1;$('mu').textContent=fmt(uMem);$('mt').textContent=fmt(tMem);
var mp=Math.round(uMem/tMem*100);$('mbar').style.width=mp+'%';$('mbar').className='bar-fill '+(mp<60?'green':(mp<85?'amber':'red'));$('mpct').textContent=mp+'%';
$('pw').textContent=p[6]||'N/A';$('gp').textContent=p[7]||'-';$('pstate').textContent=(p[7]||'').trim();
var fan=p[8]?p[8].trim():'0';var fanDisp=(fan==='[N/A]'||fan==='0')?'—':fan+'%';$('gf').textContent=fanDisp;$('gf2').textContent=fanDisp;
}).fail(function(){$('nogpu').style.display='flex';$('content').style.display='none';$('sdot').className='status-dot err';});
cockpit.spawn(["cat",PKG+"/cpu.txt"],{err:"message"}).done(function(d){
if(d&&d.trim()){var s=d.trim().split(',');if(s.length>=3){var cp=parseInt(s[0]);$('cpu-temp').textContent=isNaN(cp)?'--':cp;
$('pch-temp').textContent=s[1]+'°';$('acpi-temp').textContent=s[2]+'°';
var tp=Math.min(100,Math.max(0,cp));$('cpu-bar').style.width=tp+'%';
var tc='temp-cold';if(cp>=50)tc='temp-mild';if(cp>=65)tc='temp-warm';if(cp>=80)tc='temp-hot';$('cpu-temp').className=tc;}}});
cockpit.spawn(["cat",PKG+"/cuda.txt"],{err:"message"}).done(function(d){if(d&&d.trim())$('cv').textContent=d.trim();});
cockpit.spawn(["cat",PKG+"/processes.txt"],{err:"message"}).done(function(d){
if(!d||!d.trim()){$('pl').innerHTML='<tr><td colspan="3" style="text-align:center;color:#94a3b8;padding:1rem 0">No GPU processes running</td></tr>';return;}
var ls=d.trim().split('\\n').filter(function(l){return l.trim();});
if(!ls.length){$('pl').innerHTML='<tr><td colspan="3" style="text-align:center;color:#94a3b8;padding:1rem 0">No GPU processes running</td></tr>';return;}
var h='';ls.forEach(function(l){var s=l.split(', ');if(s.length>=3)h+='<tr><td>'+s[0]+'</td><td>'+s[1]+'</td><td>'+fmt(s[2])+'</td></tr>';});$('pl').innerHTML=h;});
$('lu').textContent=ts;
}
update();setInterval(update,5000);
})();
</script></body></html>
@@ -0,0 +1,24 @@
#!/bin/bash
# nvidia-gpu-monitor.sh — writes GPU stats to files every 5 seconds
# Install: sudo cp to /usr/local/bin/, chmod +x
# Service: systemd unit at /etc/systemd/system/nvidia-gpu-monitor.service
#
# Output files:
# /usr/share/cockpit/nvidia-gpu/gpu.txt — model,driver,temp,util,mem,power,pstate,fan
# /usr/share/cockpit/nvidia-gpu/cuda.txt — CUDA version
# /usr/share/cockpit/nvidia-gpu/processes.txt — pid,process_name,used_memory
PKG="/usr/share/cockpit/nvidia-gpu"
while true; do
# GPU info (name, driver, temp, util, mem used, mem total, power, perf state, fan)
nvidia-smi --query-gpu=name,driver_version,temperature.gpu,utilization.gpu,memory.used,memory.total,power.draw,pstate,fan.speed --format=csv,noheader,nounits 2>/dev/null | head -1 > "$PKG/gpu.txt"
# GPU processes
nvidia-smi --query-compute-apps=pid,process_name,used_memory --format=csv,noheader,nounits 2>/dev/null > "$PKG/processes.txt"
# CUDA version
nvidia-smi 2>/dev/null | grep "CUDA Version" | sed 's/.*CUDA Version: //' | head -1 > "$PKG/cuda.txt"
sleep 5
done
@@ -0,0 +1,119 @@
<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8"><title>NVIDIA GPU</title>
<link rel="stylesheet" href="../base1/cockpit.css">
<script src="../base1/cockpit.js"></script>
<style>
body{padding:0;font-family:var(--pf-t--global--font--family--body,Inter,RedHatText,Overpass,sans-serif);background:var(--pf-t--global--background--color--primary--default,#f0f2f5)}
.ct-page-fill{padding:1.5rem;max-width:1100px;margin:0 auto}
.page-header{display:flex;align-items:center;gap:1rem;margin-bottom:1.5rem;padding:1rem 1.25rem;background:var(--pf-t--global--background--color--secondary--default,linear-gradient(135deg,#0f172a,#1e293b));border-radius:14px;border:1px solid var(--pf-t--global--border--color--default,rgba(255,255,255,0.08));box-shadow:0 2px 12px rgba(0,0,0,0.06)}
.page-header h1{font-size:1.15rem;font-weight:700;margin:0;background:linear-gradient(135deg,#38bdf8,#a78bfa);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
.page-header .subtitle{font-size:0.7rem;color:var(--pf-t--global--text--color--subtle,#94a3b8);margin-left:auto;font-family:"SF Mono","JetBrains Mono","Fira Code",monospace;background:var(--pf-t--global--background--color--secondary--default,rgba(255,255,255,0.06));padding:0.2rem 0.6rem;border-radius:6px;border:1px solid var(--pf-t--global--border--color--default,rgba(255,255,255,0.05))}
.status-dot{width:10px;height:10px;border-radius:50%;display:inline-block;flex-shrink:0;position:relative}
.status-dot::after{content:'';position:absolute;inset:-4px;border-radius:50%;opacity:0.25}
.status-dot.ok{background:#22c55e;box-shadow:0 0 10px #22c55e;animation:pulse-dot 2s ease-in-out infinite}
.status-dot.warn{background:#f59e0b;box-shadow:0 0 10px #f59e0b;animation:pulse-dot 2s ease-in-out infinite}
.status-dot.err{background:#ef4444;box-shadow:0 0 10px #ef4444;animation:pulse-dot 2s ease-in-out infinite}
@keyframes pulse-dot{0%,100%{opacity:0.25}50%{opacity:0.6}}
.grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:1rem}
@media(max-width:900px){.grid{grid-template-columns:1fr 1fr}}
@media(max-width:600px){.grid{grid-template-columns:1fr}}
.card{background:var(--pf-t--global--background--color--primary--default,#fff);border:1px solid var(--pf-t--global--border--color--default,#e2e8f0);border-radius:12px;padding:1.25rem;transition:box-shadow 0.2s ease,transform 0.15s ease}
.card:hover{box-shadow:0 4px 20px rgba(0,0,0,0.06);transform:translateY(-2px)}
.card.full{grid-column:1/-1}
.card.accent-blue{border-top:3px solid #3b82f6}
.card.accent-green{border-top:3px solid #22c55e}
.card.accent-orange{border-top:3px solid #f59e0b}
.card.accent-purple{border-top:3px solid #a855f7}
.card.accent-red{border-top:3px solid #ef4444}
.card h2{font-size:0.65rem;text-transform:uppercase;letter-spacing:0.06em;color:var(--pf-t--global--text--color--subtle,#94a3b8);margin:0 0 0.85rem 0;font-weight:700}
.stat{display:flex;justify-content:space-between;align-items:center;padding:0.45rem 0}
.stat+.stat{border-top:1px solid var(--pf-t--global--border--color--default,rgba(0,0,0,0.04))}
.stat-label{color:var(--pf-t--global--text--color--subtle,#64748b);font-size:0.82rem;font-weight:500}
.stat-value{font-weight:600;font-family:"SF Mono","JetBrains Mono","Fira Code",monospace;font-size:0.85rem;color:var(--pf-t--global--text--color--default,#1e293b);white-space:nowrap}
table{width:100%;border-collapse:collapse;font-size:0.82rem}
th{text-align:left;padding:0.45rem 0.5rem;color:var(--pf-t--global--text--color--subtle,#94a3b8);font-weight:600;font-size:0.65rem;text-transform:uppercase;letter-spacing:0.04em;border-bottom:2px solid var(--pf-t--global--border--color--default,#e2e8f0)}
td{padding:0.45rem 0.5rem;border-bottom:1px solid var(--pf-t--global--border--color--default,rgba(0,0,0,0.03))}
td:last-child{text-align:right;font-family:"SF Mono","JetBrains Mono","Fira Code",monospace}
tr:last-child td{border-bottom:none}
tr:hover td{background:var(--pf-t--global--background--color--secondary--default,rgba(0,0,0,0.015))}
.footer-row{display:flex;gap:1rem;flex-wrap:wrap;margin-top:1rem;padding:0.5rem 1rem;background:var(--pf-t--global--background--color--secondary--default,rgba(0,0,0,0.015));border:1px solid var(--pf-t--global--border--color--default,rgba(0,0,0,0.04));border-radius:8px;font-size:0.72rem;color:var(--pf-t--global--text--color--subtle,#94a3b8)}
.alert-box{display:none;background:linear-gradient(135deg,#fef2f2,#fff5f5);border:1px solid #fecaca;color:#991b1b;padding:0.9rem 1.25rem;border-radius:10px;margin-bottom:1rem;font-size:0.85rem;align-items:center;gap:0.6rem}
@keyframes fadeUp{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
.grid>*{animation:fadeUp 0.3s ease-out both}
</style></head>
<body>
<div class="ct-page-fill">
<div id="nogpu" class="alert-box"><span style="font-size:1.2rem"></span> No NVIDIA GPU detected</div>
<div class="page-header">
<span class="status-dot" id="sdot"></span>
<h1>🎮 <span id="gpumodel">NVIDIA GPU</span></h1>
<span class="subtitle" id="pstate">P?</span>
</div>
<div id="content"><div class="grid">
<div class="card accent-blue"><h2>🌡 Thermal</h2>
<div class="stat"><span class="stat-label">Temperature</span><span class="stat-value" id="gt">--°C</span></div>
<div class="stat"><span class="stat-label">Fan Speed</span><span class="stat-value" id="gf"></span></div>
<div class="stat"><span class="stat-label">Utilization</span><span class="stat-value" id="gu">-</span></div>
</div>
<div class="card accent-green"><h2>📊 GPU Load</h2>
<div class="stat"><span class="stat-label">Utilization</span><span class="stat-value" id="gu2">-</span></div>
</div>
<div class="card accent-orange"><h2>💾 Memory</h2>
<div class="stat"><span class="stat-label">Used</span><span class="stat-value" id="mu">-</span></div>
<div class="stat"><span class="stat-label">Total</span><span class="stat-value" id="mt">-</span></div>
<div id="mpct" style="text-align:right;font-size:0.78rem;color:var(--pf-t--global--text--color--subtle,#94a3b8);font-family:monospace;margin-top:2px"></div>
</div>
<div class="card accent-purple"><h2>🔧 Details</h2>
<div class="stat"><span class="stat-label">Driver</span><span class="stat-value" id="dv">-</span></div>
<div class="stat"><span class="stat-label">CUDA Version</span><span class="stat-value" id="cv">-</span></div>
<div class="stat"><span class="stat-label">Perf State</span><span class="stat-value" id="gp">-</span></div>
<div class="stat"><span class="stat-label">Fan Speed</span><span class="stat-value" id="gf2">-</span></div>
</div>
<div class="card accent-red"><h2>⚡ Power</h2>
<div class="stat"><span class="stat-label">Draw</span><span class="stat-value" id="pw">N/A</span></div>
</div>
<div class="card model-card" style="display:flex;flex-direction:column;align-items:center;justify-content:center;gap:0.4rem;text-align:center">
<div style="font-size:1.8rem">🎮</div>
<div style="font-weight:700;font-size:0.9rem" id="gpmodel2">-</div>
<div style="font-size:0.72rem;color:var(--pf-t--global--text--color--subtle,#94a3b8)" id="driverver">-</div>
</div>
<div class="card full"><h2>🔄 Active Processes</h2>
<table><thead><tr><th>PID</th><th>Process</th><th>Memory</th></tr></thead>
<tbody id="pl"><tr><td colspan="3" style="text-align:center;color:var(--pf-t--global--text--color--subtle,#94a3b8);padding:1rem 0">No GPU processes running</td></tr></tbody></table>
</div>
<div class="full footer-row"><span>🕒 <span id="lu">-</span></span><span id="modelright" style="opacity:0.55">-</span></div>
</div></div>
</div>
<script>
(function(){
function $(i){return document.getElementById(i);}
function fmt(m){var v=parseInt(m);if(isNaN(v))return m+' MiB';return v>=1024?(v/1024).toFixed(1)+' GiB':v+' MiB';}
var PKG="/usr/share/cockpit/nvidia-gpu";
function update(){
var ts=new Date().toLocaleTimeString();
cockpit.spawn(["cat",PKG+"/gpu.txt"],{err:"message"}).done(function(data){
if(!data||!data.trim()){$('nogpu').style.display='flex';$('content').style.display='none';$('sdot').className='status-dot err';return;}
$('nogpu').style.display='none';$('content').style.display='block';var p=data.trim().split(', ');if(p.length<9)return;
var model=p[0];$('gpumodel').textContent=model;$('gpmodel2').textContent=model;$('sdot').className='status-dot ok';
var drv=p[1];$('dv').textContent=drv;$('driverver').textContent='Driver '+drv;$('modelright').textContent=model+' — Driver '+drv;
var t=parseInt(p[2]);if(!isNaN(t)){$('gt').textContent=t+'°C';$('sdot').className='status-dot '+(t<50?'ok':(t<75?'warn':'err'));}
var uPct=parseInt(p[3])||0;$('gu').textContent=uPct+'%';$('gu2').textContent=uPct+'%';
var uMem=parseInt(p[4])||0;var tMem=parseInt(p[5])||1;
$('mu').textContent=fmt(uMem);$('mt').textContent=fmt(tMem);var mp=Math.round(uMem/tMem*100);$('mpct').textContent=mp+'%';
$('pw').textContent=p[6]||'N/A';$('gp').textContent=p[7]||'-';$('pstate').textContent=(p[7]||'').trim();
var fan=p[8]?p[8].trim():'0';var d=(fan==='[N/A]'||fan==='0')?'—':fan+'%';$('gf').textContent=d;$('gf2').textContent=d;
}).fail(function(){$('nogpu').style.display='flex';$('content').style.display='none';$('sdot').className='status-dot err';});
cockpit.spawn(["cat",PKG+"/cuda.txt"],{err:"message"}).done(function(d){if(d&&d.trim())$('cv').textContent=d.trim();});
cockpit.spawn(["cat",PKG+"/processes.txt"],{err:"message"}).done(function(d){
if(!d||!d.trim()){$('pl').innerHTML='<tr><td colspan="3" style="text-align:center;color:var(--pf-t--global--text--color--subtle,#94a3b8);padding:1rem 0">No GPU processes running</td></tr>';return;}
var ls=d.trim().split('\n').filter(function(l){return l.trim();});if(!ls.length){$('pl').innerHTML='<tr><td colspan="3" style="text-align:center;color:var(--pf-t--global--text--color--subtle,#94a3b8);padding:1rem 0">No GPU processes running</td></tr>';return;}
var h='';ls.forEach(function(l){var s=l.split(', ');if(s.length>=3)h+='<tr><td>'+s[0]+'</td><td>'+s[1]+'</td><td>'+fmt(s[2])+'</td></tr>';});$('pl').innerHTML=h;
});
$('lu').textContent=ts;
}
update();setInterval(update,5000);
})();
</script>
</body>
</html>
@@ -0,0 +1,12 @@
[Unit]
Description=NVIDIA GPU Monitor — writes stats to files
After=nvidia-persistenced.service
[Service]
Type=simple
ExecStart=/usr/local/bin/nvidia-gpu-monitor.sh
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target