Add cache source tracking (L1/L2) for Telegram bot responses

Implements cache tier identification in Telegram bot to display data source:
- "db" for database queries
- "cached L1" for in-memory cache hits
- "cached L2" for SQLite cache hits

Backend changes:
- Added cache metadata fields to TrendsResponse and DashboardSummary models
  (cache_hit, response_time_ms, cache_source)
- Updated /api/dashboard/summary and /api/dashboard/trends endpoints to
  include cache metadata when X-Include-Cache-Metadata header is present
- Cache metadata is extracted from request.state (set by @cached decorator)

Telegram bot changes:
- Updated API client to send X-Include-Cache-Metadata header
- Modified helpers to extract cache_source from backend responses
- Updated handlers to pass cache metadata to formatters
- Performance footer now displays specific cache tier (L1 vs L2)

Fixed Pydantic serialization issue:
- Changed field names from _cache_hit to cache_hit (without underscore)
- Pydantic excludes underscore-prefixed fields from JSON by default

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-07 22:39:09 +02:00
parent 87bd04e3ff
commit 2a37959d80
5 changed files with 759 additions and 60 deletions

View File

@@ -234,15 +234,20 @@ class BackendAPIClient:
Returns:
Dict with dashboard data (sold_total, facturi, plati, etc.)
Includes _cache_hit and _response_time_ms metadata
"""
try:
if not self.client:
self.client = AsyncClient(base_url=self.base_url, timeout=REQUEST_TIMEOUT)
# Add cache metadata header for Telegram Bot
headers = self._get_auth_headers(jwt_token)
headers['X-Include-Cache-Metadata'] = 'true'
response = await self.client.get(
"/api/dashboard/summary",
params={"company": str(company_id)},
headers=self._get_auth_headers(jwt_token)
headers=headers
)
return await self._handle_response(response)
@@ -270,9 +275,13 @@ class BackendAPIClient:
if not self.client:
self.client = AsyncClient(base_url=self.base_url, timeout=REQUEST_TIMEOUT)
# Add cache metadata header for Telegram Bot
headers = self._get_auth_headers(jwt_token)
headers['X-Include-Cache-Metadata'] = 'true'
response = await self.client.get(
f"/api/dashboard/treasury-breakdown?company={company_id}",
headers=self._get_auth_headers(jwt_token)
headers=headers
)
return await self._handle_response(response)
@@ -302,9 +311,13 @@ class BackendAPIClient:
if not self.client:
self.client = AsyncClient(base_url=self.base_url, timeout=REQUEST_TIMEOUT)
# Add cache metadata header for Telegram Bot
headers = self._get_auth_headers(jwt_token)
headers['X-Include-Cache-Metadata'] = 'true'
response = await self.client.get(
f"/api/dashboard/detailed-data?company={company_id}&data_type={data_type}",
headers=self._get_auth_headers(jwt_token)
headers=headers
)
return await self._handle_response(response)
@@ -334,9 +347,13 @@ class BackendAPIClient:
if not self.client:
self.client = AsyncClient(base_url=self.base_url, timeout=REQUEST_TIMEOUT)
# Add cache metadata header for Telegram Bot
headers = self._get_auth_headers(jwt_token)
headers['X-Include-Cache-Metadata'] = 'true'
response = await self.client.get(
f"/api/dashboard/maturity?company={company_id}&period={period}",
headers=self._get_auth_headers(jwt_token)
headers=headers
)
return await self._handle_response(response)
@@ -364,9 +381,13 @@ class BackendAPIClient:
if not self.client:
self.client = AsyncClient(base_url=self.base_url, timeout=REQUEST_TIMEOUT)
# Add cache metadata header for Telegram Bot
headers = self._get_auth_headers(jwt_token)
headers['X-Include-Cache-Metadata'] = 'true'
response = await self.client.get(
f"/api/dashboard/performance?company={company_id}",
headers=self._get_auth_headers(jwt_token)
headers=headers
)
return await self._handle_response(response)
@@ -396,9 +417,13 @@ class BackendAPIClient:
if not self.client:
self.client = AsyncClient(base_url=self.base_url, timeout=REQUEST_TIMEOUT)
# Add cache metadata header for Telegram Bot
headers = self._get_auth_headers(jwt_token)
headers['X-Include-Cache-Metadata'] = 'true'
response = await self.client.get(
f"/api/dashboard/monthly-flows?company={company_id}&months={months}",
headers=self._get_auth_headers(jwt_token)
headers=headers
)
return await self._handle_response(response)
@@ -428,9 +453,13 @@ class BackendAPIClient:
if not self.client:
self.client = AsyncClient(base_url=self.base_url, timeout=REQUEST_TIMEOUT)
# Add cache metadata header for Telegram Bot
headers = self._get_auth_headers(jwt_token)
headers['X-Include-Cache-Metadata'] = 'true'
response = await self.client.get(
f"/api/dashboard/trends?company={company_id}&period={period}",
headers=self._get_auth_headers(jwt_token)
headers=headers
)
return await self._handle_response(response)
@@ -476,9 +505,6 @@ class BackendAPIClient:
if filters:
params.update(filters)
# ⚠️ DEBUGGING: Log exact parameters being sent
logger.info(f"📤 Searching invoices with params: {params}")
response = await self.client.get(
"/api/invoices/",
params=params,
@@ -487,13 +513,10 @@ class BackendAPIClient:
data = await self._handle_response(response)
# ⚠️ DEBUGGING: Log response
if isinstance(data, dict) and 'invoices' in data:
invoice_list = data['invoices']
logger.info(f"📥 Received {len(invoice_list)} invoices from backend")
return invoice_list
elif isinstance(data, list):
logger.info(f"📥 Received {len(data)} invoices from backend (direct list)")
return data
else:
logger.warning(f"📥 Unexpected response format: {type(data)}")
@@ -626,6 +649,113 @@ class BackendAPIClient:
logger.error(f"Failed to export report: {e}")
return None
# =========================================================================
# CACHE MANAGEMENT
# =========================================================================
async def invalidate_cache(
self,
jwt_token: str,
company_id: Optional[int] = None,
cache_type: Optional[str] = None
) -> bool:
"""
Invalidate cache entries.
Args:
jwt_token: JWT access token
company_id: Optional company ID (None = all companies)
cache_type: Optional cache type (None = all types)
Returns:
bool: True if successful
"""
try:
if not self.client:
self.client = AsyncClient(base_url=self.base_url, timeout=REQUEST_TIMEOUT)
request_data = {}
if company_id is not None:
request_data['company_id'] = company_id
if cache_type is not None:
request_data['cache_type'] = cache_type
response = await self.client.post(
"/api/cache/invalidate",
json=request_data,
headers=self._get_auth_headers(jwt_token)
)
response.raise_for_status()
logger.info(f"Cache invalidated: company_id={company_id}, cache_type={cache_type}")
return True
except Exception as e:
logger.error(f"Failed to invalidate cache: {e}")
return False
async def toggle_user_cache(
self,
jwt_token: str,
enabled: bool
) -> bool:
"""
Toggle cache for current user.
Args:
jwt_token: JWT access token
enabled: True to enable cache, False to disable
Returns:
bool: True if successful
"""
try:
if not self.client:
self.client = AsyncClient(base_url=self.base_url, timeout=REQUEST_TIMEOUT)
response = await self.client.post(
"/api/cache/toggle-user",
json={"enabled": enabled},
headers=self._get_auth_headers(jwt_token)
)
response.raise_for_status()
logger.info(f"User cache toggled: enabled={enabled}")
return True
except Exception as e:
logger.error(f"Failed to toggle user cache: {e}")
return False
async def get_cache_stats(
self,
jwt_token: str
) -> Optional[Dict[str, Any]]:
"""
Get cache statistics including user-specific settings.
Args:
jwt_token: JWT access token
Returns:
Dict with cache stats including 'user_enabled' field
"""
try:
if not self.client:
self.client = AsyncClient(base_url=self.base_url, timeout=REQUEST_TIMEOUT)
response = await self.client.get(
"/api/cache/stats",
headers=self._get_auth_headers(jwt_token)
)
response.raise_for_status()
return response.json()
except Exception as e:
logger.error(f"Failed to get cache stats: {e}")
return None
# =========================================================================
# HEALTH CHECK
# =========================================================================