daily-briefing/templates/dashboard.html
2026-02-13 00:24:31 +01:00

795 lines
41 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="de" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Daily Briefing | Live</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
'hal-accent': '#3b82f6',
'hal-bg': '#0f172a',
'hal-card': '#1e293b',
}
}
}
}
</script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
body { font-family: 'Inter', sans-serif; }
.glass-card {
background: rgba(30, 41, 59, 0.7);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
transition: all 0.3s ease;
}
.glass-card:hover {
border-color: rgba(59, 130, 246, 0.5);
}
.pulse-dot {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: .5; }
}
.fade-in {
animation: fadeIn 0.5s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.live-indicator {
display: inline-flex;
align-items: center;
font-size: 0.75rem;
color: #10b981;
}
.live-indicator::before {
content: '';
width: 6px;
height: 6px;
background: #10b981;
border-radius: 50%;
margin-right: 6px;
animation: pulse 1.5s infinite;
}
.cached-badge {
font-size: 0.65rem;
padding: 2px 6px;
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
color: #9ca3af;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
}
.status-online { background-color: #10b981; }
.status-offline { background-color: #ef4444; }
.progress-bar {
height: 4px;
background: rgba(255,255,255,0.1);
border-radius: 2px;
overflow: hidden;
}
.progress-fill {
height: 100%;
border-radius: 2px;
transition: width 0.5s ease;
}
.tab-active {
border-bottom: 2px solid #3b82f6;
color: #3b82f6;
}
.task-done {
text-decoration: line-through;
opacity: 0.5;
}
</style>
</head>
<body class="bg-hal-bg text-gray-100 min-h-screen">
<!-- Header -->
<header class="border-b border-gray-800 bg-hal-card/50 backdrop-blur sticky top-0 z-50">
<div class="max-w-7xl mx-auto px-4 py-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-4">
<div class="w-10 h-10 bg-gradient-to-br from-blue-500 to-purple-600 rounded-xl flex items-center justify-center text-xl">
🦞
</div>
<div>
<h1 class="text-xl font-bold text-white">Daily Briefing</h1>
<div class="flex items-center space-x-3 text-xs">
<span class="live-indicator">LIVE</span>
<span id="connection-status" class="text-green-400"></span>
</div>
</div>
</div>
<!-- Center Clock & Date -->
<div class="absolute left-1/2 transform -translate-x-1/2 hidden sm:flex flex-col items-center">
<div class="text-xl font-bold text-white font-mono tracking-wider leading-none" id="live-clock">--:--:--</div>
<div class="text-[10px] text-gray-400 font-medium uppercase tracking-widest mt-1" id="live-date">--. --. ----</div>
</div>
<div class="flex items-center space-x-3">
<button onclick="manualRefresh()" id="refresh-btn" class="px-3 py-1.5 bg-gray-800 hover:bg-gray-700 rounded-lg text-sm transition-colors flex items-center space-x-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
<span>Refresh</span>
</button>
</div>
</div>
</div>
</header>
<!-- Main Content -->
<main class="max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8 space-y-6">
<!-- Row 1: Weather Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
<!-- Weather Leverkusen -->
<div class="glass-card rounded-xl p-5 fade-in" id="weather-card">
<div class="flex items-center justify-between mb-3">
<h2 class="text-base font-semibold text-gray-200 flex items-center">
<svg class="w-5 h-5 mr-2 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z"></path>
</svg>
Wetter Leverkusen
</h2>
{% if weather.cached %}<span class="cached-badge">cached</span>{% endif %}
</div>
{% if weather.error %}
<div class="text-red-400 text-sm">{{ weather.error }}</div>
{% else %}
<div class="flex items-center justify-between mb-4">
<div>
<div class="text-4xl font-bold text-white" id="weather-temp">{{ weather.temp }}°</div>
<div class="text-gray-400 text-sm mt-1">Gefühlt {{ weather.feels_like }}°</div>
</div>
<div class="text-5xl" id="weather-icon">{{ weather.icon }}</div>
</div>
<div class="flex items-center justify-between text-sm mb-4">
<span class="text-gray-400" id="weather-desc">{{ weather.description }}</span>
<span class="text-gray-500">💧 {{ weather.humidity }}%</span>
</div>
<!-- Forecast -->
{% if weather.forecast %}
<div class="border-t border-gray-700 pt-3">
<div class="grid grid-cols-3 gap-2">
{% for day in weather.forecast %}
<div class="text-center p-2 bg-gray-800/50 rounded-lg">
<div class="text-xs text-gray-500 mb-1">{{ day.day }}</div>
<div class="text-xl mb-1">{{ day.icon }}</div>
<div class="text-sm font-semibold">{{ day.temp_max }}°</div>
<div class="text-xs text-gray-500">{{ day.temp_min }}°</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% endif %}
</div>
<!-- Weather Rab/Banjol -->
<div class="glass-card rounded-xl p-5 fade-in" id="weather-secondary-card">
<div class="flex items-center justify-between mb-3">
<h2 class="text-base font-semibold text-gray-200 flex items-center">
<svg class="w-5 h-5 mr-2 text-cyan-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
Wetter Rab/Banjol 🇭🇷
</h2>
{% if weather_secondary.cached %}<span class="cached-badge">cached</span>{% endif %}
</div>
{% if weather_secondary.error %}
<div class="text-red-400 text-sm">{{ weather_secondary.error }}</div>
{% else %}
<div class="flex items-center justify-between mb-4">
<div>
<div class="text-4xl font-bold text-white" id="weather-secondary-temp">{{ weather_secondary.temp }}°</div>
<div class="text-gray-400 text-sm mt-1">Gefühlt {{ weather_secondary.feels_like }}°</div>
</div>
<div class="text-5xl" id="weather-secondary-icon">{{ weather_secondary.icon }}</div>
</div>
<div class="flex items-center justify-between text-sm mb-4">
<span class="text-gray-400" id="weather-secondary-desc">{{ weather_secondary.description }}</span>
<span class="text-gray-500">💧 {{ weather_secondary.humidity }}%</span>
</div>
<!-- Forecast -->
{% if weather_secondary.forecast %}
<div class="border-t border-gray-700 pt-3">
<div class="grid grid-cols-3 gap-2">
{% for day in weather_secondary.forecast %}
<div class="text-center p-2 bg-gray-800/50 rounded-lg">
<div class="text-xs text-gray-500 mb-1">{{ day.day }}</div>
<div class="text-xl mb-1">{{ day.icon }}</div>
<div class="text-sm font-semibold">{{ day.temp_max }}°</div>
<div class="text-xs text-gray-500">{{ day.temp_min }}°</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% endif %}
</div>
</div>
<!-- Row 2: System Status & Home Assistant -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
<!-- System Status with CPU/RAM -->
<div class="glass-card rounded-xl p-5 fade-in" id="system-card">
<div class="flex items-center justify-between mb-4">
<h2 class="text-base font-semibold text-gray-200 flex items-center">
<svg class="w-5 h-5 mr-2 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"></path>
</svg>
System Status
</h2>
{% if system_status.cached %}<span class="cached-badge">cached</span>{% endif %}
</div>
<!-- Services Status - REMOVED AS REQUESTED -->
<!-- CPU & RAM -->
<div class="border-t border-gray-700 pt-4 space-y-3">
<!-- CPU -->
<div>
<div class="flex justify-between text-sm mb-1">
<span class="text-gray-400">CPU ({{ system_status.cpu.cores }} cores)</span>
<span class="text-white font-mono" id="cpu-percent">{{ system_status.cpu.percent }}%</span>
</div>
<div class="progress-bar">
<div class="progress-fill {{ 'bg-green-500' if system_status.cpu.percent < 50 else 'bg-yellow-500' if system_status.cpu.percent < 80 else 'bg-red-500' }}"
id="cpu-bar" style="width: {{ system_status.cpu.percent }}%"></div>
</div>
</div>
<!-- RAM -->
<div>
<div class="flex justify-between text-sm mb-1">
<span class="text-gray-400">RAM</span>
<span class="text-white font-mono" id="ram-percent">
{{ system_status.ram.used_gb }}/{{ system_status.ram.total_gb }} GB ({{ system_status.ram.percent }}%)
</span>
</div>
<div class="progress-bar">
<div class="progress-fill {{ 'bg-green-500' if system_status.ram.percent < 50 else 'bg-yellow-500' if system_status.ram.percent < 80 else 'bg-red-500' }}"
id="ram-bar" style="width: {{ system_status.ram.percent }}%"></div>
</div>
</div>
</div>
<div class="mt-4 pt-3 border-t border-gray-700 text-xs text-gray-500">
v{{ system_status.briefing_version }}
</div>
</div>
<!-- Home Assistant -->
<div class="glass-card rounded-xl p-5 fade-in" id="ha-card">
<div class="flex items-center justify-between mb-4">
<h2 class="text-base font-semibold text-gray-200 flex items-center">
<svg class="w-5 h-5 mr-2 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>
</svg>
Home Assistant
</h2>
{% if ha_status.cached %}<span class="cached-badge">cached</span>{% endif %}
</div>
{% if ha_status.online %}
<div class="space-y-3">
<div class="flex items-center justify-between">
<span class="text-gray-400 text-sm">Lampen an</span>
<span class="text-2xl font-bold text-yellow-400">{{ ha_status.lights_on }}/{{ ha_status.lights_total }}</span>
</div>
<div class="space-y-1.5 max-h-40 overflow-y-auto">
{% for light in ha_status.lights %}
<div class="flex items-center justify-between text-sm py-1 border-b border-gray-700/50 last:border-0">
<span class="text-gray-300 truncate">{{ light.name }}</span>
<span class="{{ 'text-yellow-400' if light.state == 'on' else 'text-gray-600' }}">
{{ "●" if light.state == 'on' else "○" }}
</span>
</div>
{% endfor %}
</div>
{% if ha_status.covers %}
<div class="pt-2 border-t border-gray-700">
<div class="text-xs text-gray-500 mb-2">Rolläden</div>
{% for cover in ha_status.covers %}
<div class="flex items-center justify-between text-sm">
<span class="text-gray-300 truncate">{{ cover.name }}</span>
<span class="text-gray-400">{{ cover.state }}</span>
</div>
{% endfor %}
</div>
{% endif %}
</div>
{% else %}
<div class="flex items-center space-x-2 text-red-400">
<span class="status-dot status-offline"></span>
<span>Offline</span>
</div>
<div class="text-red-400/70 text-sm mt-2">{{ ha_status.error }}</div>
{% endif %}
</div>
</div>
<!-- Row 3: Private Tasks (Haus & Garten + Jugendeinrichtung) -->
<div class="glass-card rounded-xl p-5 fade-in" id="tasks-private-card">
<div class="flex items-center justify-between mb-4">
<h2 class="text-base font-semibold text-gray-200 flex items-center">
<svg class="w-5 h-5 mr-2 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>
</svg>
Private Aufgaben
</h2>
<div class="flex items-center space-x-4">
<div class="flex items-center space-x-2">
<span class="text-blue-400 font-bold">{{ vikunja_all.private.open_count }}</span>
<span class="text-gray-400 text-sm">offen</span>
</div>
<div class="flex items-center space-x-2">
<span class="text-gray-500 font-bold">{{ vikunja_all.private.done_count }}</span>
<span class="text-gray-500 text-sm">erledigt</span>
</div>
{% if vikunja_all.cached %}<span class="cached-badge">cached</span>{% endif %}
</div>
</div>
<!-- Tabs -->
<div class="flex space-x-6 border-b border-gray-700 mb-4">
<button onclick="switchTabPrivate('open')" id="tab-private-open" class="pb-2 text-sm font-medium tab-active transition-colors">
Offen ({{ vikunja_all.private.open_count }})
</button>
<button onclick="switchTabPrivate('done')" id="tab-private-done" class="pb-2 text-sm font-medium text-gray-400 hover:text-gray-200 transition-colors">
Erledigt ({{ vikunja_all.private.done_count }})
</button>
</div>
<!-- Open Tasks -->
<div id="tasks-private-open" class="space-y-2 max-h-64 overflow-y-auto">
{% if vikunja_all.private.open %}
{% for task in vikunja_all.private.open %}
<div class="flex items-start space-x-3 p-3 bg-gray-800/50 rounded-lg hover:bg-gray-800 transition-colors">
<span class="text-blue-400 mt-0.5"></span>
<div class="flex-1 min-w-0">
<a href="http://10.10.10.10:3456/tasks/{{ task.id }}" target="_blank" class="text-gray-200 hover:text-blue-400 transition-colors cursor-pointer">{{ task.title }}</a>
<div class="flex items-center space-x-2 text-xs text-gray-500 mt-1">
<span class="px-2 py-0.5 bg-gray-700 rounded">{{ task.project }}</span>
{% if task.priority > 0 %}
<span class="text-yellow-400">★ {{ task.priority }}</span>
{% endif %}
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="text-gray-500 text-center py-8">Keine offenen Aufgaben 🎉</div>
{% endif %}
</div>
<!-- Done Tasks -->
<div id="tasks-private-done" class="space-y-2 max-h-64 overflow-y-auto hidden">
{% if vikunja_all.private.done %}
{% for task in vikunja_all.private.done %}
<div class="flex items-start space-x-3 p-3 bg-gray-800/30 rounded-lg">
<span class="text-green-500 mt-0.5"></span>
<div class="flex-1 min-w-0">
<a href="http://10.10.10.10:3456/tasks/{{ task.id }}" target="_blank" class="text-gray-500 task-done hover:text-gray-400 transition-colors cursor-pointer">{{ task.title }}</a>
<div class="text-xs text-gray-600 mt-1">
<span class="px-2 py-0.5 bg-gray-800 rounded">{{ task.project }}</span>
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="text-gray-500 text-center py-8">Noch keine erledigten Aufgaben</div>
{% endif %}
</div>
</div>
<!-- Row 4: Sam's Tasks (OpenClaw AI Tasks + Sam's Wunderwelt) -->
<div class="glass-card rounded-xl p-5 fade-in" id="tasks-sam-card">
<div class="flex items-center justify-between mb-4">
<h2 class="text-base font-semibold text-gray-200 flex items-center">
<svg class="w-5 h-5 mr-2 text-pink-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path>
</svg>
Sam's Aufgaben
</h2>
<div class="flex items-center space-x-4">
<div class="flex items-center space-x-2">
<span class="text-pink-400 font-bold">{{ vikunja_all.sam.open_count }}</span>
<span class="text-gray-400 text-sm">offen</span>
</div>
<div class="flex items-center space-x-2">
<span class="text-gray-500 font-bold">{{ vikunja_all.sam.done_count }}</span>
<span class="text-gray-500 text-sm">erledigt</span>
</div>
{% if vikunja_all.cached %}<span class="cached-badge">cached</span>{% endif %}
</div>
</div>
<!-- Tabs -->
<div class="flex space-x-6 border-b border-gray-700 mb-4">
<button onclick="switchTabSam('open')" id="tab-sam-open" class="pb-2 text-sm font-medium tab-active transition-colors">
Offen ({{ vikunja_all.sam.open_count }})
</button>
<button onclick="switchTabSam('done')" id="tab-sam-done" class="pb-2 text-sm font-medium text-gray-400 hover:text-gray-200 transition-colors">
Erledigt ({{ vikunja_all.sam.done_count }})
</button>
</div>
<!-- Open Tasks -->
<div id="tasks-sam-open" class="space-y-2 max-h-64 overflow-y-auto">
{% if vikunja_all.sam.open %}
{% for task in vikunja_all.sam.open %}
<div class="flex items-start space-x-3 p-3 bg-gray-800/50 rounded-lg hover:bg-gray-800 transition-colors">
<span class="text-pink-400 mt-0.5"></span>
<div class="flex-1 min-w-0">
<a href="http://10.10.10.10:3456/tasks/{{ task.id }}" target="_blank" class="text-gray-200 hover:text-pink-400 transition-colors cursor-pointer">{{ task.title }}</a>
<div class="flex items-center space-x-2 text-xs text-gray-500 mt-1">
<span class="px-2 py-0.5 bg-gray-700 rounded">{{ task.project }}</span>
{% if task.priority > 0 %}
<span class="text-yellow-400">★ {{ task.priority }}</span>
{% endif %}
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="text-gray-500 text-center py-8">Keine offenen Aufgaben 🎉</div>
{% endif %}
</div>
<!-- Done Tasks -->
<div id="tasks-sam-done" class="space-y-2 max-h-64 overflow-y-auto hidden">
{% if vikunja_all.sam.done %}
{% for task in vikunja_all.sam.done %}
<div class="flex items-start space-x-3 p-3 bg-gray-800/30 rounded-lg">
<span class="text-green-500 mt-0.5"></span>
<div class="flex-1 min-w-0">
<a href="http://10.10.10.10:3456/tasks/{{ task.id }}" target="_blank" class="text-gray-500 task-done hover:text-gray-400 transition-colors cursor-pointer">{{ task.title }}</a>
<div class="text-xs text-gray-600 mt-1">
<span class="px-2 py-0.5 bg-gray-800 rounded">{{ task.project }}</span>
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="text-gray-500 text-center py-8">Noch keine erledigten Aufgaben</div>
{% endif %}
</div>
</div>
<!-- Chat with Sam -->
<div class="glass-card rounded-xl p-5 fade-in" id="chat-card">
<div class="flex items-center justify-between mb-4">
<h2 class="text-base font-semibold text-gray-200 flex items-center">
<svg class="w-5 h-5 mr-2 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"></path>
</svg>
Chat mit Sam 🤖
</h2>
<div class="flex items-center space-x-2">
<span id="chat-typing" class="text-xs text-gray-500 hidden">Sam schreibt...</span>
<span id="chat-status" class="status-dot status-online"></span>
</div>
</div>
<!-- Chat Messages -->
<div id="chat-messages" class="space-y-3 max-h-64 overflow-y-auto mb-4 bg-gray-900/50 rounded-lg p-3">
<div class="flex items-start space-x-2">
<div class="w-8 h-8 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center text-sm flex-shrink-0">🤖</div>
<div class="bg-gray-800 rounded-lg px-3 py-2 text-sm text-gray-200 max-w-[80%]">
Hey! Ich bin Sam. Schreib mir hier direkt ich antworte so schnell ich kann.
</div>
</div>
</div>
<!-- Chat Input -->
<div class="flex items-center space-x-2">
<input
type="text"
id="chat-input"
placeholder="Nachricht an Sam..."
class="flex-1 bg-gray-800 border border-gray-700 rounded-lg px-4 py-2 text-sm text-gray-200 placeholder-gray-500 focus:outline-none focus:border-blue-500"
onkeypress="if(event.key==='Enter') sendChatMessage()"
>
<button onclick="sendChatMessage()" id="chat-send-btn" class="px-4 py-2 bg-blue-600 hover:bg-blue-500 rounded-lg text-sm text-white transition-colors flex items-center space-x-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"></path>
</svg>
<span>Senden</span>
</button>
</div>
</div>
<!-- Quick Actions -->
<div class="glass-card rounded-xl p-5 fade-in">
<h2 class="text-base font-semibold text-gray-200 mb-4 flex items-center">
<svg class="w-5 h-5 mr-2 text-pink-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
</svg>
Quick Actions
</h2>
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3">
<a href="https://homeassistant.daddelolymp.de" target="_blank" class="p-3 bg-gray-800 hover:bg-gray-700 rounded-lg text-center transition-colors">
<div class="text-2xl mb-1">🏠</div>
<div class="text-xs text-gray-300">Home Assistant</div>
</a>
<a href="http://10.10.10.10:3456" target="_blank" class="p-3 bg-gray-800 hover:bg-gray-700 rounded-lg text-center transition-colors">
<div class="text-2xl mb-1">📋</div>
<div class="text-xs text-gray-300">Vikunja</div>
</a>
<a href="https://clawhub.ai" target="_blank" class="p-3 bg-gray-800 hover:bg-gray-700 rounded-lg text-center transition-colors">
<div class="text-2xl mb-1">🦞</div>
<div class="text-xs text-gray-300">ClawHub</div>
</a>
<button onclick="manualRefresh()" class="p-3 bg-gray-800 hover:bg-gray-700 rounded-lg text-center transition-colors">
<div class="text-2xl mb-1">🔄</div>
<div class="text-xs text-gray-300">Refresh</div>
</button>
</div>
</div>
</main>
<script>
let ws = null;
let currentTab = 'open';
function connectWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
ws = new WebSocket(`${protocol}//${window.location.host}/ws`);
ws.onopen = function() {
document.getElementById('connection-status').textContent = '🟢';
document.getElementById('connection-status').className = 'text-green-400';
setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send('ping');
}
}, 30000);
};
ws.onmessage = function(event) {
const data = JSON.parse(event.data);
updateDashboard(data);
};
ws.onclose = function() {
document.getElementById('connection-status').textContent = '🔴';
document.getElementById('connection-status').className = 'text-red-400';
setTimeout(connectWebSocket, 5000);
};
}
function updateDashboard(data) {
// Update Weather
if (data.weather && !data.weather.error) {
document.getElementById('weather-temp').textContent = data.weather.temp + '°';
document.getElementById('weather-icon').textContent = data.weather.icon;
document.getElementById('weather-desc').textContent = data.weather.description;
}
// Update Secondary Weather
if (data.weather_secondary && !data.weather_secondary.error) {
document.getElementById('weather-secondary-temp').textContent = data.weather_secondary.temp + '°';
document.getElementById('weather-secondary-icon').textContent = data.weather_secondary.icon;
document.getElementById('weather-secondary-desc').textContent = data.weather_secondary.description;
}
// Update System Status
if (data.system_status) {
document.getElementById('cpu-percent').textContent = data.system_status.cpu.percent + '%';
document.getElementById('cpu-bar').style.width = data.system_status.cpu.percent + '%';
document.getElementById('ram-percent').textContent =
`${data.system_status.ram.used_gb}/${data.system_status.ram.total_gb} GB (${data.system_status.ram.percent}%)`;
document.getElementById('ram-bar').style.width = data.system_status.ram.percent + '%';
}
// Update HA
if (data.ha_status && data.ha_status.online) {
// Could update HA content here
}
// Update Tasks
if (data.vikunja_all) {
// Update task counts
// Could refresh task lists here if needed
}
}
function switchTabPrivate(tab) {
document.getElementById('tasks-private-open').classList.toggle('hidden', tab !== 'open');
document.getElementById('tasks-private-done').classList.toggle('hidden', tab !== 'done');
document.getElementById('tab-private-open').classList.toggle('tab-active', tab === 'open');
document.getElementById('tab-private-open').classList.toggle('text-gray-400', tab !== 'open');
document.getElementById('tab-private-done').classList.toggle('tab-active', tab === 'done');
document.getElementById('tab-private-done').classList.toggle('text-gray-400', tab !== 'done');
}
function switchTabSam(tab) {
document.getElementById('tasks-sam-open').classList.toggle('hidden', tab !== 'open');
document.getElementById('tasks-sam-done').classList.toggle('hidden', tab !== 'done');
document.getElementById('tab-sam-open').classList.toggle('tab-active', tab === 'open');
document.getElementById('tab-sam-open').classList.toggle('text-gray-400', tab !== 'open');
document.getElementById('tab-sam-done').classList.toggle('tab-active', tab === 'done');
document.getElementById('tab-sam-done').classList.toggle('text-gray-400', tab !== 'done');
}
async function manualRefresh() {
const btn = document.getElementById('refresh-btn');
btn.disabled = true;
btn.innerHTML = '<span class="animate-spin">↻</span> Lade...';
try {
const response = await fetch('/api/all');
if (response.ok) {
const data = await response.json();
updateDashboard(data);
}
} catch (e) {
console.error('Refresh failed:', e);
} finally {
btn.disabled = false;
btn.innerHTML = `
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
<span>Refresh</span>
`;
}
}
// Chat Functions
let chatWs = null;
let chatHistory = [];
function connectChatWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
chatWs = new WebSocket(`${protocol}//${window.location.host}/ws/chat`);
chatWs.onopen = function() {
console.log('Chat WebSocket connected');
};
chatWs.onmessage = function(event) {
const data = JSON.parse(event.data);
if (data.type === 'history') {
// Clear and load history
document.getElementById('chat-messages').innerHTML = '';
data.messages.forEach(msg => {
addChatMessageToUI(msg.content, msg.role === 'user', msg.id);
});
// Add welcome message if empty
if (data.messages.length === 0) {
addChatMessageToUI("Hey! Ich bin Sam. Schreib mir hier direkt ich antworte so schnell ich kann.", false, 'welcome');
}
} else if (data.type === 'message') {
const isUser = data.message.role === 'user';
addChatMessageToUI(data.message.content, isUser, data.message.id);
// Hide typing indicator for assistant messages
if (!isUser) {
document.getElementById('chat-typing').classList.add('hidden');
document.getElementById('chat-send-btn').disabled = false;
}
}
};
chatWs.onclose = function() {
console.log('Chat WebSocket disconnected, retrying...');
setTimeout(connectChatWebSocket, 5000);
};
chatWs.onerror = function(e) {
console.error('Chat WebSocket error:', e);
};
}
function addChatMessageToUI(text, isUser = false, id = '') {
const container = document.getElementById('chat-messages');
// Check if message already exists
if (id && document.getElementById(`msg-${id}`)) {
return;
}
const messageDiv = document.createElement('div');
messageDiv.id = id ? `msg-${id}` : '';
messageDiv.className = 'flex items-start space-x-2 fade-in';
if (isUser) {
messageDiv.innerHTML = `
<div class="flex-1 flex justify-end">
<div class="bg-blue-600 rounded-lg px-3 py-2 text-sm text-white max-w-[80%]">
${escapeHtml(text)}
</div>
</div>
<div class="w-8 h-8 bg-gray-700 rounded-full flex items-center justify-center text-sm flex-shrink-0">👤</div>
`;
} else {
messageDiv.innerHTML = `
<div class="w-8 h-8 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center text-sm flex-shrink-0">🤖</div>
<div class="bg-gray-800 rounded-lg px-3 py-2 text-sm text-gray-200 max-w-[80%] whitespace-pre-wrap">
${escapeHtml(text)}
</div>
`;
}
container.appendChild(messageDiv);
container.scrollTop = container.scrollHeight;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
async function sendChatMessage() {
const input = document.getElementById('chat-input');
const btn = document.getElementById('chat-send-btn');
const text = input.value.trim();
if (!text) return;
// Check WebSocket connection
if (!chatWs || chatWs.readyState !== WebSocket.OPEN) {
addChatMessageToUI('Keine Verbindung zu Sam. Versuch es später nochmal.', false, 'error');
return;
}
input.value = '';
btn.disabled = true;
document.getElementById('chat-typing').classList.remove('hidden');
// Send via WebSocket
chatWs.send(JSON.stringify({
type: 'message',
content: text
}));
// Re-enable button after timeout if no response
setTimeout(() => {
btn.disabled = false;
document.getElementById('chat-typing').classList.add('hidden');
}, 30000);
}
// Initialize
document.addEventListener('DOMContentLoaded', function() {
connectWebSocket();
connectChatWebSocket();
// Live Clock
function updateClock() {
const now = new Date();
const timeStr = now.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false });
const dateStr = now.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
document.getElementById('live-clock').textContent = timeStr;
document.getElementById('live-date').textContent = dateStr;
}
setInterval(updateClock, 1000);
updateClock();
setInterval(() => {
if (!ws || ws.readyState !== WebSocket.OPEN) {
manualRefresh();
}
}, 30000);
});
</script>
</body>
</html>