Consolidate 3 separate applications (reports-app, data-entry-app, telegram-bot) into a unified
architecture with single backend and frontend:
Backend Changes:
- Unified FastAPI backend at backend/ with modular structure
- Modules: reports, data_entry, telegram in backend/modules/
- Centralized config.py and main.py with all routers registered
- Single worker mode (--workers 1) for Telegram bot compatibility
- Shared Oracle connection pool and JWT authentication
- Unified requirements.txt and environment configuration
Frontend Changes:
- Single Vue.js SPA with module-based routing
- Unified frontend at src/ with modules in src/modules/{reports,data-entry}/
- Shared components and stores in src/shared/
- Error boundaries for module isolation
- Dual API proxy in Vite for module communication
Infrastructure:
- New unified startup scripts: start-prod.sh, start-test.sh, start-backend.sh
- Environment templates: .env.dev.example, .env.test.example, .env.prod.example
- Updated deployment scripts for Windows IIS
- Simplified SSH tunnel management
Documentation:
- Comprehensive CLAUDE.md with architecture overview
- Module-specific docs in docs/{data-entry,telegram}/
- Architecture decision records in docs/ARCHITECTURE-DECISIONS.md
- Deployment guides consolidated in deployment/windows/docs/
This migration reduces complexity, improves maintainability, and enables easier
deployment while maintaining all existing functionality.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
181 lines
4.9 KiB
Python
181 lines
4.9 KiB
Python
"""
|
|
In-memory cache with TTL (L1 cache)
|
|
Fast, limited size, lost on restart
|
|
"""
|
|
import time
|
|
import logging
|
|
from typing import Any, Optional, Dict
|
|
from collections import OrderedDict
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class MemoryCache:
|
|
"""
|
|
In-memory LRU cache with TTL support
|
|
|
|
Features:
|
|
- LRU eviction when max_size reached
|
|
- Per-entry TTL expiration
|
|
- Thread-safe operations
|
|
- Fast O(1) get/set operations
|
|
"""
|
|
|
|
def __init__(self, max_size: int = 1000):
|
|
"""
|
|
Initialize memory cache
|
|
|
|
Args:
|
|
max_size: Maximum number of entries to store
|
|
"""
|
|
self.max_size = max_size
|
|
self._cache: OrderedDict[str, Dict[str, Any]] = OrderedDict()
|
|
self._stats = {
|
|
'hits': 0,
|
|
'misses': 0,
|
|
'sets': 0,
|
|
'evictions': 0
|
|
}
|
|
|
|
async def get(self, key: str) -> Optional[Any]:
|
|
"""
|
|
Get value from cache
|
|
|
|
Args:
|
|
key: Cache key
|
|
|
|
Returns:
|
|
Cached value or None if not found/expired
|
|
"""
|
|
if key not in self._cache:
|
|
self._stats['misses'] += 1
|
|
return None
|
|
|
|
entry = self._cache[key]
|
|
|
|
# Check TTL expiration
|
|
if entry['expires_at'] < time.time():
|
|
# Expired - remove and return None
|
|
del self._cache[key]
|
|
self._stats['misses'] += 1
|
|
logger.debug(f"Memory cache expired: {key}")
|
|
return None
|
|
|
|
# Move to end (LRU - most recently used)
|
|
self._cache.move_to_end(key)
|
|
|
|
self._stats['hits'] += 1
|
|
logger.debug(f"Memory cache HIT: {key}")
|
|
return entry['value']
|
|
|
|
async def set(self, key: str, value: Any, ttl: int):
|
|
"""
|
|
Set value in cache
|
|
|
|
Args:
|
|
key: Cache key
|
|
value: Value to cache
|
|
ttl: Time to live in seconds
|
|
"""
|
|
expires_at = time.time() + ttl
|
|
|
|
# Check if we need to evict (LRU)
|
|
if key not in self._cache and len(self._cache) >= self.max_size:
|
|
# Evict oldest entry (first item in OrderedDict)
|
|
evicted_key = next(iter(self._cache))
|
|
del self._cache[evicted_key]
|
|
self._stats['evictions'] += 1
|
|
logger.debug(f"Memory cache evicted (LRU): {evicted_key}")
|
|
|
|
# Store entry
|
|
self._cache[key] = {
|
|
'value': value,
|
|
'expires_at': expires_at,
|
|
'created_at': time.time()
|
|
}
|
|
|
|
# Move to end (most recently used)
|
|
self._cache.move_to_end(key)
|
|
|
|
self._stats['sets'] += 1
|
|
logger.debug(f"Memory cache SET: {key} (TTL: {ttl}s)")
|
|
|
|
async def delete(self, key: str) -> bool:
|
|
"""
|
|
Delete entry from cache
|
|
|
|
Args:
|
|
key: Cache key
|
|
|
|
Returns:
|
|
True if deleted, False if not found
|
|
"""
|
|
if key in self._cache:
|
|
del self._cache[key]
|
|
logger.debug(f"Memory cache deleted: {key}")
|
|
return True
|
|
return False
|
|
|
|
async def clear(self):
|
|
"""Clear all entries from cache"""
|
|
count = len(self._cache)
|
|
self._cache.clear()
|
|
logger.info(f"Memory cache cleared: {count} entries removed")
|
|
|
|
async def clear_by_pattern(self, pattern: str):
|
|
"""
|
|
Clear entries matching pattern (simple prefix match)
|
|
|
|
Args:
|
|
pattern: Key prefix to match (e.g., "dashboard_summary:123")
|
|
"""
|
|
keys_to_delete = [key for key in self._cache.keys() if key.startswith(pattern)]
|
|
for key in keys_to_delete:
|
|
del self._cache[key]
|
|
|
|
logger.info(f"Memory cache cleared by pattern '{pattern}': {len(keys_to_delete)} entries")
|
|
|
|
async def cleanup_expired(self):
|
|
"""Remove all expired entries"""
|
|
now = time.time()
|
|
expired_keys = [
|
|
key for key, entry in self._cache.items()
|
|
if entry['expires_at'] < now
|
|
]
|
|
|
|
for key in expired_keys:
|
|
del self._cache[key]
|
|
|
|
if expired_keys:
|
|
logger.info(f"Memory cache cleanup: {len(expired_keys)} expired entries removed")
|
|
|
|
def get_stats(self) -> Dict[str, Any]:
|
|
"""
|
|
Get cache statistics
|
|
|
|
Returns:
|
|
Dictionary with stats (hits, misses, size, etc.)
|
|
"""
|
|
total_requests = self._stats['hits'] + self._stats['misses']
|
|
hit_rate = (self._stats['hits'] / total_requests * 100) if total_requests > 0 else 0
|
|
|
|
return {
|
|
'size': len(self._cache),
|
|
'max_size': self.max_size,
|
|
'hits': self._stats['hits'],
|
|
'misses': self._stats['misses'],
|
|
'sets': self._stats['sets'],
|
|
'evictions': self._stats['evictions'],
|
|
'hit_rate': hit_rate,
|
|
'total_requests': total_requests
|
|
}
|
|
|
|
def reset_stats(self):
|
|
"""Reset statistics counters"""
|
|
self._stats = {
|
|
'hits': 0,
|
|
'misses': 0,
|
|
'sets': 0,
|
|
'evictions': 0
|
|
}
|