Files
notify-bridge/frontend/src/lib/highlight.ts
T
alexei.dolgolyov e0bae394ee feat: comprehensive code review fixes — security, performance, quality
Backend security:
- Reject Gitea webhooks when webhook_secret is empty (was silently skipping)
- Add slowapi rate limiting on login (5/min) and setup (3/min) endpoints
- Add CORS middleware with configurable origins
- Mask telegram_webhook_secret in settings API response
- Protect system-owned command template configs from regular user modification
- Increase minimum password length to 8 characters

Backend performance:
- Batch queries in _resolve_command_context (3 queries instead of 3N)
- Concurrent album fetching with asyncio.gather in immich commands
- Singleton Jinja2 SandboxedEnvironment (reuse instead of per-render creation)
- TTLCache for rate limits (bounded memory, auto-eviction)
- Optional aiohttp session reuse in send_reply/send_media_group

Backend code quality:
- Extract dispatch_helpers.py (shared link_data loading + event filtering)
- Extract database/seeds.py from main.py (490 lines → dedicated module)
- Split immich_handler.py (415 lines) into commands/immich/ subpackage
- Replace bare except blocks with logged warnings
- Add per-provider config validation (Pydantic models)
- Truncate command input to 512 chars
- Expose usage_* and desc_* slots in capabilities and variables API

Frontend security:
- CSS.escape() for user-controlled querySelector in highlight.ts
- Client-side password min 8 chars validation on setup and password change

Frontend code quality:
- Replace any types with proper interfaces across top files
- Decompose targets/+page.svelte into TargetForm + ReceiverSection
- Fix $derived.by usage, $state mutation patterns
- Add console.warn to empty catch blocks

Frontend UX:
- Auth redirect via goto() with "Redirecting..." state
- Platform-aware Ctrl/Cmd K keyboard hint
- Remove stat-card hover transform

Frontend accessibility:
- Modal: role=dialog, aria-modal, focus trap, restore focus
- EntitySelect/IconGridSelect: listbox/option roles, aria-selected/disabled
2026-03-23 01:59:51 +03:00

106 lines
3.0 KiB
TypeScript

/**
* Card highlight system for cross-entity navigation.
*
* CrossLink/SearchPalette call requestHighlight(id) before goto().
* The destination page calls highlightFromUrl() after data loads, which
* picks up the pending ID and highlights the matching card with a
* smooth CSS keyframe glow + dim overlay.
*/
const HIGHLIGHT_DURATION = 2000;
const WAIT_TIMEOUT = 5000;
/** Pending highlight ID — set before navigation. */
let _pendingHighlight: string | null = null;
/** Request a card highlight. Called before goto(). */
export function requestHighlight(id: string | number): void {
_pendingHighlight = String(id);
}
/** Check for pending highlight, find card, scroll & highlight. Call after data loads. */
export function highlightFromUrl(): void {
if (typeof window === 'undefined') return;
// Check global pending first, then URL param as fallback
let id = _pendingHighlight;
_pendingHighlight = null;
if (!id) {
const params = new URLSearchParams(window.location.search);
id = params.get('highlight');
if (id) {
params.delete('highlight');
const qs = params.toString();
const cleanUrl = window.location.pathname + (qs ? '?' + qs : '');
window.history.replaceState(null, '', cleanUrl);
}
}
if (!id) return;
// Wait for DOM to render after loaded=true
requestAnimationFrame(() => {
requestAnimationFrame(() => {
const card = document.querySelector(`[data-entity-id="${CSS.escape(id)}"]`);
if (card) {
_highlightCard(card as HTMLElement);
} else {
_waitForCard(id!);
}
});
});
}
function _highlightCard(card: HTMLElement): void {
const overlay = _showDimOverlay();
// Scroll to card
card.scrollIntoView({ behavior: 'smooth', block: 'center' });
// Apply highlight via inline style (overrides stagger CSS class animation)
card.style.animation = 'cardHighlight 2s ease-in-out';
card.style.position = 'relative';
card.style.zIndex = '11';
// Cleanup: set animation to 'none' (inline beats class, prevents stagger replay)
setTimeout(() => {
card.style.animation = 'none';
card.style.removeProperty('position');
card.style.removeProperty('z-index');
overlay.classList.remove('active');
setTimeout(() => overlay.remove(), 300);
}, HIGHLIGHT_DURATION);
}
function _showDimOverlay(): HTMLElement {
let overlay = document.querySelector('.nav-dim-overlay') as HTMLElement | null;
if (!overlay) {
overlay = document.createElement('div');
overlay.className = 'nav-dim-overlay';
document.body.appendChild(overlay);
}
void overlay.offsetHeight;
overlay.classList.add('active');
return overlay;
}
function _waitForCard(id: string): void {
const start = Date.now();
const observer = new MutationObserver(() => {
const card = document.querySelector(`[data-entity-id="${CSS.escape(id)}"]`);
if (card) {
observer.disconnect();
setTimeout(() => _highlightCard(card as HTMLElement), 50);
return;
}
if (Date.now() - start > WAIT_TIMEOUT) {
observer.disconnect();
}
});
observer.observe(document.body, { childList: true, subtree: true });
setTimeout(() => observer.disconnect(), WAIT_TIMEOUT);
}