|
// ==UserScript== |
|
// @name Pi-hole Temperature Display |
|
// @namespace http://tampermonkey.net/ |
|
// @version 1.0 |
|
// @description Display Raspberry Pi temperature in Pi-hole dashboard |
|
// @author You |
|
// @match http://192.168.0.13/admin* |
|
// @match https://192.168.0.13/admin* |
|
// @match http://pihole/admin* |
|
// @match https://pihole/admin* |
|
// @grant GM_xmlhttpRequest |
|
// @connect 192.168.0.13 |
|
// ==/UserScript== |
|
|
|
(function() { |
|
'use strict'; |
|
|
|
const STATS_API_URL = 'http://192.168.0.13:8080/stats'; |
|
const UPDATE_INTERVAL = 5000; |
|
|
|
let previousNetworkData = null; |
|
let lastUpdateTime = null; |
|
|
|
function createWidget(id, title, iconPath) { |
|
const widget = document.createElement('div'); |
|
widget.id = id; |
|
widget.style.cssText = ` |
|
position: fixed; |
|
background: #667eea; |
|
color: white; |
|
padding: 12px 16px; |
|
border-radius: 8px; |
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); |
|
font-family: Poppins, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; |
|
z-index: 9999; |
|
width: 200px; |
|
height: 90px; |
|
transition: background 0.3s ease; |
|
overflow: hidden; |
|
`; |
|
|
|
widget.innerHTML = ` |
|
<svg style="position: absolute; right: -5px; top: 50%; transform: translateY(-50%); opacity: 0.15; width: 80px; height: 80px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
|
<path d="${iconPath}"></path> |
|
</svg> |
|
<div style="font-size: 12px; font-weight: 600; margin-bottom: 6px; position: relative; z-index: 1;"> |
|
${title} |
|
</div> |
|
<div class="stat-value" style="font-size: 32px; font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1; position: relative; z-index: 1;"> |
|
-- |
|
</div> |
|
<div class="stat-subtitle" style="font-size: 11px; margin-top: 4px; opacity: 0.9; position: relative; z-index: 1;"> |
|
</div> |
|
`; |
|
|
|
document.body.appendChild(widget); |
|
return widget; |
|
} |
|
|
|
function positionWidgets() { |
|
const widgets = [ |
|
{id: 'cpu-temp-widget', top: 70, right: 20}, |
|
{id: 'memory-widget', top: 175, right: 20}, |
|
{id: 'disk-widget', top: 280, right: 20}, |
|
{id: 'network-widget', top: 385, right: 20}, |
|
{id: 'uptime-widget', top: 490, right: 20}, |
|
{id: 'tailscale-widget', top: 595, right: 20}, |
|
{id: 'unbound-widget', top: 700, right: 20} |
|
]; |
|
|
|
widgets.forEach(w => { |
|
const el = document.getElementById(w.id); |
|
if (el) { |
|
el.style.top = `${w.top}px`; |
|
el.style.right = `${w.right}px`; |
|
} |
|
}); |
|
} |
|
|
|
function formatUptime(seconds) { |
|
const days = Math.floor(seconds / 86400); |
|
const hours = Math.floor((seconds % 86400) / 3600); |
|
const minutes = Math.floor((seconds % 3600) / 60); |
|
|
|
if (days > 0) return `${days}d ${hours}h`; |
|
if (hours > 0) return `${hours}h ${minutes}m`; |
|
return `${minutes}m`; |
|
} |
|
|
|
function formatBytes(bytes) { |
|
if (bytes < 1024) return `${bytes} B/s`; |
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB/s`; |
|
return `${(bytes / (1024 * 1024)).toFixed(2)} MB/s`; |
|
} |
|
|
|
function updateStats() { |
|
GM_xmlhttpRequest({ |
|
method: 'GET', |
|
url: STATS_API_URL, |
|
onload: function(response) { |
|
try { |
|
const data = JSON.parse(response.responseText); |
|
const currentTime = Date.now(); |
|
|
|
// Temperature |
|
const tempValue = parseFloat(data.temperature); |
|
const tempWidget = document.getElementById('cpu-temp-widget'); |
|
if (tempWidget) { |
|
tempWidget.querySelector('.stat-value').textContent = `${tempValue.toFixed(1)}°C`; |
|
|
|
if (tempValue < 60) { |
|
tempWidget.style.background = '#667eea'; |
|
} else if (tempValue < 70) { |
|
tempWidget.style.background = '#3b82f6'; |
|
} else if (tempValue < 80) { |
|
tempWidget.style.background = '#f59e0b'; |
|
} else { |
|
tempWidget.style.background = '#ef4444'; |
|
} |
|
} |
|
|
|
// Memory |
|
const memWidget = document.getElementById('memory-widget'); |
|
if (memWidget) { |
|
memWidget.querySelector('.stat-value').textContent = `${data.memory.percent}%`; |
|
memWidget.querySelector('.stat-subtitle').textContent = |
|
`${data.memory.used_mb.toFixed(0)} / ${data.memory.total_mb.toFixed(0)} MB`; |
|
|
|
if (data.memory.percent < 60) { |
|
memWidget.style.background = '#10b981'; |
|
} else if (data.memory.percent < 80) { |
|
memWidget.style.background = '#f59e0b'; |
|
} else { |
|
memWidget.style.background = '#ef4444'; |
|
} |
|
} |
|
|
|
// Disk |
|
const diskWidget = document.getElementById('disk-widget'); |
|
if (diskWidget) { |
|
diskWidget.querySelector('.stat-value').textContent = `${data.disk.percent}%`; |
|
diskWidget.querySelector('.stat-subtitle').textContent = |
|
`${data.disk.used_gb.toFixed(1)} / ${data.disk.total_gb.toFixed(1)} GB`; |
|
|
|
if (data.disk.percent < 60) { |
|
diskWidget.style.background = '#8b5cf6'; |
|
} else if (data.disk.percent < 80) { |
|
diskWidget.style.background = '#f59e0b'; |
|
} else { |
|
diskWidget.style.background = '#ef4444'; |
|
} |
|
} |
|
|
|
// Network - calculate speed |
|
const netWidget = document.getElementById('network-widget'); |
|
if (netWidget && previousNetworkData && lastUpdateTime) { |
|
const timeDiff = (currentTime - lastUpdateTime) / 1000; |
|
const sentDiff = data.network.bytes_sent - previousNetworkData.bytes_sent; |
|
const recvDiff = data.network.bytes_recv - previousNetworkData.bytes_recv; |
|
|
|
const uploadSpeed = sentDiff / timeDiff; |
|
const downloadSpeed = recvDiff / timeDiff; |
|
|
|
netWidget.querySelector('.stat-value').style.fontSize = '20px'; |
|
netWidget.querySelector('.stat-value').innerHTML = |
|
`<span style="font-size: 14px;">↓</span>${formatBytes(downloadSpeed)}`; |
|
netWidget.querySelector('.stat-subtitle').textContent = |
|
`↑ ${formatBytes(uploadSpeed)}`; |
|
|
|
netWidget.style.background = '#06b6d4'; |
|
} |
|
|
|
previousNetworkData = data.network; |
|
lastUpdateTime = currentTime; |
|
|
|
// Uptime |
|
const uptimeWidget = document.getElementById('uptime-widget'); |
|
if (uptimeWidget) { |
|
uptimeWidget.querySelector('.stat-value').textContent = formatUptime(data.uptime_seconds); |
|
uptimeWidget.style.background = '#ec4899'; |
|
} |
|
|
|
// Tailscale |
|
const tailscaleWidget = document.getElementById('tailscale-widget'); |
|
if (tailscaleWidget) { |
|
tailscaleWidget.querySelector('.stat-value').style.fontSize = '24px'; |
|
tailscaleWidget.querySelector('.stat-value').textContent = data.tailscale.status; |
|
tailscaleWidget.querySelector('.stat-subtitle').textContent = data.tailscale.ip; |
|
|
|
if (data.tailscale.status === 'Connected') { |
|
tailscaleWidget.style.background = '#10b981'; |
|
} else if (data.tailscale.status === 'Disconnected') { |
|
tailscaleWidget.style.background = '#f59e0b'; |
|
} else { |
|
tailscaleWidget.style.background = '#6b7280'; |
|
} |
|
} |
|
|
|
// Unbound |
|
const unboundWidget = document.getElementById('unbound-widget'); |
|
if (unboundWidget) { |
|
unboundWidget.querySelector('.stat-value').style.fontSize = '28px'; |
|
unboundWidget.querySelector('.stat-value').textContent = data.unbound.status; |
|
|
|
if (data.unbound.status === 'Running') { |
|
unboundWidget.style.background = '#10b981'; |
|
} else if (data.unbound.status === 'Stopped') { |
|
unboundWidget.style.background = '#ef4444'; |
|
} else { |
|
unboundWidget.style.background = '#6b7280'; |
|
} |
|
} |
|
|
|
} catch (e) { |
|
console.error('Failed to parse stats:', e); |
|
} |
|
}, |
|
onerror: function(error) { |
|
console.error('Failed to fetch stats:', error); |
|
} |
|
}); |
|
} |
|
|
|
function init() { |
|
if (document.body) { |
|
// Thermometer icon |
|
createWidget('cpu-temp-widget', 'CPU Temperature', |
|
'M14 14.76V3.5a2.5 2.5 0 0 0-5 0v11.26a4.5 4.5 0 1 0 5 0z'); |
|
|
|
// Memory chip icon |
|
createWidget('memory-widget', 'Memory Usage', |
|
'M6 2h12M6 22h12M2 6v12M22 6v12M6 6h12v12H6z'); |
|
|
|
// Hard drive icon |
|
createWidget('disk-widget', 'Disk Space', |
|
'M22 12H2M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11zM6 16h.01M10 16h.01'); |
|
|
|
// Network icon |
|
createWidget('network-widget', 'Network Traffic', |
|
'M16 3h5v5M4 20L21 3M21 16v5h-5M15 15l6 6M4 4l5 5'); |
|
|
|
// Clock icon |
|
createWidget('uptime-widget', 'Uptime', |
|
'M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83'); |
|
|
|
// Tailscale logo-like icon (network connections) |
|
createWidget('tailscale-widget', 'Tailscale', |
|
'M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5'); |
|
|
|
// DNS/Unbound icon (server/shield) |
|
createWidget('unbound-widget', 'Unbound DNS', |
|
'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z'); |
|
|
|
positionWidgets(); |
|
updateStats(); |
|
setInterval(updateStats, UPDATE_INTERVAL); |
|
} else { |
|
setTimeout(init, 100); |
|
} |
|
} |
|
|
|
init(); |
|
})(); |