diff --git a/backend/app/api/routes.py b/backend/app/api/routes.py index 6c66e06..f9d1fcc 100644 --- a/backend/app/api/routes.py +++ b/backend/app/api/routes.py @@ -12,7 +12,7 @@ from app.search import search, get_autocomplete from app.engines import registry from app.excluded import get_excluded_domains, add_excluded_domain, remove_excluded_domain from app.settings import get_all_settings, get_setting, set_setting -from app.cache import is_cache_available, flush_cache +from app.cache import is_cache_available, flush_cache, reconnect_redis router = APIRouter() @@ -236,10 +236,12 @@ async def api_remove_excluded_domain(domain: str): class SettingsResponse(BaseModel): cache_ttl_hours: float = Field(description="Cache TTL in hours (0 = disabled, max 168 = 1 week)") cache_available: bool = Field(description="Whether Redis is connected and available") + redis_url: str = Field(default="", description="Redis connection URL (e.g. redis://localhost:6379)") class UpdateSettingsRequest(BaseModel): - cache_ttl_hours: float = Field(ge=0, le=168, description="Cache TTL in hours (0 = disabled, max 168 = 1 week)") + cache_ttl_hours: float | None = Field(default=None, ge=0, le=168, description="Cache TTL in hours (0 = disabled, max 168 = 1 week)") + redis_url: str | None = Field(default=None, description="Redis connection URL (empty string to disconnect)") @router.get( @@ -260,6 +262,7 @@ async def api_get_settings(): return SettingsResponse( cache_ttl_hours=float(settings.get("cache_ttl_hours", "6")), cache_available=is_cache_available(), + redis_url=settings.get("redis_url", ""), ) @@ -279,10 +282,16 @@ curl -X PUT '$BASE_URL/api/settings' \\ tags=["Settings"], ) async def api_update_settings(body: UpdateSettingsRequest): - set_setting("cache_ttl_hours", str(body.cache_ttl_hours)) + if body.cache_ttl_hours is not None: + set_setting("cache_ttl_hours", str(body.cache_ttl_hours)) + if body.redis_url is not None: + set_setting("redis_url", body.redis_url) + await reconnect_redis(body.redis_url) + settings = get_all_settings() return SettingsResponse( - cache_ttl_hours=body.cache_ttl_hours, + cache_ttl_hours=float(settings.get("cache_ttl_hours", "6")), cache_available=is_cache_available(), + redis_url=settings.get("redis_url", ""), ) diff --git a/backend/app/cache.py b/backend/app/cache.py index 720cf3c..30b638c 100644 --- a/backend/app/cache.py +++ b/backend/app/cache.py @@ -22,11 +22,25 @@ _available: bool = False async def init_redis() -> None: - """Try to connect to Redis. If it fails, caching is silently disabled.""" + """Try to connect to Redis using DB setting first, then env var fallback.""" + from app.settings import get_setting + url = get_setting("redis_url") or os.environ.get("REDIS_URL", "") + await reconnect_redis(url) + + +async def reconnect_redis(url: str) -> None: + """Connect (or reconnect) to Redis at the given URL.""" global _redis, _available - url = os.environ.get("REDIS_URL", "") + # Close existing connection first + if _redis: + try: + await _redis.aclose() + except Exception: + pass + _redis = None + _available = False if not url: - logger.info("REDIS_URL not set — caching disabled") + logger.info("Redis URL not set — caching disabled") return try: _redis = aioredis.from_url(url, decode_responses=True, socket_connect_timeout=3) diff --git a/backend/app/settings.py b/backend/app/settings.py index b1ca2b4..5947a55 100644 --- a/backend/app/settings.py +++ b/backend/app/settings.py @@ -11,6 +11,7 @@ logger = logging.getLogger(__name__) # Default values DEFAULTS: dict[str, str] = { "cache_ttl_hours": "6", + "redis_url": "", } diff --git a/backend/data/hey_search.db b/backend/data/hey_search.db index 2fcdcb6..8efd354 100644 Binary files a/backend/data/hey_search.db and b/backend/data/hey_search.db differ diff --git a/frontend/src/components/SettingsModal.tsx b/frontend/src/components/SettingsModal.tsx index fcef7c7..57bb037 100644 --- a/frontend/src/components/SettingsModal.tsx +++ b/frontend/src/components/SettingsModal.tsx @@ -246,12 +246,16 @@ function CacheTab() { const [flushing, setFlushing] = useState(false); const [flushMsg, setFlushMsg] = useState(""); const [ttl, setTtl] = useState(6); + const [redisUrl, setRedisUrl] = useState(""); + const [urlSaving, setUrlSaving] = useState(false); + const [urlMsg, setUrlMsg] = useState(""); useEffect(() => { getSettings() .then((s) => { setSettings(s); setTtl(s.cache_ttl_hours); + setRedisUrl(s.redis_url); }) .finally(() => setLoading(false)); }, []); @@ -269,6 +273,20 @@ function CacheTab() { } }; + const handleUrlSave = async () => { + setUrlSaving(true); + setUrlMsg(""); + try { + const updated = await updateSettings({ redis_url: redisUrl.trim() }); + setSettings(updated); + setUrlMsg(updated.cache_available ? "Connected ✓" : redisUrl.trim() ? "Connection failed" : "Disconnected"); + } catch { + setUrlMsg("Failed to save"); + } finally { + setUrlSaving(false); + } + }; + const handleFlush = async () => { setFlushing(true); setFlushMsg(""); @@ -286,22 +304,46 @@ function CacheTab() { return (
+ {/* Redis URL configuration */}
-

- {settings?.cache_available - ? "Redis is connected. Repeated searches are served from cache." - : "Redis is not configured. Set REDIS_URL to enable caching."} +

+ Enter a Redis URL to enable search result caching (e.g. redis://localhost:6379).

-
+
+ { setRedisUrl(e.target.value); setUrlMsg(""); }} + onKeyDown={(e) => { if (e.key === "Enter") handleUrlSave(); }} + placeholder="redis://host:port" + className="flex-1 rounded-lg border bg-background px-3 py-2 text-sm focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none" + aria-label="Redis URL" + /> + +
+
- {settings?.cache_available ? "Connected" : "Disconnected"} + + {settings?.cache_available ? "Connected" : "Disconnected"} + + {urlMsg && ( + + — {urlMsg} + + )}
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 5161d80..9605728 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -146,6 +146,7 @@ export async function removeExcludedDomain(domain: string): Promise { export interface AppSettings { cache_ttl_hours: number; cache_available: boolean; + redis_url: string; } export async function getSettings(): Promise { @@ -154,7 +155,7 @@ export async function getSettings(): Promise { return resp.json(); } -export async function updateSettings(settings: { cache_ttl_hours: number }): Promise { +export async function updateSettings(settings: { cache_ttl_hours?: number; redis_url?: string }): Promise { const resp = await fetch(`${API_BASE}/settings`, { method: "PUT", headers: { "Content-Type": "application/json" },