implemented client side filters, mobile first styling and modals for articles

This commit is contained in:
2025-08-02 01:33:49 +02:00
parent e1f51794af
commit ccc1a90cbe
14 changed files with 1248 additions and 131 deletions

View File

@@ -22,6 +22,7 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.interval import IntervalTrigger
from fastapi import Depends, FastAPI, HTTPException, Response, status
from fastapi.staticfiles import StaticFiles
from starlette.responses import JSONResponse
from backend.app.config import (
CRON_HOURS,
@@ -65,51 +66,117 @@ scheduler.start()
@app.get("/news", response_model=List[Dict[str, Any]])
async def get_news(
country: str = "DE",
from_: str = "2025-07-01",
to_: str = datetime.now(timezone.utc).strftime("%Y-%m-%d"),
from_: str = None,
to_: str = None,
timezone_name: str = "UTC",
all_countries: bool = False,
all_dates: bool = False,
db: sqlite3.Cursor = Depends(get_db)
):
"""
Get news articles filtered by country and date range.
Now optimized for concurrent access while scheduler is running.
Now handles client timezone properly and supports multiple countries and all news.
Args:
country: Country code to filter by (default: "DE")
from_: Start date in ISO format (default: "2025-07-01")
to: End date in ISO format (default: current date)
country: Country code(s) to filter by (default: "DE").
For multiple countries, use comma-separated values like "DE,US,GB"
from_: Start date in ISO format (optional)
to_: End date in ISO format (optional)
timezone_name: Client timezone for date interpretation (default: "UTC")
all_countries: If True, returns news from all countries (overrides country param)
all_dates: If True, returns news from all dates (overrides date params)
db: Database cursor dependency
Returns:
List of news articles matching the criteria
"""
try:
datetime.fromisoformat(from_)
datetime.fromisoformat(to_)
from zoneinfo import ZoneInfo
from_ts = int(datetime.fromisoformat(from_).timestamp())
to_ts = int(datetime.fromisoformat(to_).timestamp())
# Handle timezone
try:
client_tz = ZoneInfo(timezone_name)
except Exception:
logger.warning(f"Invalid timezone '{timezone_name}', using UTC")
client_tz = timezone.utc
db.execute(
"""
SELECT id, title, description, url, published, country, created_at
FROM news
WHERE country = ?
AND published BETWEEN ? AND ?
ORDER BY published DESC LIMIT 1000
""",
(country, from_ts, to_ts)
)
where_conditions = []
params = []
return [dict(row) for row in db.fetchall()]
if not all_countries:
countries = [c.strip().upper() for c in country.split(',') if c.strip()]
if len(countries) == 1:
where_conditions.append("country = ?")
params.append(countries[0])
elif len(countries) > 1:
placeholders = ','.join(['?' for _ in countries])
where_conditions.append(f"country IN ({placeholders})")
params.extend(countries)
except ValueError:
raise HTTPException(
400, "Invalid date format. Use ISO format (YYYY-MM-DD)")
if not all_dates and (from_ or to_):
if not from_:
from_ = "2025-01-01" # Default start date
if not to_:
to_ = datetime.now(timezone.utc).strftime("%Y-%m-%d") # Default to today
# Parse and convert dates
from_date_naive = datetime.fromisoformat(from_)
to_date_naive = datetime.fromisoformat(to_)
from_date = from_date_naive.replace(tzinfo=client_tz)
to_date = to_date_naive.replace(tzinfo=client_tz)
# Include the entire end date
to_date = to_date.replace(hour=23, minute=59, second=59)
from_ts = int(from_date.timestamp())
to_ts = int(to_date.timestamp())
where_conditions.append("published BETWEEN ? AND ?")
params.extend([from_ts, to_ts])
logger.info(f"Date range: {from_date} to {to_date} (UTC timestamps: {from_ts} to {to_ts})")
# Build the complete SQL query
base_sql = """
SELECT id, title, summary, url, published, country, created_at
FROM news \
"""
if where_conditions:
sql = base_sql + " WHERE " + " AND ".join(where_conditions)
else:
sql = base_sql
sql += " ORDER BY published DESC LIMIT 1000"
# Log query info
if all_countries and all_dates:
logger.info("Querying ALL news articles (no filters)")
elif all_countries:
logger.info(f"Querying news from ALL countries with date filter")
elif all_dates:
logger.info(f"Querying ALL dates for countries: {country}")
else:
logger.info(f"Querying news: countries={country}, timezone={timezone_name}")
logger.info(f"SQL: {sql}")
logger.info(f"Parameters: {params}")
# Execute the query
db.execute(sql, params)
rows = db.fetchall()
result = [dict(row) for row in rows]
logger.info(f"Found {len(result)} news articles")
return result
except ValueError as e:
logger.error(f"Date parsing error: {e}")
raise HTTPException(400, "Invalid date format. Use ISO format (YYYY-MM-DD)")
except Exception as e:
logger.error(f"❌ Error fetching news: {e}")
raise HTTPException(
500, "Internal server error while fetching news"
)
raise HTTPException(500, "Internal server error while fetching news")
@app.get("/feeds", response_model=List[Dict[str, Any]])
@@ -125,7 +192,7 @@ async def list_feeds(db: sqlite3.Cursor = Depends(get_db)):
"""
try:
db.execute("SELECT * FROM feeds ORDER BY country, url")
return [dict(row) for row in db.fetchall()]
return JSONResponse(content=[dict(row) for row in db.fetchall()])
except Exception as e:
logger.error(f"❌ Error fetching feeds: {e}")
raise HTTPException(
@@ -133,23 +200,6 @@ async def list_feeds(db: sqlite3.Cursor = Depends(get_db)):
)
@app.get("/meta/last-sync", response_model=TimestampResponse)
async def get_last_sync(db: sqlite3.Cursor = Depends(get_db)):
"""
Get the timestamp of the last successful feed synchronization.
Args:
db: Database cursor dependency
Returns:
Object containing the timestamp as a Unix epoch
"""
db.execute("SELECT val FROM meta WHERE key='last_sync'")
row = db.fetchone()
if row is None:
import time
return {"ts": int(time.time())}
return {"ts": int(row["val"])}
@app.post("/feeds", response_model=SuccessResponse)
@@ -271,6 +321,25 @@ async def manual_sync(db: sqlite3.Cursor = Depends(get_db)):
)
@app.get("/meta/last-sync", response_model=TimestampResponse)
async def get_last_sync(db: sqlite3.Cursor = Depends(get_db)):
"""
Get the timestamp of the last successful feed synchronization.
Args:
db: Database cursor dependency
Returns:
Object containing the timestamp as a Unix epoch
"""
db.execute("SELECT val FROM meta WHERE key='last_sync'")
row = db.fetchone()
if row is None:
import time
return {"ts": int(time.time())}
return {"ts": int(row["val"])}
@app.get("/settings/cron", response_model=HoursResponse)
async def get_cron_schedule(db: sqlite3.Cursor = Depends(get_db)):
"""