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:
Le
2026-07-04 00:27:10 +07:00
parent 4a130363ec
commit 9de41ea92b
43 changed files with 2801 additions and 2800 deletions
+74
View File
@@ -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)