diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 500065d..1b86f40 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -46,10 +46,16 @@ jobs: python3 -m pip install --upgrade pip --break-system-packages if [ -f requirements.txt ]; then pip install -r requirements.txt --break-system-packages; fi if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt --break-system-packages; fi - pip install bandit pip-audit ruff --break-system-packages + # Install the project + dev tools so lint/type/test gates run for real + pip install -e ".[dev]" --break-system-packages + pip install bandit pip-audit --break-system-packages + # Lint, type-check, and tests are hard gates ā no `|| true`. - name: Ruff lint - run: ruff check . || true + run: ruff check src tests + + - name: Mypy + run: mypy src - name: Bandit (advisory) run: bandit -r . -q || true @@ -58,13 +64,7 @@ jobs: run: pip-audit -r requirements.txt 2>/dev/null || pip-audit 2>/dev/null || true - name: Pytest - run: | - if [ -d tests ] || ls test_*.py *_test.py 2>/dev/null; then - pip install pytest --break-system-packages - pytest -q || true - else - echo "No tests found ā skip" - fi + run: pytest -q secret-scan: needs: skip-ci-check diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 76a8590..132269b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,14 +34,15 @@ jobs: .venv/bin/pip install --upgrade pip .venv/bin/pip install -e ".[dev]" + # Linters are hard gates ā no `|| true`. - name: Run linters run: | echo "Running ruff..." - .venv/bin/ruff check src/ tests/ || true + .venv/bin/ruff check src/ tests/ echo "Running black check..." - .venv/bin/black --check src/ tests/ || true + .venv/bin/black --check src/ tests/ echo "Running mypy..." - .venv/bin/mypy src/ --install-types --non-interactive || true + .venv/bin/mypy src/ --install-types --non-interactive - name: Run tests with coverage env: diff --git a/README.md b/README.md index 1625651..808c35a 100644 --- a/README.md +++ b/README.md @@ -24,11 +24,11 @@ POTE tracks stock trading activity of government officials (starting with U.S. C ## Quick start -**š Already deployed?** See **[QUICKSTART.md](QUICKSTART.md)** for full usage guide! +**š Already deployed?** See **[QUICKSTART.md](docs/QUICKSTART.md)** for full usage guide! -**š¦ Deploying?** See **[PROXMOX_QUICKSTART.md](PROXMOX_QUICKSTART.md)** for Proxmox LXC deployment (recommended). +**š¦ Deploying?** See **[PROXMOX_QUICKSTART.md](docs/PROXMOX_QUICKSTART.md)** for Proxmox LXC deployment (recommended). -**š§ Want automated reports?** See **[AUTOMATION_QUICKSTART.md](AUTOMATION_QUICKSTART.md)** for email reporting setup! +**š§ Want automated reports?** See **[AUTOMATION_QUICKSTART.md](docs/AUTOMATION_QUICKSTART.md)** for email reporting setup! **š Homelab deploy (LXC 236)?** See **[docs/HANDOFF-2026-05-27.md](docs/HANDOFF-2026-05-27.md)** for ops handoff and next steps. @@ -81,14 +81,14 @@ docker-compose up -d **Getting Started**: - [`README.md`](README.md) ā This file -- [`QUICKSTART.md`](QUICKSTART.md) ā ā **How to use your deployed POTE instance** -- [`STATUS.md`](STATUS.md) ā Current project status -- [`FREE_TESTING_QUICKSTART.md`](FREE_TESTING_QUICKSTART.md) ā Test for $0 -- [`OFFLINE_DEMO.md`](OFFLINE_DEMO.md) ā Works without internet! +- [`QUICKSTART.md`](docs/QUICKSTART.md) ā ā **How to use your deployed POTE instance** +- [`STATUS.md`](docs/STATUS.md) ā Current project status +- [`FREE_TESTING_QUICKSTART.md`](docs/FREE_TESTING_QUICKSTART.md) ā Test for $0 +- [`OFFLINE_DEMO.md`](docs/OFFLINE_DEMO.md) ā Works without internet! **Deployment**: -- [`PROXMOX_QUICKSTART.md`](PROXMOX_QUICKSTART.md) ā ā **Proxmox quick deployment (5 min)** -- [`AUTOMATION_QUICKSTART.md`](AUTOMATION_QUICKSTART.md) ā ā **Automated reporting setup (5 min)** +- [`PROXMOX_QUICKSTART.md`](docs/PROXMOX_QUICKSTART.md) ā ā **Proxmox quick deployment (5 min)** +- [`AUTOMATION_QUICKSTART.md`](docs/AUTOMATION_QUICKSTART.md) ā ā **Automated reporting setup (5 min)** - [`docs/07_deployment.md`](docs/07_deployment.md) ā Full deployment guide (all platforms) - [`docs/08_proxmox_deployment.md`](docs/08_proxmox_deployment.md) ā Proxmox detailed guide - [`docs/12_automation_and_reporting.md`](docs/12_automation_and_reporting.md) ā Automation & CI/CD guide @@ -187,7 +187,7 @@ POTE now includes a complete 3-phase monitoring system: - Analyzes by ticker, sector, and political party - Generates comprehensive reports -**Full Documentation**: See [`MONITORING_SYSTEM_COMPLETE.md`](MONITORING_SYSTEM_COMPLETE.md) +**Full Documentation**: See [`MONITORING_SYSTEM_COMPLETE.md`](docs/MONITORING_SYSTEM_COMPLETE.md) ## Next Steps diff --git a/AUTOMATION_QUICKSTART.md b/docs/AUTOMATION_QUICKSTART.md similarity index 100% rename from AUTOMATION_QUICKSTART.md rename to docs/AUTOMATION_QUICKSTART.md diff --git a/DEPLOYMENT_AND_AUTOMATION.md b/docs/DEPLOYMENT_AND_AUTOMATION.md similarity index 100% rename from DEPLOYMENT_AND_AUTOMATION.md rename to docs/DEPLOYMENT_AND_AUTOMATION.md diff --git a/EMAIL_SETUP.md b/docs/EMAIL_SETUP.md similarity index 100% rename from EMAIL_SETUP.md rename to docs/EMAIL_SETUP.md diff --git a/FREE_TESTING_QUICKSTART.md b/docs/FREE_TESTING_QUICKSTART.md similarity index 100% rename from FREE_TESTING_QUICKSTART.md rename to docs/FREE_TESTING_QUICKSTART.md diff --git a/GITEA_SECRETS_GUIDE.md b/docs/GITEA_SECRETS_GUIDE.md similarity index 100% rename from GITEA_SECRETS_GUIDE.md rename to docs/GITEA_SECRETS_GUIDE.md diff --git a/docs/HANDOFF-2026-05-27.md b/docs/HANDOFF-2026-05-27.md index b101663..7deb440 100644 --- a/docs/HANDOFF-2026-05-27.md +++ b/docs/HANDOFF-2026-05-27.md @@ -150,9 +150,9 @@ make beszel-install-agents BESZEL_ONLY=pote-236 # if agent not yet installed | Doc | Purpose | |-----|---------| -| [EMAIL_SETUP.md](../EMAIL_SETUP.md) | SMTP / Mailcow / levkine.ca | -| [AUTOMATION_QUICKSTART.md](../AUTOMATION_QUICKSTART.md) | Cron + reports | -| [PROXMOX_QUICKSTART.md](../PROXMOX_QUICKSTART.md) | Original LXC provisioning | +| [EMAIL_SETUP.md](EMAIL_SETUP.md) | SMTP / Mailcow / levkine.ca | +| [AUTOMATION_QUICKSTART.md](AUTOMATION_QUICKSTART.md) | Cron + reports | +| [PROXMOX_QUICKSTART.md](PROXMOX_QUICKSTART.md) | Original LXC provisioning | | Ansible `docs/guides/projects-handoff-2026-05-26.md` | Multi-project homelab context | | Ansible `docs/guides/smtp-inventory.md` | Mailboxes | diff --git a/LOCAL_TEST_GUIDE.md b/docs/LOCAL_TEST_GUIDE.md similarity index 100% rename from LOCAL_TEST_GUIDE.md rename to docs/LOCAL_TEST_GUIDE.md diff --git a/MONITORING_SYSTEM_COMPLETE.md b/docs/MONITORING_SYSTEM_COMPLETE.md similarity index 100% rename from MONITORING_SYSTEM_COMPLETE.md rename to docs/MONITORING_SYSTEM_COMPLETE.md diff --git a/OFFLINE_DEMO.md b/docs/OFFLINE_DEMO.md similarity index 100% rename from OFFLINE_DEMO.md rename to docs/OFFLINE_DEMO.md diff --git a/PROXMOX_QUICKSTART.md b/docs/PROXMOX_QUICKSTART.md similarity index 100% rename from PROXMOX_QUICKSTART.md rename to docs/PROXMOX_QUICKSTART.md diff --git a/QUICKSTART.md b/docs/QUICKSTART.md similarity index 100% rename from QUICKSTART.md rename to docs/QUICKSTART.md diff --git a/QUICK_SETUP_CARD.md b/docs/QUICK_SETUP_CARD.md similarity index 100% rename from QUICK_SETUP_CARD.md rename to docs/QUICK_SETUP_CARD.md diff --git a/STATUS.md b/docs/STATUS.md similarity index 100% rename from STATUS.md rename to docs/STATUS.md diff --git a/TESTING_STATUS.md b/docs/TESTING_STATUS.md similarity index 100% rename from TESTING_STATUS.md rename to docs/TESTING_STATUS.md diff --git a/WATCHLIST_GUIDE.md b/docs/WATCHLIST_GUIDE.md similarity index 100% rename from WATCHLIST_GUIDE.md rename to docs/WATCHLIST_GUIDE.md diff --git a/pyproject.toml b/pyproject.toml index 8259445..0cb7c18 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,8 @@ where = ["src"] [tool.ruff] line-length = 100 target-version = "py311" + +[tool.ruff.lint] select = ["E", "F", "W", "I", "N", "UP", "B", "A", "C4", "SIM", "RET"] ignore = ["E501"] # Line too long (handled by black) diff --git a/scripts/monitor_market.py b/scripts/monitor_market.py index 9bcca90..29df27e 100755 --- a/scripts/monitor_market.py +++ b/scripts/monitor_market.py @@ -74,7 +74,7 @@ def main(tickers, interval, once, min_severity, save_report, lookback): if filtered: # Generate report - report = alert_mgr.generate_summary_report(filtered, format="text") + report = alert_mgr.generate_summary_report(filtered, output_format="text") print("\n" + report) # Save report if requested diff --git a/src/pote/analytics/__init__.py b/src/pote/analytics/__init__.py index d2639b7..4b56ed6 100644 --- a/src/pote/analytics/__init__.py +++ b/src/pote/analytics/__init__.py @@ -2,14 +2,12 @@ Analytics module for calculating returns, performance metrics, and signals. """ -from .returns import ReturnCalculator from .benchmarks import BenchmarkComparison from .metrics import PerformanceMetrics +from .returns import ReturnCalculator __all__ = [ "ReturnCalculator", "BenchmarkComparison", "PerformanceMetrics", ] - - diff --git a/src/pote/analytics/benchmarks.py b/src/pote/analytics/benchmarks.py index 568904d..23959e2 100644 --- a/src/pote/analytics/benchmarks.py +++ b/src/pote/analytics/benchmarks.py @@ -3,7 +3,7 @@ Benchmark comparison for calculating abnormal returns (alpha). """ import logging -from datetime import date, timedelta +from datetime import date from decimal import Decimal from sqlalchemy.orm import Session @@ -60,8 +60,7 @@ class BenchmarkComparison: return None # Calculate return - return_pct = ((end_price - start_price) / start_price) * 100 - return return_pct + return ((end_price - start_price) / start_price) * 100 def calculate_abnormal_return( self, @@ -219,5 +218,3 @@ class BenchmarkComparison: "benchmark": self.BENCHMARKS.get(benchmark, benchmark), "window_days": window_days, } - - diff --git a/src/pote/analytics/metrics.py b/src/pote/analytics/metrics.py index dca08df..db71bbb 100644 --- a/src/pote/analytics/metrics.py +++ b/src/pote/analytics/metrics.py @@ -4,7 +4,6 @@ Performance metrics and aggregations. import logging from collections import defaultdict -from datetime import date from sqlalchemy import func from sqlalchemy.orm import Session @@ -52,11 +51,7 @@ class PerformanceMetrics: if not official: return {"error": "Official not found"} - trades = ( - self.session.query(Trade) - .filter(Trade.official_id == official_id) - .all() - ) + trades = self.session.query(Trade).filter(Trade.official_id == official_id).all() if not trades: return { @@ -70,9 +65,7 @@ class PerformanceMetrics: # Calculate returns for all trades returns_data = [] for trade in trades: - result = self.benchmark.compare_trade_to_benchmark( - trade, window_days, benchmark - ) + result = self.benchmark.compare_trade_to_benchmark(trade, window_days, benchmark) if result: returns_data.append(result) @@ -96,9 +89,7 @@ class PerformanceMetrics: worst_trade = min(returns_data, key=lambda x: x["trade_return"]) # Total value traded - total_value = sum( - float(t.value_min or 0) for t in trades if t.value_min - ) + total_value = sum(float(t.value_min or 0) for t in trades if t.value_min) return { "name": official.name, @@ -154,20 +145,14 @@ class PerformanceMetrics: List of sector performance dictionaries """ # Get all trades with security info - trades = ( - self.session.query(Trade) - .join(Security) - .all() - ) + trades = self.session.query(Trade).join(Security).all() # Group by sector sector_data = defaultdict(list) for trade in trades: sector = trade.security.sector or "Unknown" - result = self.benchmark.compare_trade_to_benchmark( - trade, window_days, benchmark - ) + result = self.benchmark.compare_trade_to_benchmark(trade, window_days, benchmark) if result: sector_data[sector].append(result) @@ -180,14 +165,16 @@ class PerformanceMetrics: returns = [d["trade_return"] for d in data] alphas = [d["abnormal_return"] for d in data] - results.append({ - "sector": sector, - "trade_count": len(data), - "avg_return": sum(returns) / len(returns), - "avg_alpha": sum(alphas) / len(alphas), - "win_rate": sum(1 for r in returns if r > 0) / len(returns), - "beat_market_rate": sum(1 for a in alphas if a > 0) / len(alphas), - }) + results.append( + { + "sector": sector, + "trade_count": len(data), + "avg_return": sum(returns) / len(returns), + "avg_alpha": sum(alphas) / len(alphas), + "win_rate": sum(1 for r in returns if r > 0) / len(returns), + "beat_market_rate": sum(1 for a in alphas if a > 0) / len(alphas), + } + ) # Sort by average alpha results.sort(key=lambda x: x["avg_alpha"], reverse=True) @@ -229,11 +216,7 @@ class PerformanceMetrics: Returns: Dictionary with timing statistics """ - trades = ( - self.session.query(Trade) - .filter(Trade.filing_date.isnot(None)) - .all() - ) + trades = self.session.query(Trade).filter(Trade.filing_date.isnot(None)).all() if not trades: return {"error": "No trades with disclosure dates"} @@ -288,5 +271,3 @@ class PerformanceMetrics: "benchmark": benchmark, **aggregate, } - - diff --git a/src/pote/analytics/returns.py b/src/pote/analytics/returns.py index 75d45d1..4e9701b 100644 --- a/src/pote/analytics/returns.py +++ b/src/pote/analytics/returns.py @@ -94,18 +94,20 @@ class ReturnCalculator: def calculate_multiple_windows( self, trade: Trade, - windows: list[int] = [30, 60, 90, 180], + windows: list[int] | None = None, ) -> dict[int, dict]: """ Calculate returns for multiple time windows. Args: trade: Trade object - windows: List of window sizes in days + windows: List of window sizes in days (defaults to 30/60/90/180) Returns: Dictionary mapping window_days to return metrics """ + if windows is None: + windows = [30, 60, 90, 180] results = {} for window in windows: result = self.calculate_trade_return(trade, window) @@ -223,12 +225,13 @@ class ReturnCalculator: if not prices: return pd.DataFrame() + # open/high/low are nullable columns; use NaN (pandas-native) when absent. data = [ { "date": p.date, - "open": float(p.open), - "high": float(p.high), - "low": float(p.low), + "open": float(p.open) if p.open is not None else float("nan"), + "high": float(p.high) if p.high is not None else float("nan"), + "low": float(p.low) if p.low is not None else float("nan"), "close": float(p.close), "volume": p.volume, } @@ -236,4 +239,3 @@ class ReturnCalculator: ] return pd.DataFrame(data) - diff --git a/src/pote/db/models.py b/src/pote/db/models.py index 03739c0..5d90dc3 100644 --- a/src/pote/db/models.py +++ b/src/pote/db/models.py @@ -3,17 +3,17 @@ SQLAlchemy ORM models for POTE. Matches the schema defined in docs/02_data_model.md. """ -from datetime import date, datetime, timezone +from datetime import UTC, date, datetime from decimal import Decimal from sqlalchemy import ( DECIMAL, + JSON, Date, DateTime, ForeignKey, Index, Integer, - JSON, String, Text, UniqueConstraint, @@ -35,13 +35,11 @@ class Official(Base): state: Mapped[str | None] = mapped_column(String(2)) bioguide_id: Mapped[str | None] = mapped_column(String(20), unique=True) external_ids: Mapped[str | None] = mapped_column(Text) # JSON blob for other IDs - created_at: Mapped[datetime] = mapped_column( - DateTime, default=lambda: datetime.now(timezone.utc) - ) + created_at: Mapped[datetime] = mapped_column(DateTime, default=lambda: datetime.now(UTC)) updated_at: Mapped[datetime] = mapped_column( DateTime, - default=lambda: datetime.now(timezone.utc), - onupdate=lambda: datetime.now(timezone.utc), + default=lambda: datetime.now(UTC), + onupdate=lambda: datetime.now(UTC), ) # Relationships @@ -63,13 +61,11 @@ class Security(Base): sector: Mapped[str | None] = mapped_column(String(100)) industry: Mapped[str | None] = mapped_column(String(100)) asset_type: Mapped[str] = mapped_column(String(50), default="stock") # stock, bond, etc. - created_at: Mapped[datetime] = mapped_column( - DateTime, default=lambda: datetime.now(timezone.utc) - ) + created_at: Mapped[datetime] = mapped_column(DateTime, default=lambda: datetime.now(UTC)) updated_at: Mapped[datetime] = mapped_column( DateTime, - default=lambda: datetime.now(timezone.utc), - onupdate=lambda: datetime.now(timezone.utc), + default=lambda: datetime.now(UTC), + onupdate=lambda: datetime.now(UTC), ) # Relationships @@ -107,13 +103,11 @@ class Trade(Base): # Quality flags (JSON or enum list) quality_flags: Mapped[str | None] = mapped_column(Text) # e.g., "range_only,delayed_filing" - created_at: Mapped[datetime] = mapped_column( - DateTime, default=lambda: datetime.now(timezone.utc) - ) + created_at: Mapped[datetime] = mapped_column(DateTime, default=lambda: datetime.now(UTC)) updated_at: Mapped[datetime] = mapped_column( DateTime, - default=lambda: datetime.now(timezone.utc), - onupdate=lambda: datetime.now(timezone.utc), + default=lambda: datetime.now(UTC), + onupdate=lambda: datetime.now(UTC), ) # Relationships @@ -156,9 +150,7 @@ class Price(Base): adjusted_close: Mapped[Decimal | None] = mapped_column(DECIMAL(15, 4)) source: Mapped[str] = mapped_column(String(50), default="yfinance") - created_at: Mapped[datetime] = mapped_column( - DateTime, default=lambda: datetime.now(timezone.utc) - ) + created_at: Mapped[datetime] = mapped_column(DateTime, default=lambda: datetime.now(UTC)) # Relationships security: Mapped["Security"] = relationship("Security", back_populates="prices") @@ -188,9 +180,7 @@ class MetricOfficial(Base): avg_abnormal_return_1m: Mapped[Decimal | None] = mapped_column(DECIMAL(10, 6)) cluster_label: Mapped[str | None] = mapped_column(String(50)) - created_at: Mapped[datetime] = mapped_column( - DateTime, default=lambda: datetime.now(timezone.utc) - ) + created_at: Mapped[datetime] = mapped_column(DateTime, default=lambda: datetime.now(UTC)) __table_args__ = ( UniqueConstraint("official_id", "calc_date", "calc_version", name="uq_metrics_official"), @@ -212,9 +202,7 @@ class MetricTrade(Base): abnormal_return_1m: Mapped[Decimal | None] = mapped_column(DECIMAL(10, 6)) signal_flags: Mapped[str | None] = mapped_column(Text) # JSON list - created_at: Mapped[datetime] = mapped_column( - DateTime, default=lambda: datetime.now(timezone.utc) - ) + created_at: Mapped[datetime] = mapped_column(DateTime, default=lambda: datetime.now(UTC)) __table_args__ = ( UniqueConstraint("trade_id", "calc_date", "calc_version", name="uq_metrics_trade"), @@ -242,18 +230,14 @@ class MarketAlert(Base): # Metrics at time of alert price: Mapped[Decimal | None] = mapped_column(DECIMAL(15, 4)) volume: Mapped[int | None] = mapped_column(Integer) - change_pct: Mapped[Decimal | None] = mapped_column( - DECIMAL(10, 4) - ) # Price change % + change_pct: Mapped[Decimal | None] = mapped_column(DECIMAL(10, 4)) # Price change % # Severity scoring severity: Mapped[int | None] = mapped_column(Integer) # 1-10 scale # Metadata source: Mapped[str] = mapped_column(String(50), default="market_monitor") - created_at: Mapped[datetime] = mapped_column( - DateTime, default=lambda: datetime.now(timezone.utc) - ) + created_at: Mapped[datetime] = mapped_column(DateTime, default=lambda: datetime.now(UTC)) # Indexes for efficient queries __table_args__ = ( diff --git a/src/pote/ingestion/house_watcher.py b/src/pote/ingestion/house_watcher.py index ee5e745..ab3880a 100644 --- a/src/pote/ingestion/house_watcher.py +++ b/src/pote/ingestion/house_watcher.py @@ -14,6 +14,7 @@ import httpx logger = logging.getLogger(__name__) + def _default_data_urls() -> tuple[str, ...]: override = os.environ.get("POTE_HOUSE_DATA_URL", "").strip() if override: @@ -235,10 +236,9 @@ def normalize_transaction_type(txn_type: str) -> str: if "purchase" in txn_lower or "buy" in txn_lower: return "buy" - elif "sale" in txn_lower or "sell" in txn_lower: + if "sale" in txn_lower or "sell" in txn_lower: return "sell" - elif "exchange" in txn_lower: + if "exchange" in txn_lower: return "exchange" - else: - # Default to the original, lowercased - return txn_lower + # Default to the original, lowercased + return txn_lower diff --git a/src/pote/ingestion/prices.py b/src/pote/ingestion/prices.py index 56345a2..463e351 100644 --- a/src/pote/ingestion/prices.py +++ b/src/pote/ingestion/prices.py @@ -4,7 +4,7 @@ Fetches daily OHLCV data for securities and stores in the prices table. """ import logging -from datetime import date, datetime, timedelta, timezone +from datetime import UTC, date, datetime, timedelta from decimal import Decimal import pandas as pd @@ -158,7 +158,7 @@ class PriceLoader: "volume": int(row["volume"]) if pd.notna(row.get("volume")) else None, "adjusted_close": None, # We'll compute this later if needed "source": "yfinance", - "created_at": datetime.now(timezone.utc), + "created_at": datetime.now(UTC), } records.append(record) diff --git a/src/pote/monitoring/__init__.py b/src/pote/monitoring/__init__.py index 4c794a4..7205f97 100644 --- a/src/pote/monitoring/__init__.py +++ b/src/pote/monitoring/__init__.py @@ -9,4 +9,3 @@ from .market_monitor import MarketMonitor from .pattern_detector import PatternDetector __all__ = ["MarketMonitor", "AlertManager", "DisclosureCorrelator", "PatternDetector"] - diff --git a/src/pote/monitoring/alert_manager.py b/src/pote/monitoring/alert_manager.py index c56117e..1f36c52 100644 --- a/src/pote/monitoring/alert_manager.py +++ b/src/pote/monitoring/alert_manager.py @@ -4,8 +4,7 @@ Handles alert filtering, formatting, and delivery. """ import logging -from datetime import datetime, timezone -from typing import Any +from datetime import UTC, datetime from sqlalchemy.orm import Session @@ -75,21 +74,24 @@ class AlertManager: Returns: HTML formatted alert """ - severity_class = "high" if (alert.severity or 0) >= 7 else "medium" if (alert.severity or 0) >= 4 else "low" + severity_class = ( + "high" + if (alert.severity or 0) >= 7 + else "medium" if (alert.severity or 0) >= 4 else "low" + ) - html = f""" + return f"""
Severity: {alert.severity}/10
{len(alerts)} Alerts | {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')} UTC
", + "{len(alerts)} Alerts | {datetime.now(UTC).strftime('%Y-%m-%d %H:%M:%S')} UTC
", ] for alert in sorted(alerts, key=lambda a: a.severity or 0, reverse=True): @@ -241,5 +238,3 @@ class AlertManager: html_parts.append("