feat: add Admin Panel with JWT auth, DB settings, and integration management
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>
2026-03-02 10:37:30 +01:00
|
|
|
"""News service — queries market_news from PostgreSQL via shared pool."""
|
|
|
|
|
|
2026-03-02 01:48:51 +01:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import asyncpg
|
|
|
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
|
|
feat: add Admin Panel with JWT auth, DB settings, and integration management
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>
2026-03-02 10:37:30 +01:00
|
|
|
from server.db import get_pool
|
2026-03-02 01:48:51 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _row_to_dict(row: asyncpg.Record) -> Dict[str, Any]:
|
|
|
|
|
"""Convert an asyncpg Record to a plain dictionary with JSON-safe values."""
|
|
|
|
|
d: Dict[str, Any] = dict(row)
|
|
|
|
|
if "published_at" in d and d["published_at"] is not None:
|
|
|
|
|
d["published_at"] = d["published_at"].isoformat()
|
|
|
|
|
return d
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def get_news(
|
|
|
|
|
limit: int = 20,
|
|
|
|
|
offset: int = 0,
|
|
|
|
|
category: Optional[str] = None,
|
|
|
|
|
max_age_hours: int = 48,
|
|
|
|
|
) -> List[Dict[str, Any]]:
|
feat: add Admin Panel with JWT auth, DB settings, and integration management
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>
2026-03-02 10:37:30 +01:00
|
|
|
"""Fetch recent news articles from the market_news table."""
|
|
|
|
|
pool = await get_pool()
|
2026-03-02 01:48:51 +01:00
|
|
|
|
|
|
|
|
params: List[Any] = []
|
|
|
|
|
param_idx = 1
|
|
|
|
|
|
|
|
|
|
base_query = (
|
|
|
|
|
"SELECT id, source, title, url, category, published_at "
|
|
|
|
|
"FROM market_news "
|
|
|
|
|
f"WHERE published_at > NOW() - INTERVAL '{int(max_age_hours)} hours'"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if category is not None:
|
|
|
|
|
base_query += f" AND category = ${param_idx}"
|
|
|
|
|
params.append(category)
|
|
|
|
|
param_idx += 1
|
|
|
|
|
|
|
|
|
|
base_query += f" ORDER BY published_at DESC LIMIT ${param_idx} OFFSET ${param_idx + 1}"
|
|
|
|
|
params.append(limit)
|
|
|
|
|
params.append(offset)
|
|
|
|
|
|
feat: add Admin Panel with JWT auth, DB settings, and integration management
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>
2026-03-02 10:37:30 +01:00
|
|
|
async with pool.acquire() as conn:
|
2026-03-02 01:48:51 +01:00
|
|
|
rows = await conn.fetch(base_query, *params)
|
|
|
|
|
|
|
|
|
|
return [_row_to_dict(row) for row in rows]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def get_news_count(
|
|
|
|
|
max_age_hours: int = 48,
|
|
|
|
|
category: Optional[str] = None,
|
|
|
|
|
) -> int:
|
feat: add Admin Panel with JWT auth, DB settings, and integration management
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>
2026-03-02 10:37:30 +01:00
|
|
|
"""Return the total count of recent news articles."""
|
|
|
|
|
pool = await get_pool()
|
2026-03-02 01:48:51 +01:00
|
|
|
|
|
|
|
|
params: List[Any] = []
|
|
|
|
|
param_idx = 1
|
|
|
|
|
|
|
|
|
|
query = (
|
|
|
|
|
"SELECT COUNT(*) AS cnt "
|
|
|
|
|
"FROM market_news "
|
|
|
|
|
f"WHERE published_at > NOW() - INTERVAL '{int(max_age_hours)} hours'"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if category is not None:
|
|
|
|
|
query += f" AND category = ${param_idx}"
|
|
|
|
|
params.append(category)
|
|
|
|
|
|
feat: add Admin Panel with JWT auth, DB settings, and integration management
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>
2026-03-02 10:37:30 +01:00
|
|
|
async with pool.acquire() as conn:
|
2026-03-02 01:48:51 +01:00
|
|
|
row = await conn.fetchrow(query, *params)
|
|
|
|
|
|
|
|
|
|
return int(row["cnt"]) if row else 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def get_categories(max_age_hours: int = 48) -> List[str]:
|
feat: add Admin Panel with JWT auth, DB settings, and integration management
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>
2026-03-02 10:37:30 +01:00
|
|
|
"""Return distinct categories from recent news articles."""
|
|
|
|
|
pool = await get_pool()
|
2026-03-02 01:48:51 +01:00
|
|
|
|
|
|
|
|
query = (
|
|
|
|
|
"SELECT DISTINCT category "
|
|
|
|
|
"FROM market_news "
|
|
|
|
|
f"WHERE published_at > NOW() - INTERVAL '{int(max_age_hours)} hours' "
|
|
|
|
|
"AND category IS NOT NULL "
|
|
|
|
|
"ORDER BY category"
|
|
|
|
|
)
|
|
|
|
|
|
feat: add Admin Panel with JWT auth, DB settings, and integration management
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>
2026-03-02 10:37:30 +01:00
|
|
|
async with pool.acquire() as conn:
|
2026-03-02 01:48:51 +01:00
|
|
|
rows = await conn.fetch(query)
|
|
|
|
|
|
|
|
|
|
return [row["category"] for row in rows]
|