refactor: complete rewrite as React+FastAPI dashboard

Replace monolithic Jinja2 template with modern stack:

Backend (FastAPI):
- Modular router/service architecture
- Async PostgreSQL (asyncpg) for news from n8n pipeline
- Live Unraid server stats (2 servers via API)
- Home Assistant, Vikunja tasks, weather (wttr.in)
- WebSocket broadcast for real-time updates (15s)
- TTL cache per endpoint, all config via ENV vars

Frontend (React + Vite + TypeScript):
- Glassmorphism dark theme with Tailwind CSS
- Responsive grid: mobile/tablet/desktop/ultrawide
- Weather cards, hourly forecast, news with category tabs
- Server stats (CPU ring, RAM bar, Docker list)
- Home Assistant controls, task management
- Live clock, WebSocket connection indicator

Infrastructure:
- Multi-stage Dockerfile (node:22-alpine + python:3.11-slim)
- docker-compose with full ENV configuration
- Kaniko CI/CD pipeline for GitLab registry

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sam 2026-03-02 01:48:51 +01:00
parent 4bbc125a67
commit 9f7330e217
48 changed files with 6390 additions and 1461 deletions

41
server/cache.py Normal file
View file

@ -0,0 +1,41 @@
"""Simple async-safe TTL cache."""
from __future__ import annotations
import asyncio
import time
from typing import Any, Dict, Optional, Tuple
class TTLCache:
"""Thread/async-safe in-memory cache with per-key TTL."""
def __init__(self) -> None:
self._store: Dict[str, Tuple[Any, float]] = {}
self._lock = asyncio.Lock()
async def get(self, key: str) -> Optional[Any]:
async with self._lock:
entry = self._store.get(key)
if entry is None:
return None
value, expires_at = entry
if time.time() > expires_at:
del self._store[key]
return None
return value
async def set(self, key: str, value: Any, ttl: int) -> None:
async with self._lock:
self._store[key] = (value, time.time() + ttl)
async def invalidate(self, key: str) -> None:
async with self._lock:
self._store.pop(key, None)
async def clear(self) -> None:
async with self._lock:
self._store.clear()
cache = TTLCache()