mirror of
https://github.com/shawnkim1997/All-in-one-Financial-Analysis.git
synced 2026-08-22 23:28:05 +00:00
Complete migration from Streamlit to Next.js 14 App Router + FastAPI backend. Frontend (Next.js 14): - 10 pages: Overview, Research, Valuation, Technical, Markets, Earnings, News, Portfolio, Filings, Settings - Terminal Noir dark theme with custom Tailwind config - TradingView Lightweight Charts for candlestick/volume - Valuation: DCF, Sensitivity Matrix, Monte Carlo, Tornado, Reverse DCF - Financial Statements table with YoY growth badges and margin rows - SEC EDGAR inline filing viewer with section tabs - News split-view with iframe article embedding - Technical Analysis with RSI, MACD, Bollinger, Fibonacci, Moving Averages - Earnings beat/miss visualization - AI Copilot chat panel with Gemini integration Backend (FastAPI): - 13 routers: market_data, financials, valuation, technical, earnings, insider, edgar, news, portfolio, analysis, chat, estimates, fx - Services: DCF engine, Monte Carlo simulation, sensitivity analysis, risk metrics, SEC parser, technical indicators - yfinance + yahooquery data sources with fallback pattern - SQLite caching layer Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
160 lines
4.2 KiB
Python
160 lines
4.2 KiB
Python
"""Dashboard layout persistence backed by SQLite.
|
|
|
|
All functions operate on the ``dashboards`` table and return plain dicts.
|
|
"""
|
|
|
|
from datetime import datetime, timezone
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
import aiosqlite
|
|
|
|
from server.db.database import get_db
|
|
|
|
|
|
def _row_to_dict(row: aiosqlite.Row) -> Dict[str, Any]:
|
|
"""Convert an ``aiosqlite.Row`` to a plain dict.
|
|
|
|
Args:
|
|
row: A database row.
|
|
|
|
Returns:
|
|
A dict keyed by column name.
|
|
"""
|
|
return dict(row)
|
|
|
|
|
|
def _now_iso() -> str:
|
|
"""Return the current UTC time as an ISO-8601 string.
|
|
|
|
Returns:
|
|
e.g. ``'2026-03-20T12:34:56'``
|
|
"""
|
|
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S")
|
|
|
|
|
|
async def get_all_dashboards() -> List[Dict[str, Any]]:
|
|
"""Return all saved dashboards ordered by id.
|
|
|
|
Returns:
|
|
A list of dashboard dicts.
|
|
"""
|
|
db: aiosqlite.Connection = await get_db()
|
|
cursor = await db.execute("SELECT * FROM dashboards ORDER BY id")
|
|
rows = await cursor.fetchall()
|
|
return [_row_to_dict(r) for r in rows]
|
|
|
|
|
|
async def get_dashboard(dashboard_id: int) -> Optional[Dict[str, Any]]:
|
|
"""Fetch a single dashboard by its primary key.
|
|
|
|
Args:
|
|
dashboard_id: The id of the dashboard.
|
|
|
|
Returns:
|
|
A dashboard dict, or ``None`` if not found.
|
|
"""
|
|
db: aiosqlite.Connection = await get_db()
|
|
cursor = await db.execute(
|
|
"SELECT * FROM dashboards WHERE id = ?", (dashboard_id,)
|
|
)
|
|
row = await cursor.fetchone()
|
|
return _row_to_dict(row) if row else None
|
|
|
|
|
|
async def save_dashboard(name: str, layout_json: str) -> Dict[str, Any]:
|
|
"""Create a new dashboard.
|
|
|
|
Args:
|
|
name: Human-readable dashboard name.
|
|
layout_json: JSON string describing the widget layout.
|
|
|
|
Returns:
|
|
The newly created dashboard dict.
|
|
"""
|
|
db: aiosqlite.Connection = await get_db()
|
|
now = _now_iso()
|
|
|
|
cursor = await db.execute(
|
|
"""
|
|
INSERT INTO dashboards (name, layout_json, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?)
|
|
""",
|
|
(name, layout_json, now, now),
|
|
)
|
|
await db.commit()
|
|
|
|
new_id = cursor.lastrowid
|
|
result_cursor = await db.execute(
|
|
"SELECT * FROM dashboards WHERE id = ?", (new_id,)
|
|
)
|
|
row = await result_cursor.fetchone()
|
|
return _row_to_dict(row) # type: ignore[arg-type]
|
|
|
|
|
|
async def update_dashboard(
|
|
dashboard_id: int,
|
|
name: Optional[str] = None,
|
|
layout_json: Optional[str] = None,
|
|
) -> Dict[str, Any]:
|
|
"""Update an existing dashboard's name and/or layout.
|
|
|
|
At least one of *name* or *layout_json* must be provided.
|
|
``updated_at`` is set automatically.
|
|
|
|
Args:
|
|
dashboard_id: The id of the dashboard to update.
|
|
name: New dashboard name (or ``None`` to leave unchanged).
|
|
layout_json: New layout JSON (or ``None`` to leave unchanged).
|
|
|
|
Returns:
|
|
The updated dashboard dict.
|
|
|
|
Raises:
|
|
ValueError: If the dashboard does not exist or no fields given.
|
|
"""
|
|
fields: Dict[str, Any] = {}
|
|
if name is not None:
|
|
fields["name"] = name
|
|
if layout_json is not None:
|
|
fields["layout_json"] = layout_json
|
|
|
|
if not fields:
|
|
raise ValueError("At least one of 'name' or 'layout_json' must be provided")
|
|
|
|
fields["updated_at"] = _now_iso()
|
|
|
|
set_clause = ", ".join(f"{col} = ?" for col in fields)
|
|
values = list(fields.values()) + [dashboard_id]
|
|
|
|
db: aiosqlite.Connection = await get_db()
|
|
await db.execute(
|
|
f"UPDATE dashboards SET {set_clause} WHERE id = ?", # noqa: S608
|
|
values,
|
|
)
|
|
await db.commit()
|
|
|
|
cursor = await db.execute(
|
|
"SELECT * FROM dashboards WHERE id = ?", (dashboard_id,)
|
|
)
|
|
row = await cursor.fetchone()
|
|
if row is None:
|
|
raise ValueError(f"Dashboard with id={dashboard_id} not found")
|
|
return _row_to_dict(row)
|
|
|
|
|
|
async def delete_dashboard(dashboard_id: int) -> bool:
|
|
"""Delete a dashboard by id.
|
|
|
|
Args:
|
|
dashboard_id: The id to delete.
|
|
|
|
Returns:
|
|
``True`` if a row was deleted, ``False`` otherwise.
|
|
"""
|
|
db: aiosqlite.Connection = await get_db()
|
|
cursor = await db.execute(
|
|
"DELETE FROM dashboards WHERE id = ?", (dashboard_id,)
|
|
)
|
|
await db.commit()
|
|
return cursor.rowcount > 0
|