From 09d70af58f5efbbd01857916b7a3a381ef8f2471 Mon Sep 17 00:00:00 2001 From: Travis Vasceannie Date: Sat, 24 Jan 2026 11:46:35 +0000 Subject: [PATCH] feat: Prevent duplicate secure storage unavailability warnings, include segments in meeting listings, and simplify ORM converter docstrings. --- .../features/analytics/entities-tab.tsx | 7 +++- client/src/lib/storage/crypto.ts | 23 +++++++---- client/src/pages/Home.tsx | 2 +- client/src/pages/Meetings.tsx | 1 + .../converters/orm_converters.py | 40 +++++-------------- 5 files changed, 33 insertions(+), 40 deletions(-) diff --git a/client/src/components/features/analytics/entities-tab.tsx b/client/src/components/features/analytics/entities-tab.tsx index 827229c..03ee730 100644 --- a/client/src/components/features/analytics/entities-tab.tsx +++ b/client/src/components/features/analytics/entities-tab.tsx @@ -132,7 +132,12 @@ export function EntitiesTab({ entityAnalytics }: EntitiesTabProps) { labelLine={false} > {categoryData.map((entry) => ( - + ))} { } } +// Track if we've already logged the unavailable warning to avoid spam +let hasLoggedUnavailable = false; + export function isSecureStorageAvailable(): boolean { try { const hasWindow = typeof window !== 'undefined'; @@ -334,11 +337,12 @@ export function isSecureStorageAvailable(): boolean { const available = hasWindow && hasCrypto && hasSubtle && hasGetRandomValues; - if (!available && hasWindow) { + if (!available && hasWindow && !hasLoggedUnavailable) { + hasLoggedUnavailable = true; addClientLog({ level: 'warning', source: 'app', - message: 'Secure storage availability check failed', + message: 'Secure storage unavailable - crypto.subtle requires secure context (HTTPS or localhost)', metadata: { hasWindow: String(hasWindow), hasCrypto: String(hasCrypto), @@ -351,12 +355,15 @@ export function isSecureStorageAvailable(): boolean { return available; } catch (error) { - addClientLog({ - level: 'warning', - source: 'app', - message: 'Secure storage availability check threw', - details: error instanceof Error ? error.message : String(error), - }); + if (!hasLoggedUnavailable) { + hasLoggedUnavailable = true; + addClientLog({ + level: 'warning', + source: 'app', + message: 'Secure storage availability check threw', + details: error instanceof Error ? error.message : String(error), + }); + } return false; } } diff --git a/client/src/pages/Home.tsx b/client/src/pages/Home.tsx index 59f77a8..5b7d71a 100644 --- a/client/src/pages/Home.tsx +++ b/client/src/pages/Home.tsx @@ -27,7 +27,7 @@ export default function HomePage() { queryKey: ['home', 'meetings'], queryFn: () => getAPI() - .listMeetings({ limit: HOME_RECENT_MEETINGS_LIMIT, sort_order: 'newest' }) + .listMeetings({ limit: HOME_RECENT_MEETINGS_LIMIT, sort_order: 'newest', include_segments: true }) .then((r) => r.meetings), staleTime: THIRTY_SECONDS_MS, }); diff --git a/client/src/pages/Meetings.tsx b/client/src/pages/Meetings.tsx index d6d719b..6a5a675 100644 --- a/client/src/pages/Meetings.tsx +++ b/client/src/pages/Meetings.tsx @@ -75,6 +75,7 @@ export default function MeetingsPage() { states: stateFilter === 'all' ? undefined : [stateFilter as MeetingState], project_id: projectScope === 'active' ? resolvedProjectId : undefined, project_ids: projectScope === 'selected' ? selectedIdsRef.current : undefined, + include_segments: true, }); if (append) { setMeetings((prev) => [...prev, ...response.meetings]); diff --git a/src/noteflow/infrastructure/converters/orm_converters.py b/src/noteflow/infrastructure/converters/orm_converters.py index 1566678..1052c1b 100644 --- a/src/noteflow/infrastructure/converters/orm_converters.py +++ b/src/noteflow/infrastructure/converters/orm_converters.py @@ -146,17 +146,7 @@ class OrmConverter: @staticmethod def word_timing_to_domain(model: WordTimingModel) -> DomainWordTiming: - """Convert ORM WordTiming model to domain entity. - - Args: - model: SQLAlchemy WordTimingModel instance. - - Returns: - Domain WordTiming entity. - - Raises: - ValueError: If timing validation fails during entity construction. - """ + """Convert ORM WordTiming model to domain entity.""" return DomainWordTiming( word=model.word, start_time=model.start_time, @@ -168,18 +158,7 @@ class OrmConverter: def word_timing_to_orm_kwargs( word: DomainWordTiming, word_index: int ) -> dict[str, str | float | int]: - """Convert domain WordTiming to ORM model kwargs. - - Return a dict of kwargs rather than instantiating WordTimingModel directly - to avoid circular imports and allow the repository to handle ORM construction. - - Args: - word: Domain WordTiming entity. - word_index: Position of word in the segment. - - Returns: - Dict with word, word_index, start_time, end_time, probability for ORM construction. - """ + """Convert domain WordTiming to ORM model kwargs (avoids circular imports).""" return { "word": word.word, "word_index": word_index, @@ -190,16 +169,16 @@ class OrmConverter: @staticmethod def meeting_to_domain(model: MeetingModel) -> Meeting: - """Convert ORM Meeting model to domain entity. + """Convert ORM Meeting model to domain entity (includes summary if loaded).""" + meeting_id = MeetingId(model.id) - Args: - model: SQLAlchemy MeetingModel instance. + # Convert summary if loaded (eager-loaded via selectin relationship) + summary: Summary | None = None + if model.summary is not None: + summary = OrmConverter.summary_to_domain(model.summary, meeting_id) - Returns: - Domain Meeting entity. - """ return Meeting( - id=MeetingId(model.id), + id=meeting_id, title=model.title, workspace_id=model.workspace_id, created_by_id=model.created_by_id, @@ -216,6 +195,7 @@ class OrmConverter: asset_path=model.asset_path, version=model.version, processing_status=OrmConverter.processing_status_from_payload(model.processing_status), + summary=summary, ) @staticmethod