initial commit
This commit is contained in:
@@ -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>0°</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>0°</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>0°</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
|
||||
Reference in New Issue
Block a user