daily-briefing/templates/dashboard.html

802 lines
42 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">
Briefing
</div>
<div>
<h1 class="text-xl font-bold text-white">Sam's Dashboard</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 - Moved to z-0 and using better responsive classes -->
<div class="absolute left-1/2 transform -translate-x-1/2 hidden lg:flex flex-col items-center -z-10 opacity-50 pointer-events-none">
<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>
<nav class="hidden md:flex items-center space-x-8">
<a href="#weather-section" class="text-sm font-medium text-gray-400 hover:text-white transition-colors">Wetter</a>
<a href="#news-section" class="text-sm font-medium text-gray-400 hover:text-white transition-colors">News</a>
<a href="#tasks-section" class="text-sm font-medium text-gray-400 hover:text-white transition-colors">Tasks</a>
</nav>
<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 & Hourly -->
<div id="weather-section" class="space-y-6">
<div class="grid grid-cols-1 lg:grid-cols-3 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>
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>
{% 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>
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>
{% endif %}
</div>
<!-- Hourly Forecast (Leverkusen) -->
<div class="glass-card rounded-xl p-5 fade-in lg:col-span-1" id="hourly-weather-card">
<h2 class="text-base font-semibold text-gray-200 mb-4 flex items-center">
<svg class="w-5 h-5 mr-2 text-yellow-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
Nächste Stunden
</h2>
<div class="flex space-x-4 overflow-x-auto pb-2 scrollbar-hide" id="hourly-container">
{% if hourly_weather and hourly_weather.Leverkusen %}
{% for hour in hourly_weather.Leverkusen[:8] %}
<div class="flex-shrink-0 text-center p-2 bg-gray-800/40 rounded-lg min-w-[70px]">
<div class="text-[10px] text-gray-500 mb-1">{{ hour.time }}</div>
<div class="text-xl mb-1">{{ hour.icon }}</div>
<div class="text-sm font-bold">{{ hour.temp }}°</div>
<div class="text-[9px] text-gray-400">{{ hour.precip }}%</div>
</div>
{% endfor %}
{% else %}
<div class="text-gray-500 text-xs py-4">Keine Stundendaten verfügbar.</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Row 2: News Headlines -->
<div id="news-section" class="space-y-4">
<h2 class="text-lg font-bold text-white flex items-center">
<svg class="w-6 h-6 mr-2 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"></path>
</svg>
Aktuelle Schlagzeilen
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4" id="news-container">
{% if news %}
{% for item in news[:12] %}
<div class="glass-card rounded-xl p-4 flex flex-col h-full hover:scale-[1.02] transition-transform">
<div class="flex items-center justify-between mb-2">
<span class="text-[10px] font-bold uppercase tracking-wider text-gray-500">{{ item.source }}</span>
<span class="text-[9px] text-gray-600">{{ item.time }}</span>
</div>
<h3 class="text-sm font-medium text-gray-200 line-clamp-3 mb-3 flex-grow">
{{ item.title }}
</h3>
<a href="{{ item.url }}" target="_blank" class="text-blue-400 text-xs font-semibold flex items-center hover:text-blue-300">
Mehr lesen
<svg class="w-3 h-3 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"></path>
</svg>
</a>
</div>
{% endfor %}
{% else %}
<div class="col-span-full glass-card rounded-xl p-8 text-center text-gray-500">
Keine aktuellen Nachrichten geladen.
</div>
{% endif %}
</div>
</div>
<div id="tasks-section" class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- 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>
</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 Hourly Weather
if (data.hourly_weather && data.hourly_weather.Leverkusen) {
const container = document.getElementById('hourly-container');
container.innerHTML = '';
data.hourly_weather.Leverkusen.slice(0, 8).forEach(hour => {
const div = document.createElement('div');
div.className = 'flex-shrink-0 text-center p-2 bg-gray-800/40 rounded-lg min-w-[70px]';
div.innerHTML = `
<div class="text-[10px] text-gray-500 mb-1">${hour.time}</div>
<div class="text-xl mb-1">${hour.icon}</div>
<div class="text-sm font-bold">${hour.temp}°</div>
<div class="text-[9px] text-gray-400">${hour.precip}%</div>
`;
container.appendChild(div);
});
}
// Update News
if (data.news && data.news.length > 0) {
const container = document.getElementById('news-container');
container.innerHTML = '';
data.news.slice(0, 12).forEach(item => {
const div = document.createElement('div');
div.className = 'glass-card rounded-xl p-4 flex flex-col h-full hover:scale-[1.02] transition-transform';
div.innerHTML = `
<div class="flex items-center justify-between mb-2">
<span class="text-[10px] font-bold uppercase tracking-wider text-gray-500">${item.source}</span>
<span class="text-[9px] text-gray-600">${item.time}</span>
</div>
<h3 class="text-sm font-medium text-gray-200 line-clamp-3 mb-3 flex-grow">
${item.title}
</h3>
<a href="${item.url}" target="_blank" class="text-blue-400 text-xs font-semibold flex items-center hover:text-blue-300">
Mehr lesen
<svg class="w-3 h-3 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"></path>
</svg>
</a>
`;
container.appendChild(div);
});
}
// 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>