mirror of
https://github.com/wahyd4/hey-search.git
synced 2026-08-08 21:05:14 +10:00
feat: configure Redis URL via UI settings
- Add redis_url to SQLite settings table (persisted across restarts) - On startup, prefer DB-stored URL over REDIS_URL env var - Add reconnect_redis() to support live URL changes without restart - Settings API now accepts/returns redis_url field - Cache tab shows editable URL input with save button and connection status - Immediate feedback: shows Connected/Disconnected after save Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -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", ""),
|
||||
)
|
||||
|
||||
|
||||
|
||||
+17
-3
@@ -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)
|
||||
|
||||
@@ -11,6 +11,7 @@ logger = logging.getLogger(__name__)
|
||||
# Default values
|
||||
DEFAULTS: dict[str, str] = {
|
||||
"cache_ttl_hours": "6",
|
||||
"redis_url": "",
|
||||
}
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
@@ -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 (
|
||||
<div className="space-y-5">
|
||||
{/* Redis URL configuration */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Database className="h-4 w-4 text-muted-foreground" aria-hidden="true" />
|
||||
<p className="text-sm font-medium">Redis Cache</p>
|
||||
<p className="text-sm font-medium">Redis Connection</p>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
{settings?.cache_available
|
||||
? "Redis is connected. Repeated searches are served from cache."
|
||||
: "Redis is not configured. Set REDIS_URL to enable caching."}
|
||||
<p className="text-xs text-muted-foreground mb-2">
|
||||
Enter a Redis URL to enable search result caching (e.g. redis://localhost:6379).
|
||||
</p>
|
||||
<div className={cn("rounded-lg border p-1 inline-flex items-center", !settings?.cache_available && "opacity-50 pointer-events-none")}>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="url"
|
||||
value={redisUrl}
|
||||
onChange={(e) => { 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"
|
||||
/>
|
||||
<button
|
||||
onClick={handleUrlSave}
|
||||
disabled={urlSaving}
|
||||
className="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50 focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
|
||||
>
|
||||
{urlSaving ? "Saving…" : "Save"}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<span className={cn(
|
||||
"inline-block h-2 w-2 rounded-full mr-2 ml-1",
|
||||
"inline-block h-2 w-2 rounded-full",
|
||||
settings?.cache_available ? "bg-green-500" : "bg-muted-foreground"
|
||||
)} />
|
||||
<span className="text-xs text-muted-foreground mr-2">{settings?.cache_available ? "Connected" : "Disconnected"}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{settings?.cache_available ? "Connected" : "Disconnected"}
|
||||
</span>
|
||||
{urlMsg && (
|
||||
<span className={cn("text-xs font-medium", urlMsg.includes("✓") ? "text-green-600 dark:text-green-400" : urlMsg === "Disconnected" ? "text-muted-foreground" : "text-destructive")}>
|
||||
— {urlMsg}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -146,6 +146,7 @@ export async function removeExcludedDomain(domain: string): Promise<string[]> {
|
||||
export interface AppSettings {
|
||||
cache_ttl_hours: number;
|
||||
cache_available: boolean;
|
||||
redis_url: string;
|
||||
}
|
||||
|
||||
export async function getSettings(): Promise<AppSettings> {
|
||||
@@ -154,7 +155,7 @@ export async function getSettings(): Promise<AppSettings> {
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
export async function updateSettings(settings: { cache_ttl_hours: number }): Promise<AppSettings> {
|
||||
export async function updateSettings(settings: { cache_ttl_hours?: number; redis_url?: string }): Promise<AppSettings> {
|
||||
const resp = await fetch(`${API_BASE}/settings`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
||||
Reference in New Issue
Block a user