diff --git a/aggregator.py b/aggregator.py index 86bc034..20a88f8 100644 --- a/aggregator.py +++ b/aggregator.py @@ -4,6 +4,7 @@ A lightweight event broker that manages priority-based system status. """ import json +import random import threading import time from datetime import datetime @@ -16,6 +17,22 @@ ROOT_DIR = Path(__file__).parent # Configuration STATUS_FILE = Path(__file__).parent / "status.json" DEFAULT_NOTIFY_TTL = 10 # Default TTL for Priority 3 (Notify) events +CELEBRATION_DURATION = 5 # Seconds to show celebration after recovery + +# Emote variations with paired animations +OPTIMAL_EMOTES = [ + ("( ^_^)", "breathing"), # calm, content + ("( ᵔᴥᵔ)", "floating"), # dreamy + ("(◕‿◕)", "bouncing"), # cheerful + ("( ・ω・)", "swaying"), # curious + ("( ˘▽˘)", "breathing"), # cozy +] +IDLE_EMOTES = [ + ("( -_^)", "blink"), # wink + ("( ^_~)", "blink"), # wink + ("( ᵕ.ᵕ)", "blink"), # blink +] +CELEBRATION_EMOTE = ("\\(^o^)/", "celebrating") # Priority definitions PRIORITY_CONFIG = { @@ -29,9 +46,20 @@ PRIORITY_CONFIG = { events_lock = threading.Lock() active_events = {} # id -> {priority, message, timestamp, ttl} +# State tracking for personality +previous_priority = 4 +celebrating_until = 0 +last_emote_change = 0 +current_optimal_emote = OPTIMAL_EMOTES[0][0] +current_optimal_animation = OPTIMAL_EMOTES[0][1] + def get_current_state(): """Determine current state based on active events.""" + global previous_priority, celebrating_until, last_emote_change, current_optimal_emote, current_optimal_animation + + now = time.time() + with events_lock: if not active_events: priority = 4 @@ -45,11 +73,38 @@ def get_current_state(): ] config = PRIORITY_CONFIG[priority] + emote = config["emote"] + animation = config["animation"] + color = config["color"] + + # Check for recovery (was bad, now optimal) + if priority == 4 and previous_priority < 4: + celebrating_until = now + CELEBRATION_DURATION + + previous_priority = priority + + # Handle optimal state personality + if priority == 4: + if now < celebrating_until: + # Celebration mode + emote, animation = CELEBRATION_EMOTE + else: + # Rotate optimal emotes every 5 minutes, occasional idle expression + if now - last_emote_change > 300: + last_emote_change = now + # 15% chance of an idle expression (wink/blink) + if random.random() < 0.15: + current_optimal_emote, current_optimal_animation = random.choice(IDLE_EMOTES) + else: + current_optimal_emote, current_optimal_animation = random.choice(OPTIMAL_EMOTES) + emote = current_optimal_emote + animation = current_optimal_animation + return { "current_state": config["name"].lower(), - "active_emote": config["emote"], - "color": config["color"], - "animation": config["animation"], + "active_emote": emote, + "color": color, + "animation": animation, "message": config["name"] if priority == 4 else f"{config['name']} state active", "active_events": sorted(events_list, key=lambda x: x["priority"]), "last_updated": datetime.now().isoformat(timespec="seconds"), diff --git a/index.html b/index.html index c2bf065..6c37522 100644 --- a/index.html +++ b/index.html @@ -76,6 +76,102 @@ transform: scale(1.08); } } + + /* Celebrating animation - bounce and wiggle */ + .celebrating { + animation: celebrate 0.5s ease-in-out infinite; + } + + @keyframes celebrate { + 0%, 100% { + transform: translateY(0) rotate(0deg); + } + 25% { + transform: translateY(-10px) rotate(-5deg); + } + 75% { + transform: translateY(-10px) rotate(5deg); + } + } + + /* Floating animation - gentle drift */ + .floating { + animation: float 4s ease-in-out infinite; + } + + @keyframes float { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-8px); + } + } + + /* Bouncing animation - playful hop */ + .bouncing { + animation: bounce 1s ease-in-out infinite; + } + + @keyframes bounce { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-12px); + } + } + + /* Swaying animation - curious side-to-side */ + .swaying { + animation: sway 3s ease-in-out infinite; + } + + @keyframes sway { + 0%, 100% { + transform: rotate(0deg); + } + 25% { + transform: rotate(-3deg); + } + 75% { + transform: rotate(3deg); + } + } + + /* Blink animation - quick fade for winks */ + .blink { + animation: blink 0.3s ease-in-out 1; + } + + @keyframes blink { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.3; + } + } + + /* Searching animation - looking around for connection */ + .searching { + animation: search 2s ease-in-out infinite; + } + + @keyframes search { + 0%, 100% { + transform: translateX(0); + opacity: 0.6; + } + 25% { + transform: translateX(-10px); + opacity: 0.8; + } + 75% { + transform: translateX(10px); + opacity: 0.8; + } + } @@ -94,7 +190,12 @@ const data = await response.json(); updateDisplay(data); } catch (err) { - messageEl.textContent = 'Connection lost...'; + // Connection lost state + emoteEl.textContent = '( ?.?)'; + emoteEl.style.color = '#888888'; + emoteEl.className = 'searching'; + messageEl.style.color = '#888888'; + messageEl.textContent = ''; } }