feat(cache): thumbhash-validated asset cache + settings UX overhaul
Cache engine: - TelegramFileCache: configurable max_entries (LRU cap applies in both TTL and thumbhash modes), ttl_seconds<=0 disables TTL, stats() method. - Dispatcher builds an asset.id -> thumbhash resolver from event.added_assets (Immich populates thumbhash in extra) and passes it to TelegramClient, so asset-cache entries invalidate on visual change rather than age. - Watcher wires app settings into cache init: URL cache = TTL + LRU cap, asset cache = thumbhash + LRU cap. Adds soft-reset (in-memory only) used when cache params change. Settings: - New key telegram_asset_cache_max_entries (default 5000). - telegram_cache_ttl_hours default bumped 48 -> 720 (30d); now URL-only. - PUT /settings resets in-memory caches when cache keys change (files kept). - New endpoints: GET/POST /settings/telegram-cache/stats and /clear. Settings page: - Cache stats card (count + size + oldest/newest per bucket) with a hint explaining that the size is cumulative uploaded-to-Telegram bytes. - Clear-cache button behind a confirm modal. - New TimezoneSelector + LocaleSelector components replace raw inputs. - max-entries input, TTL range updated (0..8760, 0 = disabled). Mobile nav: - "More" panel now mirrors the full sidebar tree (groups + subnodes) so every destination is reachable on mobile; previously flat hand-picked list. - Nav height uses env(safe-area-inset-bottom); panel bottom + z-index fixed so content can't visually overlay the bottom bar. A11y / DOM warnings: - Password-change form has a hidden username field for password-manager association; autocomplete hints on all three password inputs. - Telegram webhook secret wrapped in a no-op form + autocomplete=off. Bug fix: - update_settings used any(await ... for ...) which raised TypeError at runtime (async generator not an iterator); replaced with explicit loop.
This commit is contained in:
@@ -20,7 +20,8 @@ router = APIRouter(prefix="/api/settings", tags=["settings"])
|
||||
_SETTING_KEYS = {
|
||||
"external_url": "NOTIFY_BRIDGE_EXTERNAL_URL",
|
||||
"telegram_webhook_secret": "NOTIFY_BRIDGE_TELEGRAM_WEBHOOK_SECRET",
|
||||
"telegram_cache_ttl_hours": None, # no env fallback, default 48
|
||||
"telegram_cache_ttl_hours": None, # URL cache TTL; 0 disables TTL
|
||||
"telegram_asset_cache_max_entries": None, # LRU cap for both caches
|
||||
"supported_locales": None, # comma-separated locale codes
|
||||
"timezone": "NOTIFY_BRIDGE_TIMEZONE", # IANA tz (e.g. "Europe/Warsaw"); empty = UTC
|
||||
}
|
||||
@@ -28,11 +29,18 @@ _SETTING_KEYS = {
|
||||
_DEFAULTS = {
|
||||
"external_url": "",
|
||||
"telegram_webhook_secret": "",
|
||||
"telegram_cache_ttl_hours": "48",
|
||||
# 720h = 30d. URL cache only; asset cache uses thumbhash validation
|
||||
# (content-addressable) and ignores TTL entirely.
|
||||
"telegram_cache_ttl_hours": "720",
|
||||
"telegram_asset_cache_max_entries": "5000",
|
||||
"supported_locales": "en,ru",
|
||||
"timezone": "UTC",
|
||||
}
|
||||
|
||||
# Settings whose changes require dropping in-memory Telegram caches so the
|
||||
# next dispatch rebuilds them with the new parameters. Files are preserved.
|
||||
_CACHE_SETTING_KEYS = {"telegram_cache_ttl_hours", "telegram_asset_cache_max_entries"}
|
||||
|
||||
|
||||
async def get_setting(session: AsyncSession, key: str) -> str:
|
||||
"""Read a setting from DB, falling back to env var then default."""
|
||||
@@ -51,6 +59,7 @@ class SettingsUpdate(BaseModel):
|
||||
external_url: str | None = None
|
||||
telegram_webhook_secret: str | None = None
|
||||
telegram_cache_ttl_hours: str | None = None
|
||||
telegram_asset_cache_max_entries: str | None = None
|
||||
supported_locales: str | None = None
|
||||
timezone: str | None = None
|
||||
|
||||
@@ -80,6 +89,7 @@ async def update_settings(
|
||||
"""Update app settings (admin). Re-registers webhooks when base URL changes."""
|
||||
old_base_url = await get_setting(session, "external_url")
|
||||
old_secret = await get_setting(session, "telegram_webhook_secret")
|
||||
old_cache_values = {k: await get_setting(session, k) for k in _CACHE_SETTING_KEYS}
|
||||
|
||||
for key in _SETTING_KEYS:
|
||||
value = getattr(body, key, None)
|
||||
@@ -93,6 +103,17 @@ async def update_settings(
|
||||
session.add(row)
|
||||
await session.commit()
|
||||
|
||||
# Drop in-memory caches if any cache-tuning setting actually changed, so
|
||||
# the next dispatch rebuilds them with the new parameters. Files survive.
|
||||
cache_changed = False
|
||||
for key in _CACHE_SETTING_KEYS:
|
||||
if await get_setting(session, key) != old_cache_values[key]:
|
||||
cache_changed = True
|
||||
break
|
||||
if cache_changed:
|
||||
from ..services.watcher import reset_telegram_caches_in_memory
|
||||
await reset_telegram_caches_in_memory()
|
||||
|
||||
new_base_url = await get_setting(session, "external_url")
|
||||
new_secret = await get_setting(session, "telegram_webhook_secret")
|
||||
|
||||
@@ -111,6 +132,25 @@ async def update_settings(
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/telegram-cache/stats")
|
||||
async def telegram_cache_stats(
|
||||
user: User = Depends(require_admin),
|
||||
):
|
||||
"""Return counts and sizes for the Telegram file_id caches."""
|
||||
from ..services.watcher import get_telegram_cache_stats
|
||||
return await get_telegram_cache_stats()
|
||||
|
||||
|
||||
@router.post("/telegram-cache/clear")
|
||||
async def clear_telegram_cache(
|
||||
user: User = Depends(require_admin),
|
||||
):
|
||||
"""Clear the Telegram file_id cache (URL and asset) from disk and memory."""
|
||||
from ..services.watcher import clear_telegram_caches
|
||||
result = await clear_telegram_caches()
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/locales")
|
||||
async def get_supported_locales(
|
||||
user: User = Depends(get_current_user),
|
||||
|
||||
Reference in New Issue
Block a user