daily-briefing/templates/dashboard.html

807 lines
42 KiB
HTML
Raw Normal View History

2026-02-13 00:24:31 +01:00
<!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
2026-02-13 00:24:31 +01:00
</div>
<div>
<h1 class="text-xl font-bold text-white">Sam's Dashboard</h1>
2026-02-13 00:24:31 +01:00
<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 lg:flex flex-col items-center z-0">
2026-02-13 00:24:31 +01:00
<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 mr-12">
<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>
2026-02-13 00:24:31 +01:00
<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 %}
2026-02-13 00:24:31 +01:00
</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 %}
2026-02-13 00:24:31 +01:00
</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>
2026-02-13 00:24:31 +01:00
</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>
2026-02-13 00:24:31 +01:00
</div>
{% endif %}
2026-02-13 00:24:31 +01:00
</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>
2026-02-13 00:24:31 +01:00
</svg>
Nächste Stunden
2026-02-13 00:24:31 +01:00
</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 %}
2026-02-13 00:24:31 +01:00
</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>
2026-02-13 00:24:31 +01:00
</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.
2026-02-13 00:24:31 +01:00
</div>
{% endif %}
</div>
</div>
<div id="tasks-section" class="grid grid-cols-1 lg:grid-cols-2 gap-6">
2026-02-13 00:24:31 +01:00
<!-- 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>
</div>
<div 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">
2026-02-13 00:24:31 +01:00
<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);
});
}
2026-02-13 00:24:31 +01:00
// 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>