Complete admin backend with login, where all integrations (weather, news, Home Assistant, Vikunja, Unraid, MQTT) can be configured via web UI instead of ENV variables. Two-layer config: ENV seeds DB on first start, then DB is source of truth. Auto-migration system on startup. Backend: db.py shared pool, auth.py JWT, settings_service CRUD, seed_service, admin router (protected), test_connections per integration, config.py rewrite. Frontend: react-router v6, login page, admin layout with sidebar, 8 settings pages (General, Weather, News, HA, Vikunja, Unraid, MQTT, ChangePassword), shared IntegrationForm + TestButton components. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
80 lines
2.2 KiB
Python
80 lines
2.2 KiB
Python
"""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 get_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=get_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=get_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, get_settings().news_cache_ttl)
|
|
return payload
|