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:
parent
4bbc125a67
commit
9f7330e217
48 changed files with 6390 additions and 1461 deletions
80
server/routers/news.py
Normal file
80
server/routers/news.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
"""News articles router -- paginated, filterable by category."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastapi import APIRouter, Query
|
||||
|
||||
from server.cache import cache
|
||||
from server.config import settings
|
||||
from server.services.news_service import get_news, get_news_count
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["news"])
|
||||
|
||||
|
||||
def _cache_key(limit: int, offset: int, category: Optional[str]) -> str:
|
||||
return f"news:{limit}:{offset}:{category}"
|
||||
|
||||
|
||||
@router.get("/news")
|
||||
async def get_news_articles(
|
||||
limit: int = Query(default=20, le=50, ge=1),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
category: Optional[str] = Query(default=None),
|
||||
) -> Dict[str, Any]:
|
||||
"""Return a paginated list of news articles.
|
||||
|
||||
Response shape::
|
||||
|
||||
{
|
||||
"articles": [ ... ],
|
||||
"total": int,
|
||||
"limit": int,
|
||||
"offset": int,
|
||||
}
|
||||
"""
|
||||
|
||||
key = _cache_key(limit, offset, category)
|
||||
|
||||
# --- cache hit? -----------------------------------------------------------
|
||||
cached = await cache.get(key)
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
# --- cache miss -----------------------------------------------------------
|
||||
articles: List[Dict[str, Any]] = []
|
||||
total: int = 0
|
||||
|
||||
try:
|
||||
articles = await get_news(limit=limit, offset=offset, category=category, max_age_hours=settings.news_max_age_hours)
|
||||
except Exception as exc:
|
||||
logger.exception("Failed to fetch news articles")
|
||||
return {
|
||||
"articles": [],
|
||||
"total": 0,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
"error": True,
|
||||
"message": str(exc),
|
||||
}
|
||||
|
||||
try:
|
||||
total = await get_news_count(max_age_hours=settings.news_max_age_hours, category=category)
|
||||
except Exception as exc:
|
||||
logger.exception("Failed to fetch news count")
|
||||
# We still have articles -- return them with total = len(articles)
|
||||
total = len(articles)
|
||||
|
||||
payload: Dict[str, Any] = {
|
||||
"articles": articles,
|
||||
"total": total,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
}
|
||||
|
||||
await cache.set(key, payload, settings.news_cache_ttl)
|
||||
return payload
|
||||
Loading…
Add table
Add a link
Reference in a new issue