Split signal scoring, add Redis cache, Tailwind design system, and fix UI coherence issues
Backend: extract pure scoring logic from signal_service.py into signal_scoring.py (h), add Redis-backed win-rate/PnL caching with graceful degradation (l), add Postgres backup/restore scripts (n), move DB/encryption secrets to Docker secrets pattern (o), fix RSI flat-price bug and MFI wraparound index bug (q, r). 134 backend tests passing. Frontend: consolidate all API calls onto shared apiFetch with auto token refresh (i), wire AnalyticsPage to the real /analytics/dashboard endpoint instead of fake random data (j), migrate all pages and shared components to a Tailwind CSS design system (k) fixing 3 mismatched color palettes found along the way. UI review also found and fixed missing mobile table scroll wrappers, non-stacking grids, and a missing nav/logout bar on ProfilePage. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
"""Shared async Redis client, used to cache state across the backend-api and
|
||||
backend-scheduler processes (e.g. signal_booster win-rate/PnL stats), which
|
||||
are separate containers and can't share Python module-level globals.
|
||||
|
||||
Every helper here degrades gracefully: if Redis is unreachable (not
|
||||
configured for local dev, container down, network hiccup), callers get
|
||||
``None``/a cache miss instead of an exception, and are expected to fall back
|
||||
to a per-process in-memory cache.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from typing import Any, Optional
|
||||
|
||||
from redis import asyncio as redis_asyncio
|
||||
|
||||
from app.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_client: Optional["redis_asyncio.Redis"] = None
|
||||
_failed_until: float = 0.0 # monotonic timestamp; skip Redis until past this
|
||||
_RETRY_COOLDOWN_SECONDS = 30
|
||||
|
||||
|
||||
def _get_client() -> Optional["redis_asyncio.Redis"]:
|
||||
"""Lazily create the Redis client. Returns None while within the retry
|
||||
cooldown after a recent failure, so a Redis outage costs at most one
|
||||
slow connect attempt per cooldown window instead of stalling every call."""
|
||||
global _client
|
||||
if time.monotonic() < _failed_until:
|
||||
return None
|
||||
if _client is None:
|
||||
_client = redis_asyncio.from_url(
|
||||
settings.REDIS_URL, decode_responses=True, socket_connect_timeout=2,
|
||||
)
|
||||
return _client
|
||||
|
||||
|
||||
def _mark_failed(exc: Exception, operation: str, key: str) -> None:
|
||||
global _failed_until
|
||||
logger.warning(
|
||||
"Redis %s(%s) failed, falling back to in-memory cache for %ds: %s",
|
||||
operation, key, _RETRY_COOLDOWN_SECONDS, exc,
|
||||
)
|
||||
_failed_until = time.monotonic() + _RETRY_COOLDOWN_SECONDS
|
||||
|
||||
|
||||
async def get_json(key: str) -> Any | None:
|
||||
"""Return the JSON-decoded value for ``key``, or None on miss/error."""
|
||||
client = _get_client()
|
||||
if client is None:
|
||||
return None
|
||||
try:
|
||||
raw = await client.get(key)
|
||||
return json.loads(raw) if raw is not None else None
|
||||
except Exception as exc:
|
||||
_mark_failed(exc, "get_json", key)
|
||||
return None
|
||||
|
||||
|
||||
async def set_json(key: str, value: Any, ttl_seconds: int) -> None:
|
||||
"""Store ``value`` as JSON under ``key`` with an expiry. Silently no-ops
|
||||
on failure — callers should already be maintaining their own in-memory
|
||||
fallback, so a failed cache write here is not fatal."""
|
||||
client = _get_client()
|
||||
if client is None:
|
||||
return
|
||||
try:
|
||||
await client.set(key, json.dumps(value), ex=ttl_seconds)
|
||||
except Exception as exc:
|
||||
_mark_failed(exc, "set_json", key)
|
||||
Reference in New Issue
Block a user