Bump to v1.1.0, enable network and docker detectors

- Version bump reflecting new features (sounds, tap reaction, docker detector)
- Rename title to Kao
- Enable network and docker detectors by default

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-03 20:57:18 -06:00
parent da6613ada3
commit 66c9790d2b
2 changed files with 352 additions and 325 deletions

View File

@@ -45,7 +45,7 @@
}, },
{ {
"name": "network", "name": "network",
"enabled": false, "enabled": true,
"script": "detectors/network.py", "script": "detectors/network.py",
"env": { "env": {
"CHECK_INTERVAL": "60", "CHECK_INTERVAL": "60",
@@ -55,7 +55,7 @@
}, },
{ {
"name": "docker", "name": "docker",
"enabled": false, "enabled": true,
"script": "detectors/docker.py", "script": "detectors/docker.py",
"env": { "env": {
"CHECK_INTERVAL": "60", "CHECK_INTERVAL": "60",

View File

@@ -1,371 +1,398 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> <meta
<title>Sentry-Emote</title> name="viewport"
<style> content="width=device-width, initial-scale=1.0, user-scalable=no"
* { />
margin: 0; <title>Kao</title>
padding: 0; <style>
box-sizing: border-box; * {
} margin: 0;
padding: 0;
box-sizing: border-box;
}
body { body {
background: #000000; background: #000000;
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
font-family: monospace; font-family: monospace;
overflow: hidden; overflow: hidden;
} }
#emote { #emote {
font-size: 18vw; font-size: 18vw;
font-weight: bold; font-weight: bold;
text-align: center; text-align: center;
transition: color 0.3s ease; transition: color 0.3s ease;
} }
#message { #message {
font-size: 4vw; font-size: 4vw;
margin-top: 2vh; margin-top: 2vh;
opacity: 0.7; opacity: 0.7;
text-align: center; text-align: center;
} }
/* Breathing animation - slow pulse */ /* Breathing animation - slow pulse */
.breathing { .breathing {
animation: breathe 3s ease-in-out infinite; animation: breathe 3s ease-in-out infinite;
} }
@keyframes breathe { @keyframes breathe {
0%, 100% { 0%,
opacity: 1; 100% {
transform: scale(1); opacity: 1;
} transform: scale(1);
50% { }
opacity: 0.7; 50% {
transform: scale(0.98); opacity: 0.7;
} transform: scale(0.98);
} }
}
/* Shaking animation - rapid jitter for Critical */ /* Shaking animation - rapid jitter for Critical */
.shaking { .shaking {
animation: shake 0.15s linear infinite; animation: shake 0.15s linear infinite;
} }
@keyframes shake { @keyframes shake {
0%, 100% { transform: translateX(0); } 0%,
25% { transform: translateX(-5px); } 100% {
75% { transform: translateX(5px); } transform: translateX(0);
} }
25% {
transform: translateX(-5px);
}
75% {
transform: translateX(5px);
}
}
/* Popping animation - scale up for Notifications */ /* Popping animation - scale up for Notifications */
.popping { .popping {
animation: pop 1s ease-in-out infinite; animation: pop 1s ease-in-out infinite;
} }
@keyframes pop { @keyframes pop {
0%, 100% { 0%,
transform: scale(1); 100% {
} transform: scale(1);
50% { }
transform: scale(1.08); 50% {
} transform: scale(1.08);
} }
}
/* Celebrating animation - bounce and wiggle */ /* Celebrating animation - bounce and wiggle */
.celebrating { .celebrating {
animation: celebrate 0.5s ease-in-out infinite; animation: celebrate 0.5s ease-in-out infinite;
} }
@keyframes celebrate { @keyframes celebrate {
0%, 100% { 0%,
transform: translateY(0) rotate(0deg); 100% {
} transform: translateY(0) rotate(0deg);
25% { }
transform: translateY(-10px) rotate(-5deg); 25% {
} transform: translateY(-10px) rotate(-5deg);
75% { }
transform: translateY(-10px) rotate(5deg); 75% {
} transform: translateY(-10px) rotate(5deg);
} }
}
/* Floating animation - gentle drift */ /* Floating animation - gentle drift */
.floating { .floating {
animation: float 4s ease-in-out infinite; animation: float 4s ease-in-out infinite;
} }
@keyframes float { @keyframes float {
0%, 100% { 0%,
transform: translateY(0); 100% {
} transform: translateY(0);
50% { }
transform: translateY(-8px); 50% {
} transform: translateY(-8px);
} }
}
/* Bouncing animation - playful hop */ /* Bouncing animation - playful hop */
.bouncing { .bouncing {
animation: bounce 1s ease-in-out infinite; animation: bounce 1s ease-in-out infinite;
} }
@keyframes bounce { @keyframes bounce {
0%, 100% { 0%,
transform: translateY(0); 100% {
} transform: translateY(0);
50% { }
transform: translateY(-12px); 50% {
} transform: translateY(-12px);
} }
}
/* Swaying animation - curious side-to-side */ /* Swaying animation - curious side-to-side */
.swaying { .swaying {
animation: sway 3s ease-in-out infinite; animation: sway 3s ease-in-out infinite;
} }
@keyframes sway { @keyframes sway {
0%, 100% { 0%,
transform: rotate(0deg); 100% {
} transform: rotate(0deg);
25% { }
transform: rotate(-3deg); 25% {
} transform: rotate(-3deg);
75% { }
transform: rotate(3deg); 75% {
} transform: rotate(3deg);
} }
}
/* Blink animation - quick fade for winks */ /* Blink animation - quick fade for winks */
.blink { .blink {
animation: blink 0.3s ease-in-out 1; animation: blink 0.3s ease-in-out 1;
} }
@keyframes blink { @keyframes blink {
0%, 100% { 0%,
opacity: 1; 100% {
} opacity: 1;
50% { }
opacity: 0.3; 50% {
} opacity: 0.3;
} }
}
/* Searching animation - looking around for connection */ /* Searching animation - looking around for connection */
.searching { .searching {
animation: search 2s ease-in-out infinite; animation: search 2s ease-in-out infinite;
} }
@keyframes search { @keyframes search {
0%, 100% { 0%,
transform: translateX(0); 100% {
opacity: 0.6; transform: translateX(0);
} opacity: 0.6;
25% { }
transform: translateX(-10px); 25% {
opacity: 0.8; transform: translateX(-10px);
} opacity: 0.8;
75% { }
transform: translateX(10px); 75% {
opacity: 0.8; transform: translateX(10px);
} opacity: 0.8;
} }
}
/* Sleeping animation - very slow, subtle breathing */ /* Sleeping animation - very slow, subtle breathing */
.sleeping { .sleeping {
animation: sleep 6s ease-in-out infinite; animation: sleep 6s ease-in-out infinite;
} }
@keyframes sleep { @keyframes sleep {
0%, 100% { 0%,
opacity: 0.4; 100% {
transform: scale(1); opacity: 0.4;
} transform: scale(1);
50% { }
opacity: 0.2; 50% {
transform: scale(0.98); opacity: 0.2;
} transform: scale(0.98);
} }
</style> }
</head> </style>
<body> </head>
<div id="emote" class="breathing">( ^_^)</div> <body>
<div id="message">Loading...</div> <div id="emote" class="breathing">( ^_^)</div>
<div id="message">Loading...</div>
<script> <script>
const emoteEl = document.getElementById('emote'); const emoteEl = document.getElementById("emote");
const messageEl = document.getElementById('message'); const messageEl = document.getElementById("message");
const POLL_INTERVAL = 2000; const POLL_INTERVAL = 2000;
const VERSION = 'v1.0.0'; const VERSION = "v1.1.0";
// Sound system // Sound system
let audioCtx = null; let audioCtx = null;
let soundEnabled = new URLSearchParams(window.location.search).get('sound') === 'on'; let soundEnabled =
let lastState = null; new URLSearchParams(window.location.search).get("sound") === "on";
let lastData = null; let lastState = null;
let isReacting = false; let lastData = null;
let isReacting = false;
function initAudio() { function initAudio() {
if (!audioCtx) { if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)(); audioCtx = new (window.AudioContext || window.webkitAudioContext)();
} }
if (audioCtx.state === 'suspended') { if (audioCtx.state === "suspended") {
audioCtx.resume(); audioCtx.resume();
} }
} }
function playTone(frequency, duration, type = 'sine', volume = 0.1) { function playTone(frequency, duration, type = "sine", volume = 0.1) {
if (!soundEnabled || !audioCtx) return; if (!soundEnabled || !audioCtx) return;
const osc = audioCtx.createOscillator(); const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain(); const gain = audioCtx.createGain();
osc.type = type; osc.type = type;
osc.frequency.value = frequency; osc.frequency.value = frequency;
gain.gain.value = volume; gain.gain.value = volume;
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + duration); gain.gain.exponentialRampToValueAtTime(
osc.connect(gain); 0.001,
gain.connect(audioCtx.destination); audioCtx.currentTime + duration,
osc.start(); );
osc.stop(audioCtx.currentTime + duration); osc.connect(gain);
} gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + duration);
}
function playWarningSound() { function playWarningSound() {
// Soft double-beep // Soft double-beep
playTone(440, 0.15); playTone(440, 0.15);
setTimeout(() => playTone(440, 0.15), 180); setTimeout(() => playTone(440, 0.15), 180);
} }
function playCriticalSound() { function playCriticalSound() {
// Urgent descending tone // Urgent descending tone
playTone(600, 0.2); playTone(600, 0.2);
setTimeout(() => playTone(400, 0.3), 220); setTimeout(() => playTone(400, 0.3), 220);
} }
function playNotifySound() { function playNotifySound() {
// Gentle ping // Gentle ping
playTone(880, 0.1, 'sine', 0.08); playTone(880, 0.1, "sine", 0.08);
} }
function playRecoverySound() { function playRecoverySound() {
// Happy ascending chirp // Happy ascending chirp
playTone(523, 0.1); playTone(523, 0.1);
setTimeout(() => playTone(659, 0.1), 100); setTimeout(() => playTone(659, 0.1), 100);
setTimeout(() => playTone(784, 0.15), 200); setTimeout(() => playTone(784, 0.15), 200);
} }
function playReactSound() { function playReactSound() {
// Cute surprised chirp // Cute surprised chirp
playTone(600, 0.08, 'sine', 0.06); playTone(600, 0.08, "sine", 0.06);
} }
function handleStateChange(newState, newEmote) { function handleStateChange(newState, newEmote) {
if (!lastState) { if (!lastState) {
lastState = newState; lastState = newState;
return; return;
} }
// State transitions that trigger sounds // State transitions that trigger sounds
if (newState !== lastState) { if (newState !== lastState) {
if (newState === 'critical') { if (newState === "critical") {
playCriticalSound(); playCriticalSound();
} else if (newState === 'warning') { } else if (newState === "warning") {
playWarningSound(); playWarningSound();
} else if (newState === 'notify') { } else if (newState === "notify") {
playNotifySound(); playNotifySound();
} else if (newState === 'optimal' && lastState !== 'optimal' && lastState !== 'sleeping') { } else if (
// Recovery - also check for celebration emote newState === "optimal" &&
playRecoverySound(); lastState !== "optimal" &&
} lastState !== "sleeping"
lastState = newState; ) {
} // Recovery - also check for celebration emote
} playRecoverySound();
}
lastState = newState;
}
}
// Handle tap - enable sound and show reaction // Handle tap - enable sound and show reaction
document.body.addEventListener('click', () => { document.body.addEventListener("click", () => {
// Enable sound on first tap (browser autoplay policy) // Enable sound on first tap (browser autoplay policy)
if (!soundEnabled) { if (!soundEnabled) {
soundEnabled = true; soundEnabled = true;
initAudio(); initAudio();
} }
// Show surprised reaction and version // Show surprised reaction and version
if (!isReacting) { if (!isReacting) {
isReacting = true; isReacting = true;
const prevEmote = emoteEl.textContent; const prevEmote = emoteEl.textContent;
const prevColor = emoteEl.style.color; const prevColor = emoteEl.style.color;
const prevClass = emoteEl.className; const prevClass = emoteEl.className;
const prevMsg = messageEl.textContent; const prevMsg = messageEl.textContent;
// Surprised face! // Surprised face!
emoteEl.textContent = '( °o°)'; emoteEl.textContent = "( °o°)";
emoteEl.className = 'popping'; emoteEl.className = "popping";
messageEl.textContent = `Kao ${VERSION}`; messageEl.textContent = `Kao ${VERSION}`;
playReactSound(); playReactSound();
// Return to normal after 1.5s // Return to normal after 1.5s
setTimeout(() => { setTimeout(() => {
if (lastData) { if (lastData) {
updateDisplay(lastData); updateDisplay(lastData);
} else { } else {
emoteEl.textContent = prevEmote; emoteEl.textContent = prevEmote;
emoteEl.style.color = prevColor; emoteEl.style.color = prevColor;
emoteEl.className = prevClass; emoteEl.className = prevClass;
messageEl.textContent = prevMsg; messageEl.textContent = prevMsg;
} }
isReacting = false; isReacting = false;
}, 1500); }, 1500);
} }
}); });
// Also init if ?sound=on // Also init if ?sound=on
if (soundEnabled) { if (soundEnabled) {
document.addEventListener('DOMContentLoaded', initAudio); document.addEventListener("DOMContentLoaded", initAudio);
} }
async function fetchStatus() { async function fetchStatus() {
try { try {
const response = await fetch('/status'); const response = await fetch("/status");
if (!response.ok) throw new Error('Failed to fetch'); if (!response.ok) throw new Error("Failed to fetch");
const data = await response.json(); const data = await response.json();
updateDisplay(data); updateDisplay(data);
} catch (err) { } catch (err) {
// Connection lost state // Connection lost state
emoteEl.textContent = '( ?.?)'; emoteEl.textContent = "( ?.?)";
emoteEl.style.color = '#888888'; emoteEl.style.color = "#888888";
emoteEl.className = 'searching'; emoteEl.className = "searching";
messageEl.style.color = '#888888'; messageEl.style.color = "#888888";
messageEl.textContent = ''; messageEl.textContent = "";
} }
} }
function updateDisplay(data) { function updateDisplay(data) {
lastData = data; lastData = data;
// Don't update display while showing reaction // Don't update display while showing reaction
if (isReacting) return; if (isReacting) return;
// Check for state changes and play sounds // Check for state changes and play sounds
handleStateChange(data.current_state, data.active_emote); handleStateChange(data.current_state, data.active_emote);
emoteEl.textContent = data.active_emote; emoteEl.textContent = data.active_emote;
emoteEl.style.color = data.color; emoteEl.style.color = data.color;
messageEl.style.color = data.color; messageEl.style.color = data.color;
// Only show message when there's something to report // Only show message when there's something to report
const topEvent = data.active_events && data.active_events[0]; const topEvent = data.active_events && data.active_events[0];
messageEl.textContent = (topEvent && topEvent.message) || ''; messageEl.textContent = (topEvent && topEvent.message) || "";
// Update animation class // Update animation class
emoteEl.className = ''; emoteEl.className = "";
if (data.animation) { if (data.animation) {
emoteEl.classList.add(data.animation); emoteEl.classList.add(data.animation);
} }
} }
// Initial fetch and start polling // Initial fetch and start polling
fetchStatus(); fetchStatus();
setInterval(fetchStatus, POLL_INTERVAL); setInterval(fetchStatus, POLL_INTERVAL);
</script> </script>
</body> </body>
</html> </html>